负环与差分约束

目录
  • 负环与差分约束
    • 1. 基本概念、方法
      • 1.1 负环
        • 1.1.1 spfa 判负环/正环
        • 1.1.2 tarjan+缩点 判断正环/负环
        • 1.1.3 拓扑排序 判断正环/负环
      • 1.2 差分约束
    • 2. 例题
      • 2.1 负环/正环判定
        • 2.1.1 spfa判断负环/正环
        • 2.1.2 tarjan求scc+缩点判断正环/负环
        • 2.1.3 拓扑排序判断正环/负环
      • 2.2 差分约束
        • 2.2.1 spfa差分约束
        • 2.2.2 tarjan求scc + 缩点 + dp 差分约束
        • 2.2.3 拓扑排序 差分约束

负环与差分约束

1. 基本概念、方法

1.1 负环

1.1.1 spfa 判负环/正环

适用条件: 边权有正有负有零
判负环: 如果存在负环,那么spfa将一直跑不出结果,因此只需要考虑如果两个点之间有n个点,那么由抽屉原理,必然存在负环.常用的方法为spfa判断负环,该方法在一般的图中,复杂度为O(km),但理论时间复杂度为O(nm)

判正环: 判断正环的思路相反:在i和j之间跑最长路,一旦i和j之间的点的数目大于等于n,认为出现正环

技巧:
1.技巧1:有时候判断负环容易超时,因为一个要让两个点间的点数大于等于n的时候比较费时,所以可以去记录一下当前进入队列的点的总数count,一旦这个总数比较大的时候.比如这个点数count>=2n时,我们认为很大概率存在负环;
2.技巧2:把队列换成栈,一旦存在负环,那么使用栈来处理能够更快得到一个负环

spfa算法明确:

  1. 如果spfa只要求最短路,那么一开始要把所有点距离都初始化为0x3f,把源点放入队列,做标记,源点距离dist[s] = 0
  2. 如果spfa只要判负环,那么需要把所有点距离都初始化为0x3f, 同时所有点都放入队列。但这样求出的dis数组数值不对,只能表示相对关系
  3. 如果spfa既要求最短路,又要判负环,那么需要把所有点初始化为0x3f,同时所有点都放入队列,然后把源点做标记,源点距离dist[s] = 0

1.1.2 tarjan+缩点 判断正环/负环

适用条件: 边权全部>=0(或全部<=0)
判负环/正环: tarjan跑scc,然后缩点,判断每个超级点内是否存在大于0(小于0)的边,如果存在说明存在正环(负环)。

1.1.3 拓扑排序 判断正环/负环

适用条件: 边权全部>0(或全部<0)
判断正环/负环: 跑拓扑排序算法,如果最后拓扑序列内数目==n,那么有解,无正环/负环,否则存在正环/负环。

1.2 差分约束

    差分约束问题就是求解一组不等式。当题目给定的条件可以转化为不等式组的时候就是求解差分约束。当求最小值,跑最长路;求最大值,跑最短路。同时一旦发现正(负)环那么无解。
    差分约束的步骤:

  1. 根据题目条件,建图。求最小值,跑最长路,就转化为:xi>=xj+c,然后add(j, i, c);求最大值,跑最短路,就转化为xi<=xj+c,然后add(j, i, c)。
  2. 按照题目要求、边权情况,选择不同算法跑最短(长)路,由此导致了判断负(正)环的方法不同(见1.1):
    ① 如果边权有正有零有负,那么选择spfa来求最短(长)路,时间复杂度为O(km),且同时使用spfa判断负(正)环
    ② 如果边权都大于等于0(都小于等于0),那么选择tarjan求scc + 缩点 + dp求最长路(最短路),且同时使用tarjan判断正环(负环)
    ③ 如果边权都大于0,拓扑排序+dp求最长(短)路,同时直接拓扑排序判断正环(负环)。

2. 例题

2.1 负环/正环判定

2.1.1 spfa判断负环/正环

acwing904虫洞
判断图中是否存在负环

#include 

using namespace std;

int const N = 5e2 + 10, M = 3e6 + 10;
int e[M], ne[M], w[M], idx, h[N], t, n, m, wi, dist[N], cnt[N], st[N];

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

