您好,欢迎来到好走旅游网。
搜索
您的当前位置:首页3.4 SPFA找负环

3.4 SPFA找负环

来源:好走旅游网

负环

有向图或者无向图中, 存在环路, 使得一圈的权值之和 < 0, 称为负环
负环为什么要单独处理, 比如最短路中, 如果一个点能走到负环上, 那么就能在这个点上转无穷多次, 每转1次, 距离就会小一点, 因此图形中, 某些点的最短路就会变成 − ∞ -\infty , 会影响最短路算法的正确性

求负环的常用方法, 基于SPFA
(1) : 统计每个点入队的次数, 如果某个点入队n次(起点不算, 其他点入队n次, 增加n条边), 说明存在负环

(2) : 统计当前每个点最短路中包含的边数, 如果某个点的最短路所包含的边数 >= n, 则也说明存在负环 yxc:一般来说喜欢这种方式

同样n条边, n + 1个点, 总共n个点, 必然存在环, 并且路径可以减小, 负环

算法竞赛进阶指南: Bellman-ford算法判断负环时间复杂度为O(nm), 因为每次转1圈 迭代m条边, 更新1条边,需要迭代n次, 更新O(nm)次, 才能找到负环.SPFS稍微块一些, 但最坏O(nm), 因此一般用SPFA

(yxc:比较trick的做法, 玄学做法 ):比方说所有点入队次数超过某个定值的时候, 就可以说存在负环.

当所有点的入队次数超过2n, 我们就认为图中有很大可能, 是存在负环的.

(yxc: 注意, 这个不一定正确, 肯定有可以构造出来不正确的情况, 但是呢, 这个实际上能取得不错的运行效果, 但我们做一个题目, 一直不停的超时, 不妨试一下这个做法.)(Trick!)

AcWing 904. 虫洞

分析

他希望能够看到出发之前的自己。
请你判断一下约翰能否做到这一点。
就是裸的SPFA求负环问题
数据M = 2500* 2 = 无向边5000 + 虫洞200条边 = 5200, N = 500, 5200 * 500 ~= 10^7

code

#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 5210;
int h[N], e[M], ne[M], w[M], idx;
int n, m1, m2;
bool st[N];
int q[N];
int cnt[N];
int dist[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(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] + 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;
        scanf("%d%d%d", &n, &m1, &m2);
        
        while (m1 --){
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            add(a, b, c), add(b, a, c);
        }
        while (m2 --){
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            add(a, b, -c);
        }
        
        if (spfa()) puts("YES");
        else puts("NO");
    }
    return 0;
}

AcWing 361. 观光奶牛

分析

f i f_i fi:点的权值
t i t_i ti:边的权值
求环上使得 ∑ f i ∑ t i \frac{\sum f_i}{\sum t_i} tifi最大
在图论问题, 求形如 ∑ f i ∑ t i \frac{\sum f_i}{\sum t_i} tifi的问题, 叫做0-1分数规划, 所有形如0-1分数规划的问题, 一般用二分来做
最小值0, 最大值(分子都取1000, 分母都取1), 因此分数值在(0, 1000]之间.
取一个mid, 然后取判断一下图中是否存在一个环, 使得 ∑ f i ∑ t i > m i d \frac{\sum f_i}{\sum t_i} > mid tifi>mid, 假设可以写一个函数判断 ∑ f i ∑ t i > m i d \frac{\sum f_i}{\sum t_i} > mid tifi>mid, 那么我们要找的值在[mid, R]之间, L = mid; 如果判断出来没有任何一个环 ∑ f i ∑ t i > m i d \frac{\sum f_i}{\sum t_i} > mid tifi>mid, 说明答案在[0, mid - 1], R = mid - 1;
因此可以根据判断的结果, 每次可以将答案的区间每次缩小一般, 最终可以找到答案

如果判断图中是否存在一个环使得 ∑ f i ∑ t i > m i d \frac{\sum f_i}{\sum t_i} > mid tifi>mid?
由于所有分母是整数,
∑ f i > m i d ∗ ∑ t i ∑ f i − m i d ∗ ∑ t i > 0 \sum f_i > mid * \sum t_i\\ \sum f_i - mid * \sum t_i > 0 fi>midtifimidti>0
这里既有点权, 又有边权, 不是很方便, 如果求的是最短距离的话, 可以把点权放到边上去,如果是有向边的话, 点权可以放到出边/入边上, 都是等价的.

为什么点权可以放到边上去呢?
比如说把点权f1放到边上, 边权变成了f1 + t1. 现在要证明的是把点权放到出边上的是等价的
那么对于任何一个环来说, 点权 + 边权 = ∑ f i + ∑ t i \sum f_i + \sum t_i fi+ti, 同样道理, 把点权放到出边上, 总共的权值之和 = ∑ f i + ∑ t i \sum f_i + \sum t_i fi+ti, 结果是一样的

