第四章 图论(4):SPFA求负环、差分约束、LCA

目录

  • 一、SPFA求负环
    • 1.0 SPFA判断负环
    • 1.1 虫洞
    • 1.2 观光奶牛 (spfa && 01分数规划)
    • 1.3 单词环
  • 二、差分约束
    • 2.1 糖果
    • 2.2 区间
    • 2.3 排队布局
    • 2.4 雇佣收银员
    • 2.5 再卖菜
  • 三、最近公共祖先 (LCA)
    • 3.1 祖孙询问 (倍增法)
    • 3.2 距离 (Tarjan算法)
    • 3.3 次小生成树
    • 3.4 暗之连锁

一、SPFA求负环

一般会和01分数规划结合

负环:一个环且环上所有权值之和小于零

负环对最短路径的影响:如果在求最短路径的过程中走进了负环,每在负环里面旋转一圈,总的权值就会减少,所有会导致路径无限旋转。

求负环的方法,基于SPFA(Bellman_Ford效率比较差),更推荐方法二

  • 方法一(基于Bellman_Ford): 统计每个点入队的次数,如果某个点入队n,则存在负环;
  • 方法二:统计当前每个点的最短路中所包含的边数,如果某个点的最短路所包含的边数大于等于n,则说明存在负环。

有一个经验,可能在平时使用SPFA的时候会被超时卡掉,但是我们可以认为:当所有点的入队次数超过2n时,图中有很大可能是存在负环的。

1.0 SPFA判断负环

ACWing 852

算法思路:

  • dist[x]记录从起始结点到x结点的最短距离
  • cnt[x]表示当前从起始节点到当前结点x的最短路径的边数

每次更新dist[x]的时候,将cnt[x] ++; 。当cnt[x] >= n时,证明从起始节点到x结点的最短路径经过了n条边,即有n+1个结点,但是图总共仅有n个结点,所以至少有两个结点相同,即存在环,因为环上结点的dist[]在遍历过程中会不停的往-∞变小,所以一定是一个负环,故证得存在负环。

注意:

  • 本题不需要对dist数组进行初始化,因为求的是否存在负环,而不是距离的值;
  • 代码最开始元素入队列,由于本题是找是否存在负环,而不是找是否存在从1开始的负环,所以最开始元素入队列的时候不能仅将1入队列,而且从1开始也可能到不了负环,所以最开始应将所有结点加入队列。
#include 
#include 
#include 
#include 

using namespace std;

const int N = 1e5 + 10;

int n, m;
int h[N], e[N], w[N], ne[N], idx;
int dist[N], cnt[N];
bool st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int spfa() {
    queue<int> q;
    for (int i = 1; i <= n; i++)
        st[i] = true, q.push(i);
    while (q.size()) {
        int t = q.front(); q.pop();
        st[t] = false;
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;
                if (!st[j])
                    q.push(j), st[j] = true;
            }
        }
    }
    return false;
}

int main() {
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    while (m--) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    if (spfa()) puts("Yes");
    else puts("No");
    return 0;
}

1.1 虫洞

ACwing 904

#include 
#include 
#include 

using namespace std;

const int N = 510, M = 5210; // 单向边200条,双向变2500条

int n, m1, m2;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int q[N], cnt[N]; // cnt当前最短路径长度
bool st[N];

void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; }