// spfa求负环(正环)
bool spfa() {
    queue q;
    
    memset(dist, 0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);
    // 思路1:在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于把全部点放入队列。这样cnt维护的就是到虚拟源点的距离。
    // 思路2:spfa算法一开始加入多少个点到队列都没有关系,因为这些点一开始距离都是无穷,不会引起答案差异。但是由于判断负环,必须把所有点都加入,防止出现孤立连通块的情况。这样cnt维护的就是和某些虚拟点的距离。
    for (int i = 1; i <= n; i ++ ) {
        st[i] = true;
        q.push(i);
    }
    // dist[0] = 0, st[0] = 1, q.push(0);  如果希望能够正确求出dis数组,那么还需要加上这个代码
    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]) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, -0x3f, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;  // 更新边数
                if (cnt[j] >= n) return true;  // 如果j点到源点的边数大于等于n
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

int main() {
    cin >> t;
    while (t--) {
        cin >> n >> m >> wi;
        idx = 0;
        memset(h, -1, sizeof h);
        for (int i = 1, a, b, c; i <= m; ++i) {
            scanf("%d %d %d", &a, &b, &c);
            add(a, b, c), add(b, a, c);
        }
        for (int i = 1, a, b, c; i <= wi; ++i) {
            scanf("%d %d %d", &a, &b, &c);
            add(a, b, -c);
        }
        
        if (spfa()) printf("YES\n");
        else printf("NO\n");
    }
    return 0;
}

acwing361观光奶牛
给定一张L个点、P条边的有向图,每个点都有一个权值f[i],每条边都有一个权值t[i]。求图中的一个环,使“环上各点的权值之和”除以“环上各边的权值之和”最大。输出这个最大值。
点数N~1e3, 边数M~5e3

/*
本题是最大比率环+01分数规划
方法为二分 + 构图跑负环
即枚举二分枚举答案,然后根据这个mid来重新构图:把每个点和这个点对应的一条出边对应起来作为环上的一条边,
具体操作就是当对t点的所有出边进行更新的时候,原来的边权w[i],变为mid*w[i] - f[t]。这样把点权放到每个出边的边权上。
然后跑spfa判断是否存在负环即可
*/
#include 

using namespace std;

int const N = 1e3 + 10, M = 5e5 + 10;
double const eps = 1e-8;
int e[M], ne[M], w[M], idx, h[N], n, m, cnt[N], st[N], f[N];
double dist[N];

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

// spfa求负环(正环)
bool spfa(double mid) {
    queue q;
    
    memset(dist, 0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);
    // 思路1:在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于把全部点放入队列。这样cnt维护的就是到虚拟源点的距离。
    // 思路2:spfa算法一开始加入多少个点到队列都没有关系,因为这些点一开始距离都是无穷,不会引起答案差异。但是由于判断负环,必须把所有点都加入,防止出现孤立连通块的情况。这样cnt维护的就是和某些虚拟点的距离。
    for (int i = 1; i <= n; i ++ ) {
        st[i] = true;
        q.push(i);
    }
    // dist[0] = 0, st[0] = 1, q.push(0);  如果希望能够正确求出dis
    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] * mid - f[t]) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, -0x3f, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
                dist[j] = dist[t] + w[i] * mid - f[t];  // 边权发生改变
                cnt[j] = cnt[t] + 1;  // 更新边数
                if (cnt[j] >= n) return true;  // 如果j点到源点的边数大于等于n
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

int main() {
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; ++i) scanf("%d", &f[i]);
    for (int i = 1, a, b, c; i <= m; ++i) {
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c);
    }
    
    double l = 0, r = 1e9;
    while (r - l > eps) {
        double mid = (l + r) / 2;
        if (spfa(mid)) l = mid;
        else r = mid;
    }
    printf("%.2lf", l);
    return 0;
}

acwing1165单词环
我们有 n 个字符串,每个字符串都是由 a∼z 的小写英文字母组成的。如果字符串 A 的结尾两个字符刚好与字符串 B 的开头两个字符相匹配,那么我们称 A 与 B 能够相连(注意:A 能与 B 相连不代表 B 能与 A 相连)。我们希望从给定的字符串中找出一些,使得它们首尾相连形成一个环串(一个串首尾相连也算),我们想要使这个环串的平均长度最大。
n~1e5

/* 如果把每个单词当成一个点那么建图的时候会超时,因此把两个字母当成一个点,那么只有676个点
单词的长度当成边权,然后01分数规划处理
处理的时候边权变为w[i]-mid*f[i],然后判断是否存在正环,存在则mid太小 */
#include 