∑ ( f i − m i d ∗ t i ) > 0 \sum (f_i - mid * t_i) > 0 (fimidti)>0
即: 将原本 i → j i \to j ij上的权重由 t k t_k tk 看成是 f i − m i d ∗ t k f_i - mid * t_k fimidtk

code

注意dist 换成double, double l, r;

#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010, M = 5010;
int h[N], e[M], wt[M], ne[M], idx;
int n, m;
int wf[N];
bool st[N];
double dist[N];
int q[N], cnt[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(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 ++ ){
        scanf("%d", &wf[i]);
    }
    
    while (m --){
        int a, b, c;
        scanf("%d%d%d", &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;
}

1165. 单词环

分析

需要考虑3个问题

建图: 可以把每个字符串看成1个点, 1后面可以接2, 就建一条 1 → 2 1 \to 2 12, 2后面接3, 建立 2 → 3 2 \to 3 23, 然后 3 → 1 3 \to 1 31, 这样建图是可以, 但是数据会过大, 题目 1 0 5 10^5 105个点, 假设字符串都是字母aaaaaaa…aa, 那么每个字符串都可以连接, 需要建 ( 1 0 5 ) 2 = 1 0 10 (10^5)^2 = 10^{10} (105)2=1010, 爆空间/时间
可以看成一个pair的形式, 将第1个字符串ababc看成ab ⟶ 5 \stackrel{5}{\longrightarrow} 5bc
对于第2字符串bckjaca来说, 相当于bc ⟶ 7 \stackrel{7}{\longrightarrow} 7ca
第3个字符串caahoynaab, 相当于ca ⟶ 10 \stackrel{10}{\longrightarrow} 10ab
一共 1 0 5 10^5 105条边, 因为题目要求接龙2个字母才可以接, 26* 26 = 676个点

这样的建图方式, 这样的图中的环 是否和题目中的环是等价的呢?
这样的图中, 将任何一个点换成原来的字符串后, 等价于图中的定义的字符串的环.
任意的字符串的环, 将每个字符串看成一条边, 字符串的开头和结尾看成两个点, 就可以对应图里的一个环

6.7 ∗ 1 0 7 6.7 * 10^7 6.7107 复杂度
剩下的问题变成
w i w_i wi: 边权, 点权: 单词权重为1
∑ w i ∑ 1 \frac{\sum w_i}{\sum 1} 1wi最大
先看下答案区间, 分子, 分母 >0, 因此最小值0, 最大值的话, 每条边权最大1000, 单词权重1, 因此最大1000
无解的话, 也需要判断, 有解的话一定在(0, 1000]内, 如果在范围内找不到, 表示无解.
每次二分一个中点M , 判断图中是否存在环 使得左边等式 > M
∑ w i ∑ 1 > M ⇔ ∑ w i > M ∗ ∑ 1 ⇔ ∑ w i − M ∑ 1 > 0 ⇔ ∑ ( w i − M ∗ 1 ) > 0 \frac{\sum w_i}{\sum 1} > M\\ \Leftrightarrow \sum w_i > M * \sum 1\\ \Leftrightarrow \sum w_i - M \sum 1 > 0 \\ \Leftrightarrow \sum (w_i - M * 1) > 0 1wi>Mwi>M1wiM1>0(wiM1)>0
所以, 这里没有点权, 1也是在单词的边权上, 我们可以重新定义我们的边权为 w i − M ∗ 1 ⇔ w_i - M * 1 \Leftrightarrow wiM1图中是否存在正环
可以发现 0 < M ≤ 1000 0 < M \leq 1000 0<M1000, 但M = 0的时候如果都没有正环, 那么M > 0的时候, 边权会减小, 那就更没有可能有正环了, 因此判断的时候, 直接将M = 0 代入, 来判断

code

#include <iostream>
#include <cstring>
using namespace std;
const int N = 700, M = 1e5 + 10;
int n;
int h[N], e[M], w[M], ne[M], idx;
int q[N], cnt[N];
double dist[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; // 10000 是经验值
                if (cnt[j] >= N) return true;
                if (!st[j]){
                    st[j] = true;
                    q[tt ++ ] = j;
                    if (tt == N) tt = 0;
                }
            }
        }
    }
    return false;
}
int main(){
    char str[1010];
    while (scanf("%d", &n), n){
        idx = 0;
        memset(h, -1, sizeof h);
        for (int i = 0; i < n; i ++ ){
            scanf("%s", str);
            int len = strlen(str);
            if (len >= 2){
                int left = (str[0] - 'a') * 26 + (str[1] - 'a');
                int right = (str[len - 2] - 'a') * 26 + (str[len - 1] - 'a');
                add(left, right, len);
            }
        }

        if (!check(0)) puts("No solution");
        else {
            double l = 0, r = 1010;
            while (r - l > 1e-4){ 
                double mid = (l + r) / 2;
                if (check(mid)) l = mid;
                else r = mid;
            }
            printf("%lf\n", r);
        }
    }
    return 0;
}

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- haog.cn 版权所有 赣ICP备2024042798号-2

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务