• 负环与差分约束


    负环与差分约束

    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 <bits/stdc++.h>
    
    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<int> 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
    ");
            else printf("NO
    ");
        }
        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 <bits/stdc++.h>
    
    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<int> 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 <bits/stdc++.h>
    
    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<int> 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
    ");
            else printf("%lf
    ", 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 <bits/stdc++.h>
    
    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<int> 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
    ", 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 <bits/stdc++.h>
    
    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<int> 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
    ");
            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 <bits/stdc++.h>
    
    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<int> 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
    ";
        }
        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<bits/stdc++.h>
    
    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
    ", res);
        }
        return 0;
    }
    

    2.2.3 拓扑排序 差分约束

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

    /*
    本题是差分约束的简化版,形成的边只有正权边
    如果存在正环那么无解,换言之,如果不存在拓扑序则无解,因此可以使用拓扑排序来判断
    如果有解,求出拓扑序后,直接按照拓扑序更新最长路即可
    */
    #include<bits/stdc++.h>
    
    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<int> ans;
    
    // 拓扑排序
    bool topsort()
    {
        queue<int> 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
    ");
            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;
    }
    
  • 相关阅读:
    android activity状态的保存
    java android 序列号serializable和parcelable
    java 中的 自定义viewUtils框架
    CSS隐藏元素的几种妙法
    WCF、WebAPI、WCFREST、WebService之间的区别
    前端大全
    最全前端资源汇集
    Unicode和汉字编码小知识
    关于写保护
    js实现密码加密
  • 原文地址:https://www.cnblogs.com/spciay/p/13128703.html
Copyright © 2020-2023  润新知