using namespace std;

int const N = 27 * 27, M = 1e5 + 10;
double const eps = 1e-4;
int e[M], ne[M], idx, h[N], n, cnt[N], st[N], tt, rr, q[677];
double dist[N], w[M];
char s[1001];

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

// spfa求负环(正环)
bool spfa(double mid) {
    stack q;
    
    memset(dist, -0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);
    // 思路1:在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于把全部点放入队列。这样cnt维护的就是到虚拟源点的距离。
    // 思路2:spfa算法一开始加入多少个点到队列都没有关系,因为这些点一开始距离都是无穷,不会引起答案差异。但是由于判断负环,必须把所有点都加入,防止出现孤立连通块的情况。这样cnt维护的就是和某些虚拟点的距离。
    for (int i = 1; i <= 676; i ++ ) {
        st[i] = true;
        q.push(i);
    }
    // dist[0] = 0, st[0] = 1, q.push(0);  如果希望能够正确求出dis数组,那么还需要加上这个代码
    while (q.size())  {
        int t = q.top();  // 取队首
        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] - mid) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, -0x3f, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
                dist[j] = dist[t] + w[i] - mid;
                cnt[j] = cnt[t] + 1;  // 更新边数
                if (cnt[j] >= 676) return true;  // 如果j点到源点的边数大于等于n
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

int main() {
    while (cin >> n && n != 0) {
        idx = 0;
        memset(h, -1, sizeof h);
        for (int i = 1; i <= n; ++i) {
            scanf("%s", s);
            int len = strlen(s);
            if (len < 2) continue;
            int a = (s[0] - 'a') * 26 + (s[1] - 'a' + 1);
            int b = (s[len - 2] - 'a') * 26 + (s[len - 1] - 'a' + 1);
            add(a, b, len);
        }
        
        double l = 0, r = 1e3;
        while (r - l > eps) {
            double mid = (l + r) / 2;
            if (spfa(mid)) l = mid;
            else r = mid;
        }
        if (fabs(l) < eps) printf("No solution\n");
        else printf("%lf\n", l);
    }
    return 0;
}

2.1.2 tarjan求scc+缩点判断正环/负环

// 缩点建图(顺便判断是否有解)
    bool success = true;
    for (int i = 1; i <= n + 1; i ++ ) {
        for (int j = h1[i]; ~j; j = ne[j]) {
            int k = e[j];
            int a = scc[i], b = scc[k];
            if (a == b) {
                if (w[j] > 0) {
                    success = false;  // 存在正环
                    break;
                }
            }
            else add(a, b, w[j], h2);
        }
        if (!success) break;
    }

2.1.3 拓扑排序判断正环/负环

判断是否能够构成拓扑序列,能的话说明没有正环/负环,否则有。

2.2 差分约束

2.2.1 spfa差分约束

acwing362区间
给定 n 个区间 [ai,bi]和 n 个整数 ci。你需要构造一个整数集合 Z,使得∀i∈[1,n],Z 中满足ai≤x≤bi的整数 x 不少于 ci 个。求这样的整数集合 Z 最少包含多少个数。
n~5e4, ai,bi~5e4

/*
本题是考察差分约束
本题需要从0~50000中选出尽量少的整数,使得区间[ai, bi]内都有至少ci个数字被选
这里提供的条件为:
1.s[bi] - s[ai-1] >= ci
2.s[k] - s[k - 1] >= 0
3.s[k - 1] - s[k] >= -1
因此,我们需要-1~50000这50002个整数分别作为图中的节点
但是我们可以把整体向上加一,即把0~50001作为节点
*/

#include 

using namespace std;

const int N = 50010, M = N * 3;

int n;
int h[N], w[M], e[M], ne[M], 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 ++ ;
}

// spfa求负环(正环)
bool spfa() {
    queue q;
    
    memset(dist, -0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);
    // 思路1:在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于把全部点放入队列。这样cnt维护的就是到虚拟源点的距离。
    // 思路2:spfa算法一开始加入多少个点到队列都没有关系,因为这些点一开始距离都是无穷,不会引起答案差异。但是由于判断负环,必须把所有点都加入,防止出现孤立连通块的情况。这样cnt维护的就是和某些虚拟点的距离。
    for (int i = 1; i <= n; i ++ ) {
        st[i] = true;
        q.push(i);
    }
    dist[0] = 0, st[0] = 1, q.push(0);  // 如果希望能够正确求出dis数组,那么还需要加上这个代码
    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]) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, -0x3f, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;  // 更新边数
                if (cnt[j] >= N) return true;  // 如果j点到源点的边数大于等于n
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

