负环

负环

1. 负环原理

原理

  • 求解图中的负环,是基于spfa算法(n表示图中顶点数):

(1)统计每个点入队的次数,如果某个点入队n次,则说明存在负环;

(2)统计当前每个点的最短路中所包含的边数,如果某点的最短路径所包含的边数大于等于n,则说明存在负环。

  • 对于(1)来说,因为spfa算法是基于bellmanford算法改进来的,对于bellmanford算法来说,如果迭代了n此之后还有节点被更新的话,说明存在负环;对应到spfa算法来说,某个点每入队一次,则说明其被更新了一次,因此如果某个点入队大于等于n次,则说明其被更新了大于等于n次,说明存在负环。
  • 对于(2)来说,如果某点的最短路径所包含的边数大于等于n,则说明这条路径中至少存在n+1个点,因为只有n个点,根据抽屉原理,必定有两个节点相同,因为只有距离变小时才会更新,因此存在负环。
  • 上面的两种方法最常用的是(2)。原因如下:

在这里插入图片描述

  • spfa算法:

在这里插入图片描述

  • spfa求负环:

在这里插入图片描述

  • spfa算法求负环,有时候可能时间复杂度接近 O ( n × m ) O(n\times m) O(n×m),会超时,对于方法(2)这里有一个技巧:当所有的点入队的次数超过2n(或者3n)时,我们就认为图中有很大可能是存在负环的。

代码模板

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

const int N = 2010, M = 10010;

int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N];  // cnt[i]表示到虚拟源点的距离-1
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() {
    
    queue<int> q;
    for (int i = 1; i <= n; i++) {  // 相当于有一个虚拟源点,所有的点都要加入
        q.push(i);
        st[i] = true;
    }
    
    while (q.size()) {
        
        int t = q.front(); q.pop();
        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.push(j);
                    st[j] = true;
                }
            }
        }
    }
    
    return false;
}

int main() {
    
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);
    
    for (int i = 0; i < m; 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;
}	

2. AcWing上的负环题目

AcWing 904. 虫洞

问题描述

分析

  • 本题有多组数据。
  • 农田代表图中的点,路径代表双向边(其上的权值为正),虫洞代表单向边(其上的权值为负)。因此本问题就可以转换成:该图中是否存在负环?
  • 直接使用spfa算法求解即可。

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 510, M = 5210;

int n, m, k;  // n:顶点数; m:双向边数; k:单向边数
int h[N], e[M], w[M], ne[M], idx;
int dist[N], cnt[N];
int 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++;
}

bool spfa() {
    
    // dist初始值无所谓
    // st最终在队列为空时也会全部变为false
    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] + 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;
    cin >> T;
    while (T--) {
        
        memset(h, -1, sizeof h);
        idx = 0;
        
        cin >> n >> m >> k;
        while (m--) {
            int a, b, c;
            cin >> a >> b >> c;
            add(a, b, c), add(b, a, c);
        }
        while (k--) {
            int a, b, c;
            cin >> a >> b >> c;
            add(a, b, -c);
        }
        
        if (spfa()) puts("YES");
        else puts("NO");
    }
    
    return 0;
}

AcWing 361. 观光奶牛

问题描述

分析

  • 分析题目可知,此题让我们求解一个环,使得

∑ f i ∑ t i \frac{\sum f_i}{\sum t_i} tifi

取得最大值。

  • 所有形如这样的问题,有一个统一的名称:01分数规划。这样的问题一般都可以采用二分来解决。
  • 针对本题,因为 1 ≤ f [ i ] , t [ i ] ≤ 1000 1\le f[i],t[i] \le 1000 1f[i],t[i]1000,所以答案所在的范围是:(0, 1000],可以对该区间进行二分。
  • 对于区间[l, r]以及mid=(l + r) / 2,如果有

∑ f i ∑ t i > m i d f i 是 环 上 的 点 的 权 值 , t i 是 环 上 边 的 权 值 \frac{\sum f_i}{\sum t_i} > mid \quad f_i是环上的点的权值,t_i是环上边的权值 tifi>midfiti

则说明答案在[mid, r]之间,否则答案在[l, mid]之间,接着二分即可。

  • 对上面的不等式进行变形:

∑ f i ∑ t i > m i d ∑ f i − m i d × ∑ t i > 0 ∑ ( f i − m i d × t i ) > 0 \frac{\sum f_i}{\sum t_i} > mid \\ \sum f_i - mid \times \sum t_i > 0 \\ \sum (f_i-mid \times t_i) > 0 tifi>midfimid×ti>0(fimid×ti)>0

  • 根据上面的变形,我们可以将点上的权值变到出边上去,即每条边的权值变为 f i − m i d × t i f_i-mid \times t_i fimid×ti,这样方便我们处理。
  • 在变换边权之后,最终问题就转化成了:图中是否存在正环。
  • 那么如何求解正环呢?可以将所有的边权取个反,问题就变成了是否存在负环;但是实际上我们不需要这样做,我们只需要求最长路径即可。

在这里插入图片描述

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 1010, M = 5010;

int n, m;
int wf[N];  // 每个点的权值
int h[N], e[M], wt[M], ne[M], idx;
double dist[N];
int cnt[N];  // cnt[i]表示到达i的最长路径经过的边数
int q[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) {
    
    // 该函数会被调用多次,因此需要初始化cnt
    memset(cnt, 0, sizeof cnt);
    // dist不需要初始化,因为存在正环的话,到达该点的距离会被更新为+∞
    // st不需要初始化,因为最终队列为空,st的值会全部变为false
    
    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;
    for (int i = 1; i <= n; i++) cin >> wf[i];
    
    memset(h, -1, sizeof h);
    while (m--) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    
    double l = 0, r = 1010;
    while (r - l > 1e-4) {
        double mid = (l + r) / 2;
        if (check(mid)) l = mid;
        else r = mid;
    }
    
    printf("%.2lf\n", l);
    
    return 0;
}

