负环与差分约束

文章目录

  • 负环与差分约束
    • 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<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, 0xc0, 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<int> q;
    
    memset(dist, 0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);
    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, 0xc0, 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

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