int main()
{
    scanf("%d", &n);

    memset(h, -1, sizeof h);
    // 读入n个点
    while (n -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        a ++, b ++;  // 整体加一
        add(a - 1, b, c);  // 加边
    }

    // 把1~50001加边
    for (int i = 1; i <= 50001; i ++ )
    {
        add(i - 1, i, 0);
        add(i, i - 1, -1);
    }

    // 跑最长路
    spfa();
    printf("%d\n", dist[50001]);
    return 0;
}

acwing1170排队布局
N头奶牛站成一排,有M1对关系和M2对关系。M1对关系希望A和B至多相隔L,M2对关系希望A和B至少相隔D。输出一个整数,如果不存在满足要求的方案,输出-1;如果 1 号奶牛和 N 号奶牛间的距离可以任意大,输出-2;否则,输出在满足所有要求的情况下,1 号奶牛和 N 号奶牛间可能的最大距离。

/*本题求解最大值,就是跑最短路,得到i<=j+c的关系建图,
然后spfa跑最短路,由于要求1号点到n号点的最短距离,所以直接把1号点作为源点
如果存在负环,那么输出-1;
如果不存在负环,但dis[n]=0x3f3f3f3f,那么-2
否则,输出dis[n]
本题需要注意的是,由于1号点可能为孤立点,因此如果直接把1号点放入队列,其他不放入队列,
那么可能判不出负环,因此需要把所有的点都放入队列。*/

#include 

using namespace std;

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 ++ ;
}

// spfa求负环(正环)
bool spfa() {
    queue q;
    
    memset(dist, 0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);
    // 思路1:在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于把全部点放入队列。这样cnt维护的就是到虚拟源点的距离。
    // 思路2:spfa算法一开始加入多少个点到队列都没有关系,因为这些点一开始距离都是无穷,不会引起答案差异。但是由于判断负环,必须把所有点都加入,防止出现孤立连通块的情况。这样cnt维护的就是和某些虚拟点的距离。
    for (int i = 1; i <= n; i ++ ) {
        st[i] = true;
        q.push(i);
    }
    dist[1] = 0;  
    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]) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, -0x3f, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;  // 更新边数
                if (cnt[j] >= N) return true;  // 如果j点到源点的边数大于等于n
                if (!st[j]) {
                    q.push(j);
                    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()) printf("-1");
    else {
        if (dist[n] == 0x3f3f3f3f) printf("-2\n");
        else printf("%d", dist[n]);
    }

    return 0;
}

acwing393雇佣收银员
24小时需要的收银员数目为r[0], r[1], ..., r[23]
有N个申请人,每个申请人可以工作8小时,问最少需要雇佣多少个收银员才能保证24小时不断营业
N~1e3

/*假设在第i小时开始工作的人有num[i]个,我们选择其中的xi个,那么有0<=xi<=num[i]
对于第i小时,需要r[i]个人,而能够在这个时刻工作的人有xi-7+xi-6+...+xi,要满足xi-7+xi-6+...+xi>=r[i]
则,整理上面式子得到:
记s为xi的前缀和
1. si>=si-1
2. si01>=si-num[i]
3. si>=si-8+r[i], i >=8
4. si>=s16+i+ r[i] - s24
那么我们去枚举s24,一旦发现当前s24的值建出来的图不存在正环,说明存在最小值。
同时,s24是定值,因此还需要添加s24>=c, s24<=c*/
#include 

using namespace std;

int const N = 25, M = 24 * 5;
int e[M], ne[M], w[M], idx, h[N], t, n, wi, dist[N], cnt[N], st[N];
int num[N], r[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 s24) {
    memset(h, -1, sizeof h);
    idx = 0;
    add(0, 24, s24), add(24, 0, -s24);
    for (int i = 1; i <= 24; ++i) {
        add(i - 1, i, 0), add(i, i - 1, -num[i]);
        if (i >= 8) add(i - 8, i, r[i]);
        if (i <= 7) add(16 + i, i, r[i] - s24);
    }
}