bool spfa() {
    memset(dist, 0, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);
    int hh = 0, tt = 0;
    for (int i = 1; i <= n; i++) {
        q[tt++] = i;
        st[i] = true;
    }
    while (hh != tt) {
        int t = q[hh++];
        if (hh == N) hh = 0; // 循环队列判断是否走到终点
        st[t] = false;
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;
                if (!st[j]) {
                    q[tt++] = j; if (tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        scanf("%d%d%d", &n, &m1, &m2);
        memset(h, -1, sizeof h);
        idx = 0;
        for (int i = 0; i < m1; i++) {
            int a, b, c; scanf("%d%d%d", &a, &b, &c);
            add(a, b, c), add(b, a, c);
        }
        for (int i = 0; i < m2; i++) {
            int a, b, c; scanf("%d%d%d", &a, &b, &c);
            add(a, b, -c); // 虫洞是负向边
        }
        if (spfa()) puts("YES");
        else puts("NO");
    }
    return 0;
}

1.2 观光奶牛 (spfa && 01分数规划)

ACwing 361

由题,假设点上权值记为 f i f_i fi,边上权值记为 t j t_j tj,那么题目即求 m a x ∑ i = 1 L f i ∑ j = 1 P t j max \frac{\sum_{i=1}^L f_i}{\sum_{j=1}^P t_j} maxj=1Ptji=1Lfi我们将在图论中求解形如 ∑ f i ∑ t j \frac{\sum f_i}{\sum t_j} tjfi的值的问题称为01分数规划

求解01分数规划问题的方法:二分
由题可知 L ∈ [ 2 , 1000 ] L \in [2, 1000] L[2,1000] P ∈ [ 2 , 5000 ] P \in [2, 5000] P[2,5000]。那么 ∑ f i ∑ t j ∈ ( 0 , 1000 ] \frac{\sum f_i}{\sum t_j} \in (0, 1000] tjfi(0,1000]。我们可以在区间 ∈ ( 0 , 1000 ] \in (0, 1000] (0,1000]上取一个值 m i d mid mid,判断在图中是否存在一个环,使图中 ∑ f i ∑ t j > m i d \frac{\sum f_i}{\sum t_j} > mid tjfi>mid,根据这个判断结果可以将区间缩小到 m i d mid mid的左边或者右边。

求解 ∑ f i ∑ t j > m i d \frac{\sum f_i}{\sum t_j} > mid tjfi>mid
上式变形有: ∑ f i − m i d × ∑ t i > 0 \sum f_i - mid \times \sum t_i > 0 fimid×ti>0,即需要先计算一个环中的所有点权和边权。在求最短路径过程中,如果出现了点权和边权,可以将点权放到出边 (或者入边) 上,与边上权值求和构成新的边权,与分别求边权、点权的和的效果等价。经过上述操作后,上面的式子就可以转换为: ∑ ( f i − m i d × t i ) > 0 \sum (f_i - mid \times t_i) > 0 (fimid×ti)>0,这个问题就变成了:图形中是否存在一个环,使其环上的权值之和大于0,即图中是否存在正环。求解正环可以将图中的所有边的权值改为负值,即变为求解是否存在负环。但是实际过程中,可以不用这样操作,更简单的做法是将求负环中的最短路修改为求最长路径,然后统计最长路径中包含的边数是否大于等于图中所有点数之和即可。

这个题的建图方法,让我容易联想到 最小生成树 中的 6、新的开始 一题。

#include 
#include 
#include 

using namespace std;

const int N = 1010, M = 5010;

int n, m;
int wf[N], wt[M]; // 存储点权、边权
int h[N], e[M], ne[M], idx;
double dist[N];
int q[N], cnt[N];
bool st[N];

void add(int a, int b, int c) {
    e[idx] = b, wt[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

bool check(double mid) { // 图中是否存在正环
    memset(dist, 0, sizeof dist);
    memset(st, 0, sizeof st);
    memset(cnt, 0, sizeof cnt);
    int hh = 0, tt = 0;
    for (int i = 1; i <= n; i++) {
        q[tt++] = i;
        st[i] = true;
    }
    while (hh != tt) {
        int t = q[hh++];
        if (hh == N) hh = 0;
        st[t] = false;
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] < dist[t] + wf[t] - mid * wt[i]) { // 边的权值发生了变化
                dist[j] = dist[t] + wf[t] - mid * wt[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;
                if (!st[j]) {
                    q[tt++] = j; // 循环队列加入元素
                    if (tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main() {
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i++) cin >> wf[i]; // 读入点权
    for (int j = 0; j < m; j++) { // 读入边权
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }

    double l = 0, r = 1000;
    // double精度保证:保留两位小数精确到1e-4,保留三位小数精确到1e-5
    while (r - l > 1e-4) {
        double mid = (l + r) / 2;
        if (check(mid)) l = mid;
        else r = mid;
    }
    printf("%.2lf\n", l);
    return 0;
}

1.3 单词环

ACwing 1165

题目类似文章 第三章 搜索(2)DFS 2.4 单词接龙,不同的是,这个题是求一个环。还有建图方法,单词接龙是将每个单词看成一个点,使用单词a后缀和单词b前缀的最小公共长度为权值(为使最后拼接长度最长)建立边

建图方法
因为每个单词只有首尾两个单词起作用,将每一个单词看成一条边,比如ababc,我们建立一条边ab → bc边上权值为字母长度5。比如三个单词ababc、bckjaca、caahoynaab可以建图:第四章 图论(4):SPFA求负环、差分约束、LCA_第1张图片
假设所有边上权值记为 w i w_i wi,那么原问题转换为了求解 m a x { ∑ w i ∑ 1 } max\{ \frac{\sum w_i}{\sum 1} \} max{1wi},其中 ∑ 1 \sum 1 1表示所有的字符串数目,即图中点的个数。

利用上一题01分数规划的思路,因为 ∑ w i ∑ 1 ∈ ( 0 , 1000 ] \frac{\sum w_i}{\sum 1} \in (0,1000] 1wi(0,1000],在区间中取一个点 m i d mid mid,每一次判断图中是否存在一个环满足 ∑ w i ∑ 1 > m i d \frac{\sum w_i}{\sum 1} > mid 1wi>mid,将式子变形有 ∑ ( w i − m i d × 1 ) > 0 \sum (w_i - mid \times 1) > 0 (wimid×1)>0,我们将边上的权值重新定义为 w i − m i d × 1 w_i - mid \times 1 wimid×1,其中 m i d ∈ ( 0 , 1000 ] mid \in (0, 1000] mid(0,1000],则原问题就转成了图中是否存在一个环使其权值大于零,即是否存在一个正环

因为所有边的权重只有越大的时候才会可能出现正环,且重新定义的边的权重为 w i − m i d × 1 w_i - mid \times 1 wimid×1。若 m i d = 0 mid=0 mid=0的时候,图中不存在正环,那么当 m i d > 0 mid > 0 mid>0的时候,权重 w i − m i d × 1 w_i - mid \times 1 wimid×1就会更小,图中就更不可能出现正环。因此实际计算的时候可以直接将 m i d = 0 mid=0 mid=0带入,即 m i d ∈ [ 0 , 1000 ] mid \in [0,1000] mid[0,1000]

#include 
#include 
#include 

using namespace std;

// 因为每个点上两个字符,每个字符有26种可能,所以每个点有26*26=676种可能,即有676个点
// M为边数,即字符串的个数
const int N = 700, M = 100010;

int n;
int h[N], e[M], w[M], ne[M], idx;
double dist[N];
int q[N], cnt[N];
bool st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

bool check(double mid) {
    memset(st, 0, sizeof st);
    memset(cnt, 0, sizeof cnt);
    int hh = 0, tt = 0;
    for (int i = 0; i < 676; i++) {
        q[tt++] = i;
        st[i] = true;
    }
    int count = 0; // 统计所有点被更新的总次数
    while (hh != tt) {
        int t = q[hh++];
        if (hh == N) hh = 0;
        st[t] = false;
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] < dist[t] + w[i] - mid) {
                dist[j] = dist[t] + w[i] - mid;
                cnt[j] = cnt[t] + 1;
                if (++count > 10000) return true; // 经验上的trick
                if (cnt[j] >= N) return true; // 注意这里的N
                if (!st[j]) {
                    q[tt++] = j;
                    if (tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main() {
    char str[1010];
    while (scanf("%d", &n), n) {
        memset(h, -1, sizeof h);
        idx = 0;
        for (int i = 0; i < n; i++) { // 读入每个一字符串
            scanf("%s", str);
            int len = strlen(str);
            if (len >= 2) { // 必须大于2
                int left = (str[0] - 'a') * 26 + str[1] - 'a'; // 将首尾两个字符其当成26进制数
                int right = (str[len - 2] - 'a') * 26 + str[len - 1] - 'a';
                add(left, right, len);
            }
        }
        if (!check(0)) puts("No solution"); // 首先判断0
        else {
            double l = 0, r = 1000;
            while (r - l > 1e-4) { // 精度问题同上题
                double mid = (l + r) / 2;
                if (check(mid)) l = mid;
                else r = mid;
            }

            printf("%lf\n", r);
        }
    }
    return 0;
}

对于判断图中是否有负环的问题,除了加一个trick,也可以考虑将代码中的队列换成栈,效果也不错:

#include 
#include 
#include 

using namespace std;

// 因为每个点上两个字符,每个字符有26种可能,所以每个点有26*26=676种可能,即有676个点
// M为边数,即字符串的个数
const int N = 700, M = 100010;

int n;
int h[N], e[M], w[M], ne[M], idx;
double dist[N];
int q[N], cnt[N];
bool st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

bool check(double mid) {
    memset(st, 0, sizeof st);
    memset(cnt, 0, sizeof cnt);
    int hh = 0, tt = 0; 
    for (int i = 0; i < 676; i++) {
        q[tt++] = i;
        st[i] = true;
    }
    while (hh != tt) {
        int t = q[--tt]; // 变成了栈
        st[t] = false;
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] < dist[t] + w[i] - mid) {
                dist[j] = dist[t] + w[i] - mid;
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= N) return true;
                if (!st[j]) {
                    q[tt++] = j;
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main() {
    char str[1010];
    while (scanf("%d", &n), n) {
        memset(h, -1, sizeof h);
        idx = 0;
        for (int i = 0; i < n; i++) { // 读入每个一字符串
            scanf("%s", str);
            int len = strlen(str);
            if (len >= 2) { // 必须大于2
                int left = (str[0] - 'a') * 26 + str[1] - 'a'; // 将首尾两个字符其当成26进制数
                int right = (str[len - 2] - 'a') * 26 + str[len - 1] - 'a';
                add(left, right, len);
            }
        }
        if (!check(0)) puts("No solution"); // 首先判断0
        else {
            double l = 0, r = 1000;
            while (r - l > 1e-4) { // 精度问题同上题
                double mid = (l + r) / 2;
                if (check(mid)) l = mid;
                else r = mid;
            }
            printf("%lf\n", r);
        }
    }
    return 0;
}

二、差分约束

应用1

求不等式组的可行解。对于不等式组中,每一个不等式都是形如 x i ≤ x j + c k x_i \le x_j + c_k xixj+ck,其中 x i 、 x j x_i、x_j xixj均为自变量, c k c_k ck为常量,则差分约束可以求出该组不等式的一组可行解。

证明

假设图中存在一条边j → i,边上权值为c,图中存在最短路且不存在负环。在求完最短路之后,就存在关系 d i s t ( i ) ≤ d i s t ( j ) + c dist(i) \le dist(j) + c dist(i)dist(j)+c (如果 d i s t ( i ) > d i s t ( j ) + c dist(i) > dist(j) + c dist(i)>dist(j)+c 的话,那么可以使用j → i这条边来继续更新 d i s t ( i ) dist(i) dist(i) ),也就是说对于每一条边,当求完最短路后且图中不存在负环,都会满足上面的不等式。

如果将图中每一条边都看成不等式,即 d i s t ( i ) dist(i) dist(i)记为 x i x_i xi d i s t ( j ) dist(j) dist(j)记为 x j x_j xj,那么在求完该图的最短路径后,且不存在负环,该图上的边均满足: x i ≤ x j + c k x_i \le x_j + c_k xixj+ck。因此任何一个图的最短路问题都可以变成一个不等式组的问题;反过来,对于不等式组中的任何一个不等式来说,都可以变成求完最短路后且不含负环的图的一条边。

第四章 图论(4):SPFA求负环、差分约束、LCA_第2张图片

细节:原点需要满足的条件(最长路、最短路求可行解都必须满足)——从原点出发,一定可以到所有的边。

求可行解的步骤

  1. 先将不等式 x i ≤ x j + c k x_i \le x_j + c_k xixj+ck 转化成一条从 x j x_j xj走到 x i x_i xi且权值为 c k c_k ck的一条边;
  2. 找一个超级源点,使得从该点出发一定可以遍历到所有的边;
  3. 从该源点出发求该图的最短路。
  4. 如果图中存在负环,那么原不等式无解;如果无负环,则 d i s t [ i ] dist[i] dist[i]为原不等式组的一个可行解。

如果图中不存在最短路径,即存在负环的情况

假设存在如下图
第四章 图论(4):SPFA求负环、差分约束、LCA_第3张图片

存在不等式 x 2 ≤ x 1 + c 1 x 1 ≤ x k + c k x 2 ≤ x 1 + c 1 ≤ x k + c k + c 1 . . . x 2 ≤ x 2 + c 2 + c 3 + . . . + c k + c 1 \begin{aligned} x_2 &\le x_1 + c_1\\ x_1 &\le x_k + c_k\\ x_2 &\le x_1 + c_1 \le x_k + c_k + c_1 \\ ... \\ x_2& \le x_2 + c_2 + c_3 + ... + c_k + c_1\\ \end{aligned} x2x1x2...x2x1+c1xk+ckx1+c1xk+ck+c1x2+c2+c3+...+ck+c1又因为是负环,所以 c 2 + c 3 + . . . + c k + c 1 < 0 c_2 + c_3 + ... + c_k + c_1 < 0 c2+c3+...+ck+c1<0,所以有 x 2 < x 2 x_2 < x_2 x2<x2矛盾,即如果图中存在负环,则不等式组就是矛盾的。

对于使用最长路径,则应该满足 d i s t ( i ) ≥ d i s t ( j ) + c k dist(i) \ge dist(j) + c_k dist(i)dist(j)+ck,即 x j ≤ x i − c k x_j \le x_i - c_k xjxick,在图中即为从i → j的一条边,且权值为 − c k -c_k ck。根据上面同样的道理进行放缩,可以得出:如果无解,即存在正环。

应用2

如何求可行解中的最大值和最小值,这里的最值指的是每个变量 x i x_i xi的最值。
结论:如果求的是最小值,则应该求最长路;如果求的是最大值,则应该求最短路。

对于求解最值,题目中一定会给一个绝对条件,如果像上面的不等式组一样,只有 x 1 、 x 2 、 . . . 、 x k x_1、x_2、...、x_k x1x2...xk之间的相对关系,而没给出一个如 x 0 ≥ 0 x_0 \ge 0 x00的绝对关系,那么是不能求出最值的。

问题:如果转化不等式中的一个绝对关系 x i ≤ c x_i \le c xic,其中 c c c是一个常数,如何将这类的不等式转化进图中?
方法:建立一个超级源点,比如 0 0 0点,然后建立0 → i且权值为c的一条边,则就有 x i ≤ x 0 + c x_i \le x_0 + c xix0+c

以求 x i x_i xi的最大值为例:所有从 x i x_i xi出发,构成的不等式链 x i ≤ x j + c 1 ≤ x k + c 2 + c 1 ≤ c 1 + c 2 + . . . x_i \le x_j + c_1 \le x_k + c_2 + c_1 \le c1 + c2 +... xixj+c1xk+c2+c1c1+c2+...所计算出的上界,最终 x i x_i xi的最大值等于所有上界中的最小值

对于每一条不等式链 x i ≤ x j + c 1 ≤ x k + c 2 + c 1 ≤ c 1 + c 2 + . . . x_i \le x_j + c_1 \le x_k + c_2 + c_1 \le c1 + c2 +... xixj+c1xk+c2+c1c1+c2+...,其在图中所对应的即为每一条从 0 0 0号点出发走到点 i i i的路径。比如有下图路径在这里插入图片描述
那么每条边就会存在不等式关系 x 1 ≤ x 0 + c 1 x 3 ≤ x 1 + c 3 . . . x i ≤ x i − 1 + c i − 1 \begin{aligned} x_1 &\le x_0 + c_1\\ x_3 &\le x_1 + c_3\\ ...\\ x_i &\le x_{i-1} + c_{i-1}\\ \end{aligned} x1x3...xix0+c1x1+c3xi1+ci1将所有不等式放缩后就有 x i ≤ c 1 + c 3 + . . . + c i − 1 x_i \le c1 + c3 + ... + c_{i-1} xic1+c3+...+ci1所有上界中的最小值,即为所有0 → i路径长度的最小值,即最短路径。

反之,如果求最小值,则应该对应每一个不等式的下界中的最大值,即最长路径。

一个总结:差分约束求最长路

  • 边权无限制:spfa,时间复杂度 O ( n m ) O(nm) O(nm),最坏 O ( k m ) O(km) O(km)
  • 边权非负:如果存在强连通分量,那么强连通分量里面的边权必须是0,否则无解,此时可以使用有向图的tarjan算法,时间复杂度 O ( n + m ) O(n+m) O(n+m)
  • 边权> 0:那么此图一定无环,有环必然无解,所以可以使用拓扑排序来求解,时间复杂度 O ( n + m ) O(n+m) O(n+m)

2.1 糖果

ACwing 1169

因为每一个小朋友都要分到糖果,因此可以找到绝对关系 x ≥ 1 x \ge 1 x1。建立超级源点 x 0 x_0 x0,所以 x ≥ 1 x \ge 1 x1可以转换为 x ≥ x 0 + 1 x \ge x_0 + 1 xx0+1

再分析题目后,将所有不等式关系转换为" ≥ \ge "后可以得到如下不等式组:

  • A = B: A ≥ B & & B ≥ A A \ge B \&\& B \ge A AB&&BA
  • A < B: B ≥ A + 1 B \ge A + 1 BA+1
  • A ≥ \ge B: A ≥ B A \ge B AB
  • A > B: A ≥ B + 1 A \ge B + 1 AB+1
  • A ≤ \le B: B ≥ A B \ge A BA
  • x i ≥ 1 x_i \ge 1 xi1 x i ≥ x 0 + 1 x_i \ge x_0 + 1 xix0+1

问题就转换为求每一个 x i x_i xi的最小值,然后求和即可。

前提条件检查;因为有 x ≥ x 0 + 1 x \ge x_0 + 1 xx0+1,即表示0 → i,从0可以走到任意点i,所以可以从原点走到任意边,满足条件(但是反之不一定成立,因为即便可以走到任意边,如果存在孤立点的话就不满足了)。

#include 
#include 
#include 

using namespace std;

typedef long long LL;

// 点数是N
// 边数是M,最坏情况下A=B,需要建立两个方向的边;同时还有虚拟原点到每个点的边,总共建立3倍的边
const int N = 100010, M = 300010;

int n, m;
int h[N], e[M], w[M], ne[M], idx;
LL dist[N];
int q[N], cnt[N]; // q表示栈,cnt用于求解正环
bool st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

bool spfa() {
    int hh = 0, tt = 1;
    memset(dist, -0x3f, sizeof dist); // 求的是最长路
    dist[0] = 0;
    q[0] = 0; // 超级源点入栈 与 全部结点入栈 效果一样
    st[0] = true;
    while (hh != tt) {
        int t = q[--tt];
        st[t] = false;
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] < dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n + 1) return false;
                if (!st[j]) {
                    q[tt++] = j;
                    st[j] = true;
                }
            }
        }
    }
    return true;
}

int main() {
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    while (m--) {
        int x, a, b;
        scanf("%d%d%d", &x, &a, &b);
        if (x == 1) add(b, a, 0), add(a, b, 0);
        else if (x == 2) add(a, b, 1);
        else if (x == 3) add(b, a, 0);
        else if (x == 4) add(b, a, 1);
        else add(a, b, 0);
    }
    for (int i = 1; i <= n; i++) // 从超级源点到每个点的边
        add(0, i, 1);

    if (!spfa()) puts("-1"); // 判断是否存在正环
    else {
        LL res = 0;
        for (int i = 1; i <= n; i++) res += dist[i];
        printf("%lld\n", res);
    }
    return 0;
}

2.2 区间

ACwing 362

因为 a i 、 b i a_i、b_i aibi可能取 0 0 0,为了方便使用前缀和,对每一个输入的 a i 、 b i a_i、b_i aibi+1,所以 a i 、 b i ∈ [ 1 , 50001 ] a_i、b_i \in [1, 50001] aibi[1,50001]

由前缀和, S 0 = 0 S_0 = 0 S0=0 S i S_i Si表示从1 ~ i中被选出的数的个数,题目要求 m i n { S 50001 } min\{S_{50001}\} min{S50001},应该使用最长路求解。

有不等式关系( i ∈ [ 1 , 50001 ] i \in [1,50001] i[1,50001]):

  • S i ≥ S i − 1 S_i \ge S_{i-1} SiSi1
  • S i − S i − 1 ≤ 1 S_i - S_{i-1} \le 1 SiSi11,表示第i个数选没有选,选的话为1,不选为0。不等式转化为 S i − 1 ≥ S i − 1 S_{i-1} \ge S_i - 1 Si1Si1
  • 对于每一个区间 [ a , b ] [a,b] [a,b]必须选择 c c c个数,即有 S b − S a − 1 ≥ c S_b - S_{a-1} \ge c SbSa1c

需要验证一下:从源点出发,是否一定可以走到所有的边。
根据条件(1),从i-1可以走到i,因此从0可以走到1,从1可以走到2,…,因此存在这样的源点。

#include 
#include 
#include 

using namespace std;

// M表示边数,三个不等式三种情况各5w条边
const int N = 50010, M = 150010;

int n;
int h[N], e[M], w[M], ne[M], idx;
int dist[N], q[N];
bool st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void spfa() {
    memset(dist, -0x3f, sizeof dist);
    dist[0] = 0; st[0] = true;
    int hh = 0, tt = 1; q[0] = 0;
    while (hh != tt) {
        int t = q[hh++];
        if (hh == N) hh = 0;
        st[t] = false;
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] < dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                if (!st[j]) {
                    q[tt++] = j;
                    if (tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
}

int main() {
    scanf("%d", &n);
    memset(h, -1, sizeof h);
    for (int i = 1; i < N; i++) { // 先建立前两种情况的边
        add(i - 1, i, 0);
        add(i, i - 1, -1);
    }
    for (int i = 0; i < n; i++) { // 建立第三种情况的边
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        a++, b++;
        add(a - 1, b, c);
    }
    spfa(); // 题目必有解,不用判断是否存在正环
    printf("%d\n", dist[50001]);
    return 0;
}

2.3 排队布局

ACwing 1170

由题可知,假设两头奶牛的坐标为 x a 、 x b x_a、x_b xaxb x a < x b x_axa<xb,那么由如下关系:

  • x i ≤ x i + 1 + 0 , i ∈ [ 1 , n ) x_i \le x_{i+1} + 0,i \in [1, n) xixi+1+0i[1,n),表示i + 1 → i,边上权值为0
  • x b ≤ x a + L x_b \le x_a + L xbxa+L,表示a → b,边上权值为L
  • x a ≤ x b − D x_a \le x_b - D xaxbD,表示b → a,边上权值为-D

对于第一问:如果不存在满足要求的方案,输出-1。
建立超级源点0,建立从0指向其余所有点的边0 → i,则有 x i ≤ x 0 + 0 x_i \le x_0 + 0 xix0+0,且边上权值为0,这样就能从点0指向所有结点。求最大值即求最短路径。

对于第二问:如果 1 号奶牛和 N 号奶牛间的距离可以任意大,输出-2。
1号点固定在位置0上,即 x 1 = 0 x_1 = 0 x1=0,判断 x n x_n xn是否可以无限大。等价于求1号点到其余所有点的最长路径,求完之后取每个点 x n x_n xn最大值。如果 x n = + ∞ x_n = +\infty xn=+,那么它可以无限大;否则它的最大值即为 x n x_n xn

#include 
#include 
#include 

using namespace std;

// 第一种边1000条,后两种边各1w条
const int N = 1010, M = 10000 + 10000 + 1000 + 10, INF = 0x3f3f3f3f;

int n, m1, m2;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int q[N], cnt[N];
bool st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// 加入前size个点
bool spfa(int size) {
    int hh = 0, tt = 0;
    memset(dist, 0x3f, sizeof dist);
    memset(st, 0, sizeof st);
    memset(cnt, 0, sizeof cnt);
    for (int i = 1; i <= size; i++) {
        q[tt++] = i;
        dist[i] = 0;
        st[i] = true;
    }
    while (hh != tt) {
        int t = q[hh++];
        if (hh == N) hh = 0;
        st[t] = false;
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;
                if (!st[j]) {
                    q[tt++] = j;
                    if (tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main() {
    scanf("%d%d%d", &n, &m1, &m2);
    memset(h, -1, sizeof h);
    for (int i = 1; i < n; i++) // 建立第一种边
        add(i + 1, i, 0);
    while (m1--) { // 第二种边
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        if (a > b) swap(a, b); // 注意大小
        add(a, b, c);
    }
    while (m2--) { // 第三种边
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        if (a > b) swap(a, b);
        add(b, a, -c);
    }
    if (spfa(n)) // 存在负环,加入所有点
        puts("-1");
    else {
        spfa(1); // 仅加入第一个点
        if (dist[n] == INF) puts("-2");
        else printf("%d\n", dist[n]);
    }
    return 0;
}

2.4 雇佣收银员

ACwing 393

由题目分析,使用 n u m [ i ]   ( i ∈ [ 0 , 23 ] ) num[i]\space (i \in [0, 23]) num[i] (i[0,23]),表示第 i i i点来的人数, x i   ( x i ∈ [ 0 , 23 ] ) x_i \space (x_i \in[0, 23]) xi (xi[0,23])表示从第 i i i点来的人 n u m [ i ] num[i] num[i]中挑选的人数,则有如下关系:

  • 0 ≤ x i ≤ n u m s [ i ] 0 \le x_i \le nums[i] 0xinums[i]
  • i i i时刻服务的人是否足够,由题可知在 x i − 7 、 x i − 6 、 . . . 、 x i x_{i-7}、x_{i-6}、...、x_i xi7xi6...xi来的人都可以在第 i i i时刻服务,所以有 x i − 7 + x i − 6 + . . . + x i ≥ r i x_{i-7} + x_{i-6} + ... + x_i \ge r_i xi7+xi6+...+xiri

使用前缀和,将 i = i + 1 i = i + 1 i=i+1,则上面有 x i 、 i ∈ [ 1 , 24 ] x_i、i \in [1, 24] xii[1,24],且前缀和 S 0 = 0 , S i = x 1 + x 2 + . . . + x i S_0 = 0,S_i = x_1 + x_2 + ... + x_i S0=0Si=x1+x2+...+xi,上述不等式有:

  • 0 ≤ S i − S i − 1 ≤ n u m [ i ] , i ∈ [ 1 , 24 ] 0 \le S_i - S_{i-1} \le num[i],i \in [1, 24] 0SiSi1num[i]i[1,24]
  • i ≥ 8 i \ge 8 i8时, S i − S i − 8 ≥ r i S_i - S_{i-8} \ge r_i SiSi8ri;若 0 < i < 7 0 < i < 7 0<i<7时, S i + S 24 − S i + 16 ≥ r i S_i + S_{24} - S_{i + 16} \ge r_i Si+S24Si+16ri

将不等式整理有 S i ≥ S i − 1 + 0 S i − 1 ≥ S i − n u m [ i ] S i ≥ S i − 8 + r i , i ≥ 8 S i ≥ S i + 16 − S 24 + r i , 0 < i < 7 \begin{aligned} S_i &\ge S_{i-1} + 0\\ S_{i-1} &\ge S_i - num[i]\\ S_i &\ge S_{i-8} + r_i,i \ge 8\\ S_i &\ge S_{i + 16} - S_{24} + r_i, 0 < i < 7\\ \end{aligned} SiSi1SiSiSi1+0Sinum[i]Si8+rii8Si+16S24+ri0<i<7除了第四个式子,其余式子均为我们熟悉的形式。对于第四个式子的处理,因为 0 ≤ N ≤ 1000 0 \le N \le 1000 0N1000,我们可以枚举所有的 S 24 S_{24} S24的所有取值,那么 S 24 S_{24} S24即变为一个常量,那么第四个式子也变为了我们熟悉的形式。

那么问题就转换成在 N ∈ [ 0 , 1000 ] N \in [0,1000] N[0,1000]中,从小到大枚举每一个数,求出第一个使得我们问题有解的 S 24 S_{24} S24的值即为所求,若枚举完都无解,那么该问题无解。

有第一个不等式,存在一条i-1 → i且权值为0的边,又因为 i ∈ [ 1 , 24 ] i \in [1, 24] i[1,24],那么可知0号点可以连接到所有的点,所以将0号点作为超级原点。

#include 
#include 
#include 

using namespace std;

// n<=25 m<=25*3
const int N = 30, M = 100;

int n;
int h[N], e[M], w[M], ne[M], idx;
int r[N], num[N];
int dist[N], q[N], cnt[N];
bool st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// 建图

void build(int c) {
    // 图的初始化
    memset(h, -1, sizeof h);
    idx = 0;

    // 对s24是定值的体现:s24 <= c,s24 >= c
    // 要与s0联系,即可转换为:s24 <= s0 + c,s0 <= s24 - c
    add(0, 24, c), add(24, 0, -c);

    for (int i = 1; i <= 7; i++) add(i + 16, i, r[i] - c);
    for (int i = 8; i <= 24; i++) add(i - 8, i, r[i]);
    for (int i = 1; i <= 24; i++) {
        add(i, i - 1, -num[i]);
        add(i - 1, i, 0);
    }
}

bool spfa(int c) { // 传入S24
    build(c);

    memset(dist, -0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);

    int hh = 0, tt = 1;
    dist[0] = 0;
    q[0] = 0;
    st[0] = true;

    while (hh != tt) {
        int t = q[hh++];
        if (hh == N) hh = 0;
        st[t] = false;

        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] < dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= 25) return false;
                if (!st[j]) {
                    q[tt++] = j;
                    if (tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }

    return true;
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        // 读入24个r
        for (int i = 1; i <= 24; i++)
            cin >> r[i];

        cin >> n;
        memset(num, 0, sizeof num);

        // 统计每个时间段的人数,0作为超级源点
        for (int i = 0; i < n; i++) {
            int t;
            cin >> t;
            num[t + 1]++;
        }

        bool success = false;
        for (int i = 0; i <= 1000; i++) // 依次枚举每一个S24的值
            if (spfa(i)) { // 只要有一个成功即可
                cout << i << endl;
                success = true;
                break;
            }

        if (!success) puts("No Solution");
    }

    return 0;
}

2.5 再卖菜

ACwing 3265

假设存在两个序列 a 1 , a 2 , ⋯   , a n a_1,a_2, \cdots, a_n a1,a2,,an b 1 , b 2 , ⋯   , b n b_1, b_2, \cdots, b_n b1,b2,,bn

先考虑 b b b 序列中的一般情况,即 b i = a i − 1 + a i + a i + 1 3   ( i ∈ [ 2 , n − 1 ] ) b_i = \frac{a_{i-1} + a_i + a_{i + 1}}{3}\ (i\in[2,n-1]) bi=3ai1+ai+ai+1 (i[2,n1]) 的时候 ,这里之前可以预处理处序列 a a a 的前缀和 S S S 以方便求值,那么 b i = ⌊ S i + 1 − S i − 2 3 ⌋ b_i = \left \lfloor \frac{S_{i+1} - S_{i-2}}{3} \right \rfloor bi=3Si+1Si2 ,那么就存在关系: 3 b i ≤ S i + 1 − S i − 2 ≤ 3 b i + 2 3b_i \le S_{i+1} - S_{i-2} \le 3b_i + 2 3biSi+1Si23bi+2又根据题目可知 a i ≥ 1 a_i \ge 1 ai1,因此还存在条件 S i − S i − 1 ≥ 1 S_i - S_{i-1} \ge 1 SiSi11

综上:题目转换为在满足条件 { 3 b i ≤ S i + 1 − S i − 2 ≤ 3 b i + 2 S i − S i − 1 ≥ 1 \begin{cases} &3b_i \le S_{i+1} - S_{i-2} \le 3b_i + 2\\ &S_i - S{i-1} \ge 1\\ \end{cases} {3biSi+1Si23bi+2SiSi11的前提下求出 S i ( i ∈ [ 1 , n ] ) S_i(i\in[1, n]) Si(i[1,n])的值。

因为求解最小值,所以应该求解最长路,其差分格式对应于 d b ≥ d a + c d_b \ge d_a + c dbda+c,表示一条从 a a a 指向 b b b 且权值为 c c c 的边。

将所有的不等式转化成差分的格式:
{ S i + 1 ≥ S i − 2 + 3 b i S i − 2 ≥ S i + 1 − ( 3 b i + 2 ) S i ≥ S i − 1 + 1 \begin{cases} &S_{i+1} \ge S_{i-2} + 3b_i\\ &S_{i-2} \ge S_{i+1} - (3b_i + 2)\\ &S_i \ge S_{i-1} + 1\\ \end{cases} Si+1Si2+3biSi2Si+1(3bi+2)SiSi1+1注意这个题目存在一个绝对值 S 0 = 0 S_0 = 0 S0=0

因为差分约束可以求出每个元素的最小值,那么也就相当于求解出了字典序的最小值。

#include 
#include 
#include 
using namespace std;

const int N = 310, M = N * 3; // 有三个不等式,要建三条边

int n;
int h[N], e[M], w[M], ne[M], idx;
int dist[N], q[N];
int b[N];
bool st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void spfa() {
    int hh = 0, tt = 1;
    memset(dist, -0x3f, sizeof dist);
    dist[0] = 0;
    q[0] = 0;
    while (hh != tt) {
        int t = q[hh++];
        if (hh == N) hh = 0;
        st[t] = false;
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] < dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                if (!st[j]) {
                    q[tt++] = j;
                    if (tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
}

int main() {
    cin >> n;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i++) cin >> b[i];
    for (int i = 2; i < n; i++) {
        add(i - 2, i + 1, b[i] * 3);
        add(i + 1, i - 2, -(b[i] * 3 + 2));
    }
    add(0, 2, b[1] * 2), add(2, 0, -(b[1] * 2 + 1)); // 边上两个点
    add(n - 2, n, b[n] * 2), add(n, n - 2, -(b[n] * 2 + 1));
    for (int i = 1; i <= n; i++) add(i - 1, i, 1);
    spfa();
    for (int i = 1; i <= n; i++) cout << dist[i] - dist[i - 1] << ' '; // dist[i] 对应于 S[i]
    return 0;
}

三、最近公共祖先 (LCA)

注:一个点本身也可称为其自己的祖先。

LCA问题解法

向上标记法,时间复杂度 O ( n ) O(n) O(n)


倍增法(在线做法:多次询问,询问一次,得一次结果)

预处理出数组 f a [ i , j ] fa[i, j] fa[i,j],表示从i开始,向上走 2 j 2^j 2j步所能走到的结点,其中 j ∈ [ 0 , ⌊ l o g 2 n ⌋ ] j \in [0, \lfloor log_2^n \rfloor] j[0,log2n⌋]
预处理处数组 d e p t h [ i ] depth[i] depth[i],表示从上到下的层数,其值等于从当前结点到根结点路径上的边数+1

数组 f a [ i , j ] fa[i,j] fa[i,j]的预处理方法(基于DP):

  • j = 0 j=0 j=0的时候, f [ i , j ] = i f[i,j]=i f[i,j]=i的父结点;
  • j > 0 j > 0 j>0的时候,可以分两步走。第一次往上走 2 j − 1 2^{j-1} 2j1步,第二次再往上走 2 j − 1 2^{j-1} 2j1步,两次之和为往上走 2 j 2^j 2j步,即有 f [ i , j ] = f [ f [ i , j − 1 ] , j − 1 ] f[i,j] = f[f[i, j-1], j-1] f[i,j]=f[f[i,j1],j1]

求两个结点 x 、 y x、y xy的最近公共祖先:

  • 第一步先将两个点跳到同一层,即将深度较深的那个点往上跳。这里的处理,首先求出两个点深度的差值记为 t t t。之前预处理的数组 f a fa fa,在这个数组中我们相当于已经知道了 2 0 , 2 1 , . . . , 2 k 2^0,2^1,...,2^k 2021...2k,我们要使用这些数来拼凑出 t t t二进制拼凑)。在实际的过程中,我们不需要将这个值计算出来,只要判断 d e p t h ( x ) ≥ d e p t h ( y ) depth(x) \ge depth(y) depth(x)depth(y),就说明点 x x x依然在 y y y的下面,则 x x x还需要继续往上跳,直到两个点跳到同一层即可。

具体做法举个例子,比如 t = 11 t =11 t=11,现在有 2 0 , 2 1 , 2 2 , 2 3 , 2 4 2^0,2^1,2^2,2^3,2^4 2021222324。现在我们将 t t t的值从高位开始拼凑出 t t t,如果出现第一个出现小于等于 t t t的位置,将其记为 1 1 1

  • 首先 t = 11 > 2 3 = 8 t=11 > 2^3=8 t=11>23=8,那么就有 1 1 1_ _ _,现在 t = t − 2 3 = 3 t = t-2^3 = 3 t=t23=3
  • 然后继续往低位走,有 t = 3 < 2 2 = 4 t= 3 < 2^2=4 t=3<22=4,那么就有 10 10 10_ _;
  • 继续往低位走,有 t = 3 > 2 1 = 2 t = 3 > 2^1 = 2 t=3>21=2,就有 101 101 101_,现在 t = t − 2 1 = 1 t = t - 2^1 = 1 t=t21=1
  • 继续往低位走,有 t = 1 ≥ 2 0 t = 1 \ge 2^0 t=120,那么就有 1011 1011 1011,此时 t = 0 t=0 t=0,拼凑结束。
  • 第二步让两个点同时往上跳,直到一直跳到它们的最近公共祖先的下一层。这里跳的步数同样使用二进制拼凑的思路,直到跳到了最近公共祖先的下一层,则最后最近公共祖先即为 f ( x , 0 ) f(x,0) f(x,0) (或者 f ( y , 0 ) f(y, 0) f(y,0))。

注意,这里只是跳到了最近公共祖先的下一层,而没有跳到公共祖先。如果两个点都跳到公共祖先的话,则有 f ( a , k ) = f ( b , k ) f(a,k)=f(b,k) f(a,k)=f(b,k),虽然两个点指向了同一个点,但是并不能判断这个点是最近的公共祖先。如果仅仅是跳到最近公共祖先的下一层的结点,这时候 f ( a , k ) ≠ f ( b , k ) f(a,k) \ne f(b,k) f(a,k)=f(b,k),则说明两个结点还没有找到公共祖先,如果再同时往上跳一层就找到了公共祖先,那么这个时候就可以判断这个点是它们的最近公共祖先。

细节:我们设置一个哨兵 d e p t h [ 0 ] = 0 depth[0] = 0 depth[0]=0,如果从 i i i 2 j 2^j 2j步会跳过根结点,那么我们设置 f a ( i , j ) = 0 fa(i,j) = 0 fa(i,j)=0

时间复杂度:预处理 O ( n l o g n ) O(nlogn) O(nlogn),查询 O ( l o g n ) O(logn) O(logn)


Tarjan算法(离线做法:多次询问需全部输入,统一处理):本质是对向上标记法的优化

在做DFS过程中(从左往右),将所有结点分成三大类(如下图所示):

  • 已经遍历过的点:所有已经被遍历过,且回溯了的点,标记为2,图中绿色边上的点;
  • 正在搜索的点:已经被遍历过,尚未回溯的点,标记为1,图中红色边上的点;
  • 还未遍历到的点:未被遍历,且未回溯的点,标记为0,图中橙色边上的点。

第四章 图论(4):SPFA求负环、差分约束、LCA_第4张图片

我们可以发现,对于已经遍历过(绿色部分的点)的任意一个点和当前正在遍历(红色部分的点)的点的LCA即为每一个绿色点祖宗结点。因此我们可以将这些点使用一个并查集来维护,如图中紫色部分。每个结点应该在这一片区域(紫色一团)回溯完毕之后进行合并。

在遍历当前结点的时候,可以扫描所有与当前结点相关的询问,如果所询问的另外一个点已经被遍历且回溯了,那么我们可以直接得到两个点的公共祖先即为另外一个点在并查集中的代表元素。

时间复杂度: O ( n + m ) O(n + m) O(n+m)


3.1 祖孙询问 (倍增法)

ACwing 1172

#include 
#include 
#include 

using namespace std;

const int N = 40010, M = N * 2; // 均为无向边

int n, m;
int h[N], e[M], ne[M], idx;
int depth[N], fa[N][16]; // 最长路径4w条边,取log后值大于15小于16,故取16
int q[N];

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void bfs(int root) {
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[root] = 1; // 0号点是哨兵结点
    int hh = 0, tt = 0;
    q[0] = root;
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (depth[j] > depth[t] + 1) {
                depth[j] = depth[t] + 1;
                q[++tt] = j;
                fa[j][0] = t;
                for (int k = 1; k <= 15; k++)
                    fa[j][k] = fa[fa[j][k - 1]][k - 1];
            }
        }
    }
}

// 求a和b的最近公共祖先
int lca(int a, int b) {
    if (depth[a] < depth[b]) swap(a, b); // 确保a在b下面
    for (int k = 15; k >= 0; k--)
        if (depth[fa[a][k]] >= depth[b]) // 因为设置了哨兵,如果跳出了根结点会导致 0 >= depth[b] 不成立
            a = fa[a][k];

    if (a == b) return a; // a或者b即为LCA

    // a、b同时跳
    for (int k = 15; k >= 0; k--)
        if (fa[a][k] != fa[b][k]) { // 因为设置了哨兵,如果跳出了根结点,该if条件不会成立
            a = fa[a][k];
            b = fa[b][k];
        }
    return fa[a][0];
}

int main() {
    scanf("%d", &n);
    int root = 0;
    memset(h, -1, sizeof h);

    for (int i = 0; i < n; i++) {
        int a, b;
        scanf("%d%d", &a, &b);
        if (b == -1) root = a;
        else add(a, b), add(b, a);
    }

    bfs(root); // 预处理数组depth和fa

    scanf("%d", &m);
    while (m--) {
        int a, b;
        scanf("%d%d", &a, &b);
        int p = lca(a, b);
        if (p == a) puts("1");
        else if (p == b) puts("2");
        else puts("0");
    }

    return 0;
}

3.2 距离 (Tarjan算法)

ACwing 1171

假设每个结点 i i i到根结点的距离为 d ( i ) d(i) d(i),现在存在两个结点 x 、 y x、y xy,及它们的最近公共祖先结点 p p p,那么两个结点之间的最短距离(因为是在树里面,所以两个结点之前的路径唯一,最短距离也唯一)等于: d ( x ) + d ( y ) − 2 × d ( p ) d(x) + d(y) - 2 \times d(p) d(x)+d(y)2×d(p)

#include 
#include 
#include 
#include 

using namespace std;

typedef pair<int, int> PII;

const int N = 10010, M = N * 2;

int n, m;
int h[N], e[M], w[M], ne[M], idx;
int dist[N]; // 存储每个点和根结点的距离
int p[N];
int res[M]; // 存储每个询问的结果
int st[N];
vector<PII> query[N];   // first存查询的另外一个点,second存查询编号

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// 求出每个点和根结点的距离
void dfs(int u, int fa) { // 因为是无向图,需要存储上一次操作
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (j == fa) continue;
        dist[j] = dist[u] + w[i];
        dfs(j, u);
    }
}

int find(int x) {
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

void tarjan(int u) {
    st[u] = 1; // 当前搜的结点标记为1
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (!st[j]) {
            tarjan(j);
            p[j] = u; // 合并并查集
        }
    }

    // 遍历所有和u相关的查询
    for (auto item: query[u]) {
        int y = item.first, id = item.second; // 节点编号y,查询编号id
        if (st[y] == 2) { // 如果已经遍历过且回溯完成
            int anc = find(y);
            res[id] = dist[u] + dist[y] - dist[anc] * 2;
        }
    }

    st[u] = 2;
}

int main() {
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);

    // 读入n-1条边
    for (int i = 0; i < n - 1; i++) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }

    // 读入每个询问
    for (int i = 0; i < m; i++) {
        int a, b;
        scanf("%d%d", &a, &b);
        if (a != b) { // a=b不用处理,距离是0,res默认值为0
            query[a].push_back({b, i});
            query[b].push_back({a, i});
        }
    }

    for (int i = 1; i <= n; i++) p[i] = i;

    // 求出每个点和根结点的距离
    dfs(1, -1);

    tarjan(1);

    for (int i = 0; i < m; i++) printf("%d\n", res[i]);

    return 0;
}

3.3 次小生成树

ACwing 356

预处理三个数组:

  • f a ( i , j ) fa(i,j) fa(i,j):同上
  • d 1 ( i , j ) d_1(i,j) d1(i,j):记录从 i i i开始,往上跳 2 j 2^j 2j步,这个路径上的最大边权
  • d 2 ( i , j ) d_2(i,j) d2(i,j):记录从 i i i开始,往上跳 2 j 2^j 2j步,这个路径上的次大边权

对于 d 1 ( i , j ) 、 d 2 ( i , j ) d_1(i,j)、d_2(i,j) d1(i,j)d2(i,j)的预处理:
将向上跳的过程分成两段,第一段i → anc,跳了 2 j − 1 2^{j-1} 2j1步,第二段从anc往上跳 2 j − 1 2^{j-1} 2j1步。

  • 第一段的最大值为 d 1 ( i , j − 1 ) d_1(i, j-1) d1(i,j1),次大值为 d 2 ( i , j − 1 ) d_2(i,j-1) d2(i,j1)
  • 第二段的最大值为 d 1 ( a n c , j − 1 ) d_1(anc, j-1) d1(anc,j1),次大值为 d 2 ( a n c , j − 1 ) d_2(anc, j-1) d2(anc,j1)
    整体过程的最大值和次大值在上面四个数中取一个 m a x max max m i n min min即可。
#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 100010, M = 300010, INF = 0x3f3f3f3f;

int n, m;

struct Edge {
    int a, b, w;
    bool used; // 表示该边是否在最小生成树中

    bool operator<(const Edge &t) const {
        return w < t.w;
    }
} edge[M];

int p[N];
int h[N], e[M], w[M], ne[M], idx;
int depth[N], fa[N][17], d1[N][17], d2[N][17]; // 16 < log_2^10w < 17,d1和d2为最大边和次大边
int q[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int find(int x) {
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

LL kruskal() {
    for (int i = 1; i <= n; i++) p[i] = i;
    sort(edge, edge + m);
    LL res = 0;
    for (int i = 0; i < m; i++) {
        int a = find(edge[i].a), b = find(edge[i].b), w = edge[i].w;
        if (a != b) {
            p[a] = b;
            res += w;
            edge[i].used = true;
        }
    }

    return res;
}

// 建图
void build() {
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i++)
        if (edge[i].used) {
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            add(a, b, w), add(b, a, w);
        }
}

// 预处理数组fa、d1、d2
void bfs() {
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[1] = 1;
    q[0] = 1;
    int hh = 0, tt = 0;
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (depth[j] > depth[t] + 1) {
                depth[j] = depth[t] + 1;
                q[++tt] = j;
                fa[j][0] = t;
                d1[j][0] = w[i], d2[j][0] = -INF;
                for (int k = 1; k <= 16; k++) {
                    int anc = fa[j][k - 1];
                    fa[j][k] = fa[anc][k - 1];
                    int distance[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]};
                    d1[j][k] = d2[j][k] = -INF;
                    for (int u = 0; u < 4; u++) {
                        int d = distance[u];
                        if (d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
                        else if (d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
                    }
                }
            }
        }
    }
}

int lca(int a, int b, int w) {
    static int distance[N * 2]; // 最大次大值
    int cnt = 0;
    if (depth[a] < depth[b]) swap(a, b);
    for (int k = 16; k >= 0; k--)
        if (depth[fa[a][k]] >= depth[b]) {
            distance[cnt++] = d1[a][k];
            distance[cnt++] = d2[a][k];
            a = fa[a][k];
        }
    if (a != b) {
        for (int k = 16; k >= 0; k--)
            if (fa[a][k] != fa[b][k]) {
                distance[cnt++] = d1[a][k];
                distance[cnt++] = d2[a][k];
                distance[cnt++] = d1[b][k];
                distance[cnt++] = d2[b][k];
                a = fa[a][k], b = fa[b][k];
            }
        distance[cnt++] = d1[a][0];
        distance[cnt++] = d1[b][0];
    }

    int dist1 = -INF, dist2 = -INF;
    for (int i = 0; i < cnt; i++) {
        int d = distance[i];
        if (d > dist1) dist2 = dist1, dist1 = d;
        else if (d != dist1 && d > dist2) dist2 = d;
    }

    if (w > dist1) return w - dist1;
    if (w > dist2) return w - dist2;
    return INF;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i++) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edge[i] = {a, b, c};
    }

    LL sum = kruskal();
    build();
    bfs();

    LL res = 1e18; // long long的最大值
    for (int i = 0; i < m; i++) // 枚举所有的非树边,取一个最大最小值
        if (!edge[i].used) {
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            res = min(res, sum + lca(a, b, w));
        }
    printf("%lld\n", res);

    return 0;
}

3.4 暗之连锁

ACwing 352

如图所示,对于每一条非树边所构成的环,如果砍掉环上的树边,要使整体不连通,就必须还需看条环上的非树边。在每一条树边上用 c c c记录砍掉当前树边后,要使整体不连通,还需需要砍掉多少条非树边。因此有:

  • c = 0 c=0 c=0:表示砍掉任意一条树边即可,树边有 m m m条,方案数 + m +m +m
  • c = 1 c=1 c=1:表示砍掉当前树边后还需砍掉当前环上的非树边,方案数 + 1 +1 +1
  • c > 1 c>1 c>1:表示砍掉当前树边后还需砍掉多个环上的非树边,因为只能砍掉一刀,砍掉一刀无法是整体不连通,因此方案数 + 0 +0 +0
    第四章 图论(4):SPFA求负环、差分约束、LCA_第5张图片

问题的核心就转换为了如何快速给每条边上 + 1 +1 +1并确定每个边上的值?可以想到差分,不过这里是树上的差分。假设树上两个点 x 、 y x、y xy以及最近公共祖先 p p p以某个结点为根节点的子树上,所有结点值的和,记为其与其父结点相连的边的权值,如下图所示。
第四章 图论(4):SPFA求负环、差分约束、LCA_第6张图片
那么在一棵树上,对两个结点 x 、 y x、y xy + c +c +c,对其最近公共祖先 p p p上值 − 2 × c -2 \times c 2×c,对这棵树之外的部分是没有影响的,效果如下如:第四章 图论(4):SPFA求负环、差分约束、LCA_第7张图片
因此原问题做法为,对于每一条非树边x → y,先求它们的最近公共祖先p,然后让x、y结点上的值都 + c +c +c,让结点p的值 − 2 × c -2 \times c 2×c。之后遍历整棵树,求每棵子树的总权值为多少,对于所有权值和为0+m,权值和为1的加上1,所有权值和>1的加上0。注意根结点没有父结点,即没有与父节点相连的边,所以根结点不能计算。

#include 
#include 
#include 

using namespace std;

const int N = 100010, M = N * 2;

int n, m;
int h[N], e[M], ne[M], idx;
int depth[N], fa[N][17]; // 16 < log_2^(10w) < 17
int d[N]; // 存储每个点上差分的值
int q[N];
int ans;

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 预处理数组fa,depth
void bfs()  {
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[1] = 1;
    int hh = 0, tt = 0;
    q[0] = 1;

    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (depth[j] > depth[t] + 1) {
                depth[j] = depth[t] + 1;
                q[++tt] = j;
                fa[j][0] = t;
                for (int k = 1; k <= 16; k++)
                    fa[j][k] = fa[fa[j][k - 1]][k - 1];
            }
        }
    }
}

int lca(int a, int b) {
    if (depth[a] < depth[b]) swap(a, b);
    for (int k = 16; k >= 0; k--)
        if (depth[fa[a][k]] >= depth[b])
            a = fa[a][k];
    if (a == b) return a;
    for (int k = 16; k >= 0; k--)
        if (fa[a][k] != fa[b][k]) {
            a = fa[a][k];
            b = fa[b][k];
        }
    return fa[a][0];
}

// 返回每棵子树的和
int dfs(int u, int father) {
    int res = d[u];
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (j != father) {
            int s = dfs(j, u);
            if (s == 0) ans += m;
            else if (s == 1) ans++;
            res += s;
        }
    }

    return res;
}

int main() {
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i++) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a);
    }

    bfs();

    // 读入非树边
    for (int i = 0; i < m; i++) {
        int a, b;
        scanf("%d%d", &a, &b);
        int p = lca(a, b); // 读入祖先
        d[a]++, d[b]++, d[p] -= 2;
    }
    dfs(1, -1);
    printf("%d\n", ans);

    return 0;
}

你可能感兴趣的:(算法笔记,图论,算法,LCA)