AcWing 1165. 单词环

问题描述

分析

  • (1)首先我们需要考虑如何建图:一种直观的建图方式是顶点是各个字符串,如果字符串s2能接在s1的后面,那么连一条从s1指向s2的边。这样的话图中顶点的个数最多有 1 0 5 10^5 105个,可能是完全图,因此边的数量在 1 0 10 10^{10} 1010量级,会超时,因此这样的建图方式不可取。

  • 我们换一种建图的方式:顶点表示一个字符串的前两个字母或者后两个字母,边表示字符串的长度。相当于将上面一种建图方式中的顶点变成了边,边变成了顶点。这样的话,图中顶点的个数为676( 26 × 26 26\times 26 26×26),边数最多 1 0 5 10^5 105个。

  • (2)另外我们还需要考虑一个问题,如何使得

∑ w i ∑ s i w i 表 示 环 上 字 符 串 长 度 , s i 恒 为 1 \frac{\sum w_i}{\sum s_i} \quad w_i表示环上字符串长度,s_i恒为1 siwiwisi1

最大。我们可以使用和上一题一样的思路:01分数规划,使用二分解决。

  • 针对本题,因为 1 ≤ w [ i ] ≤ 1000 1\le w[i] \le 1000 1w[i]1000,所以答案所在的范围是:(0, 1000],可以对该区间进行二分。
  • 对于区间[l, r]以及mid=(l + r) / 2,如果有

∑ w i ∑ 1 > m i d \frac{\sum w_i}{\sum 1} > mid 1wi>mid

则说明答案在[mid, r]之间,否则答案在[l, mid]之间,接着二分即可。

  • 对上面的不等式进行变形:

∑ w i ∑ 1 > m i d ∑ w i − m i d × ∑ 1 > 0 ∑ ( w i − m i d × 1 ) > 0 \frac{\sum w_i}{\sum 1} > mid \\ \sum w_i - mid \times \sum 1 > 0 \\ \sum (w_i-mid \times 1) > 0 1wi>midwimid×1>0(wimid×1)>0

  • 根据上面的变形,每条边的权值变为 w i − m i d w_i-mid wimid,在变换边权之后,最终问题就转化成了:图中是否存在正环。
  • 那么如何求解正环呢?可以将所有的边权取个反,问题就变成了是否存在负环;但是实际上我们不需要这样做,我们只需要求最长路径即可。
  • (3)另外题目可能没有解,如何判断这种情况呢?对于改变后的边权 w i − m i d w_i-mid wimid,这个值越大,越有可能有正环,如果mid取0时都没有正环,则说明没有解。特判一下即可,我们可以使用AcWing 361. 观光奶牛的做法解决这个问题。
  • (4)如果单纯使用AcWing 361. 观光奶牛方法解决这个问题,会超时(TLE),因此需要使用上面原理中提到的技巧:当所有的点入队的次数超过2n(或者3n)时,我们就认为图中有很大可能是存在负环的。但是注意:本题的点数太少,不到700,但是边数非常多,最多有10000条,所以经验值不适用了,需要取一个更大的值。
  • 另外对于求负环的问题,如果超时的话,我们还可以尝试将队列改为栈,有可能更快找到这个环,这样一般都是能解决TLE问题的。

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 700, M = 100010;

int n;  // 图中边数,有多少字符串就有多少条有向边
int h[N], e[M], w[M], ne[M], idx;
double dist[N];
int cnt[N];  // cnt[i]表示到达i的最长路径经过的边数
int 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++;
}

bool check(double mid) {
    
    memset(cnt, 0, sizeof cnt);
    
    int hh = 0, tt = 0;
    for (int i = 0; i < 26 * 26; 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 (cnt[j] >= N) return true;  // n是边数,N才是点数
                
                if (++count > N * 10) return true;  // 经验上的trick
                
                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 c = strlen(str);
            if (c >= 2) {
                int a = (str[0] - 'a') * 26 + str[1] - 'a';
                int b = (str[c - 2] - 'a') * 26 + str[c - 1] - 'a';
                add(a, b, c);
            }
        }
        
        if (!check(0)) puts("No solution");
        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;
}
#include <iostream>
#include <cstring>

using namespace std;

const int N = 700, M = 100010;

int n;  // 图中边数,有多少字符串就有多少条有向边
int h[N], e[M], w[M], ne[M], idx;
double dist[N];
int cnt[N];  // cnt[i]表示到达i的最长路径经过的边数
int stk[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(cnt, 0, sizeof cnt);
    
    int tt = 0;
    for (int i = 0; i < 26 * 26; i++) {
        stk[++tt] = i;
        st[i] = true;
    }
    
    while (tt > 0) {  // 此时tt指向栈顶元素
        
        int t = stk[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;  // n是边数,N才是点数
                
                if (!st[j]) {
                    stk[++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 c = strlen(str);
            if (c >= 2) {
                int a = (str[0] - 'a') * 26 + str[1] - 'a';
                int b = (str[c - 2] - 'a') * 26 + str[c - 1] - 'a';
                add(a, b, c);
            }
        }
        
        if (!check(0)) puts("No solution");
        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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值