// spfa求负环(正环)
bool spfa() {
    queue q;
    
    memset(dist, -0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);
    // 思路1:在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于把全部点放入队列。这样cnt维护的就是到虚拟源点的距离。
    // 思路2:spfa算法一开始加入多少个点到队列都没有关系,因为这些点一开始距离都是无穷,不会引起答案差异。但是由于判断负环,必须把所有点都加入,防止出现孤立连通块的情况。这样cnt维护的就是和某些虚拟点的距离。
    for (int i = 0; i <= 24; i ++ ) {
        st[i] = true;
        q.push(i);
    }
    // dist[0] = 0, st[0] = 1, q.push(0);  如果希望能够正确求出dis数组,那么还需要加上这个代码
    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]) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, -0x3f, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;  // 更新边数
                if (cnt[j] >= 25) return true;  // 如果j点到源点的边数大于等于n
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

int main() {
    cin >> t;
    while (t--) {
        memset(num, 0, sizeof num);
        for (int i = 1; i <= 24; ++i) scanf("%d", &r[i]);
        cin >> n;
        for (int i = 1, k; i <= n; ++i) {
            scanf("%d", &k);
            k++;
            num[k] ++;
        }
        
        bool success = false;
        for (int i = 0; i <= n; ++i) {
            build(i);
            if (!spfa()) {
                cout << i << endl;
                success = true;
                break;
            }
        }
        if (!success) cout << "No Solution\n";
    }
    return 0;
}

2.2.2 tarjan求scc + 缩点 + dp 差分约束

acwing1169糖果
幼儿园里有 N 个小朋友,老师现在想要给这些小朋友们分配糖果,要求每个小朋友都要分到糖果。老师需要满足小朋友们的 K 个要求。老师想知道他至少需要准备多少个糖果。
要求有5种:
如果 X=1.表示第 A 个小朋友分到的糖果必须和第 B 个小朋友分到的糖果一样多。
如果 X=2,表示第 A 个小朋友分到的糖果必须少于第 B 个小朋友分到的糖果。
如果 X=3,表示第 A 个小朋友分到的糖果必须不少于第 B 个小朋友分到的糖果。
如果 X=4,表示第 A 个小朋友分到的糖果必须多于第 B 个小朋友分到的糖果。
如果 X=5,表示第 A 个小朋友分到的糖果必须不多于第 B 个小朋友分到的糖果。
N~1e5, K~1e5, 1 <=A, B <= N

/*
原来的思路是建图后,做差分约束,跑spfa,一旦发现出现正环那么无解,否则求出最长距离,然后累加,这种方法时间卡在spfa上,
spfa有可能跑出O(nm)的时间导致超时
由于数据比较特殊,只有0和1两种,那么可以换一个方法:
对于每一个环,它一定是属于scc,而只要出现1条边权为1的边那么就是出现正环,所有我们可以缩点后,判断每个scc内部是否出现
边权为1的边,一旦出现就是正环,无解;如果没有出现,那么有解,求完scc后缩点,然后按照缩点的逆序(拓扑序)进行dp,求出
最长链dis,然后答案就是每个超级点内点的个数*这个点的最长距离的累加值。
*/
#include

using namespace std;

typedef long long LL;

int const N = 1e5 + 10, M = 6e5 + 10;
// dfn记录每个点的时间戳,low记录每个点的回溯值,scc[i]=x表示i在标号为x的强连通分量里,stk维护一个栈,sccnum记录强连通分量的个数
int dfn[N], low[N], scc[N], stk[N], sccnum, top, timestamp;  
int h1[N], h2[N], e[M], ne[M], idx, w[M];
int n, m;
int scc_count[N];
int dis[N];

// a->b有一条边
void add(int a, int b, int c, int h[])
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// tarjan算法求强连通分量
void tarjan(int root, int h[])
{
    if (dfn[root]) return;  // 时间戳不为0,返回
    dfn[root] = low[root] = ++timestamp;  // 记录当前点的时间戳和回溯值,初始化二者相同,而后dfn[root]>=low[root]
    stk[++top] = root;  // 把根放入栈内
    for (int i = h[root]; i != -1; i = ne[i])  // 遍历每一个与根节点相邻的点
    {
        int j = e[i];  // 与i相邻的点为j
        if (!dfn[j])  // j点没有访问过
        {
            tarjan(j, h);  // 继续dfs,得到所有以j点为根的子树内所有的low和dfn
            low[root] = min(low[root], low[j]);  // 根的low是其子树中low最小的那个
        }
        else if (!scc[j])  // 如果j这个点还在栈内(在栈内的话不属于任何一个scc),同时一个栈内的点在一个scc内
        {
            low[root] = min(low[root], dfn[j]);  // low代表所能到达的最小的时间戳
        }
    }
    
    // 如果root的后代不能找到更浅的节点(更小的时间戳)
    if (low[root] == dfn[root])  // 只有某个强连通分量的根节点的low和dfn才会相同
    {
        sccnum++;
        while (1)  // 出栈直到栈空
        {
            int x = stk[top--];
            scc[x] = sccnum;
            if (x == root) break;
        }
    }
}

