3.4 SPFA找负环

负环

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

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

实际上就是bellman-ford算法:
bellman-ford算法说迭代n次, 还有点被更新的话, 说明存在负环
bellman-ford算法, 每更新1次, 最短路径中边的数量就会+1, 更新n次, 最短路径中边数 >= n. 如果1条路径中边的数量 >= n, 一共n条边, n + 1个点, 总共只有n个点, 那么必定有两个点重合, 必然存在一个环, 只有在边的距离不断变小才会更新, 更新这两个点形成的环总和一定 < 0.
转移到spfa里, 某个点入队1次, 说明被更新了1次, 入队>=n, 说明被更新>=n次

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

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

算法竞赛进阶指南: Bellman-ford算法判断负环时间复杂度为O(nm), 因为每次转1圈 迭代m条边, 更新1条边,需要迭代n次, 更新O(nm)次, 才能找到负环.SPFS稍微块一些, 但最坏O(nm), 因此一般用SPFA
在这里插入图片描述
spfa O(m)
实际上 时间复杂度O(nm)比较慢, 虽然理论来说来只有当边数 >= n的时候, 才存在负环, 很多情况下这样会超时, 但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
在这里插入图片描述
那么问题等价于 图形中是否存在环, 使得所有边权之和 > 0, 也就等价于 图中是否存在正环
在这里插入图片描述
正环的, 把所有边权值取一个负号, 实际中, 不用真的取负号, 而将求最短路改成求最长路, 然后统计下每个路径包含的边数即可
最终: 用二分->判断不等式成不成立, 成立的话, 答案落在右半区间; 否则, 答案在左半区间.
数据范围: 点数*边数 = 5000, 000不是很大

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. 建图
  2. ∑ w i ∑ s i \frac{\sum w_i}{\sum s_i} siwi 最大
  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;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值