int main()
{
    cin >> n >> m;
    memset(h1, -1, sizeof h1);
    memset(h2, -1, sizeof h2);

    // 建图
    for (int i = 0, x, a, b; i < m; ++i) {
        scanf("%d %d %d", &x, &a, &b);
        if (x == 1) add(a, b, 0, h1), add(b, a, 0, h1);
        else if (x == 2) add(a, b, 1, h1);
        else if (x == 3) add(b, a, 0, h1);
        else if (x == 4) add(b, a, 1, h1);
        else if (x == 5) add(a, b, 0, h1);
    }

    // tarjan求scc
    for (int i = 1; i <= n; ++i)
        if (!dfn[i]) tarjan(i, h1);
    
    // 计算每个强连通分量内点的个数
    for (int i = 1; i <= n; ++i) scc_count[scc[i]] ++;
    
    // 缩点建图(顺便判断是否有解)
    bool success = true;
    for (int i = 1; i <= n; i ++ ) {
        for (int j = h1[i]; ~j; j = ne[j]) {
            int k = e[j];
            int a = scc[i], b = scc[k];
            if (a == b) {
                if (w[j] > 0) {
                    success = false;
                    break;
                }
            }
            else add(a, b, w[j], h2);
        }
        if (!success) break;
    }

    // 做dp求最长路
    if (!success) puts("-1");
    else {
        for (int i = sccnum; i; i--) dis[i] = 1;
        for (int i = sccnum; i; i -- ) {
            for (int j = h2[i]; ~j; j = ne[j]) {
                int k = e[j];
                dis[k] = max(dis[k], dis[i] + w[j]);
            }
        }

    // 求答案
    LL res = 0;
    for (int i = 1; i <= sccnum; i ++ ) res += (LL)dis[i] * scc_count[i];
    printf("%lld\n", res);
    }
    return 0;
}

2.2.3 拓扑排序 差分约束

acwing1192奖金
公司按照每个人的贡献给每个人发奖金。奖金存在M对关系,每对关系为a,b,表示a的奖金比b高。每位员工工资最少为100元,问最少需要发多少奖金。

/*
本题是差分约束的简化版,形成的边只有正权边
如果存在正环那么无解,换言之,如果不存在拓扑序则无解,因此可以使用拓扑排序来判断
如果有解,求出拓扑序后,直接按照拓扑序更新最长路即可
*/
#include

using namespace std;

int const N = 1e4 + 10, M = 2e4 + 10;
int n, m;
int din[N], dis[N];
int e[M], ne[M], h[N], idx;
vector ans;

// 拓扑排序
bool topsort()
{
    queue q;
    for (int i = 1; i <= n; ++i)
        if (!din[i]) q.push(i);
    
    while (q.size())
    {
        auto t = q.front();
        q.pop();
        ans.push_back(t);

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            din[j]--;
            if (!din[j]) q.push(j);
        }
    }

    return ans.size() == n;    
}

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

int main()
{
    // 建图
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 0; i < m; ++i)
    {
        int a, b;
        scanf("%d %d", &a, &b);
        add(b, a);
        din[a] ++;
    }

    // 拓扑排序判断是否有解
    if (!topsort()) 
    {
        printf("Poor Xed\n");
        return 0;
    }

    // 按照拓扑排序更新最长路
    for (int i = 1; i <= n; ++i) dis[i] = 100;
    for (int i = 0; i < n; ++i)
    {
        int t = ans[i];
        for (int j = h[t]; ~j; j = ne[j])
        {
            int k = e[j];
            dis[k] = max(dis[k], dis[t] + 1);
        }
    }

    // 计算答案
    int ans = 0;
    for (int i = 1; i <= n; ++i) ans += dis[i];
    cout << ans << endl;
    return 0;
}

你可能感兴趣的:(图论)