负环 —— SPFA扩展

本文探讨了在有向图和无向图中,如何利用SPFA算法检测负环,特别介绍了两种方法:一种是基于Bellman-Ford的扩展,另一种是通过构造虚拟源点简化操作。着重讲解了如何通过SPFA的时间复杂度分析来快速识别负环,并提供了技巧和示例题解析。
摘要由CSDN通过智能技术生成

负环的定义

在有向图或无向图中,存在一个环,权值之和为负数;

在这里插入图片描述
那么按常规最短路,就会无限在这上面更新了,也就是使得 d i s t ( u ) = − I N F dist(u) = -INF dist(u)=INF

负环的问题最经常结合01分数规划

方法

在这里插入图片描述
建议使用第二种方法,一般来说,方法二跑的比较快;

方法一解释

这个方法是由 B e l l m a n − F o r d Bellman-Ford BellmanFord扩展过来的;

BF算法告诉我们,如果我们迭代了 n n n次,还有节点没更新,那么说明存在负环;

而我们的SPFA是基于BF算法的,在SPFA中,某个点每入队一次,就说明它被更新了一次;

因为我们每次入队都是变小的,那么更新 n n n次,自然就存在负环了;

这个方法完全等价于 B e l l m a n − F o r d Bellman-Ford BellmanFord的方法;

方法二解释(推荐使用)

因为我们一共就 n n n个点,但是现在已经经过了 n n n条边,即包含了 n + 1 n+1 n+1个点;

那么必然存在两个相同的点(存在环),因为SPFA只会在变小的时候更新,因此存在负环;


时间复杂度(技巧)

SPFA的时间复杂度一般是 O ( m ) O(m) O(m)的;

求负环的话,一般在 O ( n m ) O(nm) O(nm)

但是一般来说 O ( n m ) O(nm) O(nm)的时间复杂度是会超时的;

因此当我们超时的时候,我们可以考虑取一个巧;


这有一个常见的技巧,不一定对,但是效果不错;

当SPFA跑的很慢的时候,我们就可以认为存在负环了;

比如说我们可以这样,当所有点的入队次数超过 2 ∗ n 2*n 2n,我们就认为图中有很大可能是存在负环的;

当然这个 2 ∗ n 2*n 2n可以自己改,不一定正确;

方法二模板以及解释

模板

传送门

解释

为什么可以直接往队列中放入所有点?

我们可以构建一个虚拟源点,往每个点连接一条权值为 0 0 0的边;

如果原图存在负环,那么我们新增虚拟源点以后也必然存在负环,因此是等价的;

那么我们按SPFA的流程去走,放入虚拟源点,然后去更新;

我们会发现,虚拟源点会将它能到的所有点入队,而这就相当于在原图上将所有点入队的操作,我们直接省略构建虚拟源点的那一步;


为什么求负环的时候,不需要像求最短路一样,将距离设置为无穷,即 d i s t ( i ) = I N F dist(i) = INF dist(i)=INF,而是 d i s t ( i ) = 0 dist(i) = 0 dist(i)=0

如果存在负环,那么有某些点到虚拟源点的距离是 − I N F -INF INF

由于所有边的权值都是有限的,且负环上的点的个数也是有限的,也就是说权值和也是有限的;

因此我们无论将初值赋为多少,最终都能跑到 − I N F -INF INF去,也就是说会跑无限次(必然超过 n n n次),因此在求负环的时候,赋值无所谓;

例题

虫洞

传送门

题面

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

思路

模板题,我们使用的是方法二;

Code

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>

using namespace std;

typedef long long ll;

const int N = 510,M = 1e4 + 10;

int dist[N],head[N],tot;
bool vis[N];
struct Edge{
    int next,to,val;
}e[M];
void add(int u,int v,int w){
    ++tot;
    e[tot].to = v;
    e[tot].val = w;
    e[tot].next = head[u];
    head[u] = tot;
}
int n,m1,m2;
int cnt[N];//cnt(i)表示第i个点包含的边的数量
bool spfa(){
    memset(vis,0,sizeof vis);
    memset(dist,0,sizeof dist);
    memset(cnt,0,sizeof cnt);
    queue<int> que;
    for(int i=1;i<=n;++i) que.push(i);
    while(!que.empty()){
        int u = que.front();
        que.pop();
        vis[u] = 0;
        for(int i=head[u];i;i=e[i].next){
            int to = e[i].to,w = e[i].val;
            if(dist[to] > dist[u] + w){
                dist[to] = dist[u] + w;
                cnt[to] = cnt[u] + 1;
                if(cnt[to] >= n) return 1;
                if(!vis[to]){
                    que.push(to);
                    vis[to] = 1;
                }
            }
        }
    }
    return 0;
}
void solve(){
    tot = 0;
    memset(head,0,sizeof head);
    cin >> n >> m1 >> m2;
    for(int i=1,u,v,w;i<=m1;++i){
        cin >> u >> v >> w;
        add(u,v,w),add(v,u,w);
    }
    for(int i=1,u,v,w;i<=m2;++i){
        cin >> u >> v >> w;
        add(u,v,-w);
    }
    if(spfa()) cout << "YES\n";
    else cout << "NO\n";
}

signed main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int t;
    cin >> t;
    while(t--)
        solve();
    return 0;
}

观光奶牛

传送门

题面

在这里插入图片描述
在这里插入图片描述

思路

题目要求我们使得 ∑ ( f i ) ∑ ( t i ) \frac{\sum(f_i)}{\sum(t_i)} (ti)(fi)最大;

对于这类问题,我们称为0-1分数规划

像这种问题有一个通用的解法,那就是二分;

设答案为 a n s ans ans,当前二分的值为 m i d mid mid

假设我们现在有一个函数check

m i d ≤ a n s mid ≤ ans midans,即 m i d ≤ ∑ ( f i ) ∑ ( t i ) mid ≤ \frac{\sum(f_i)}{\sum(t_i)} mid(ti)(fi),那么根据二分的思想,我们应该去右区间找;

否则我们应该去左区间找;

现在我们将上面的式子变形一下,

得到 m i d ∗ ∑ ( t i ) − ∑ ( f i ) ≤ 0 { mid*{\sum(t_i) - \sum(f_i)}} ≤ 0 mid(ti)(fi)0

取出累加号,得到 ∑ ( m i d ∗ t i − f i ) ≤ 0 {\sum (mid*t_i-f_i)} ≤ 0 (midtifi)0


至于判断 ∑ ( m i d ∗ t i − f i ) ≤ 0 {\sum (mid*t_i-f_i)} ≤ 0 (midtifi)0,因为我们的边是有向边,我们将点权放在出边上(放入边也行);

也就是说,我们现在将边权看成 m i d ∗ t i − f i mid*t_i-f_i midtifi

那么现在判断 ∑ ( m i d ∗ t i − f i ) ≤ 0 {\sum (mid*t_i-f_i)} ≤ 0 (midtifi)0,其实就等价于判断图上是否存在一个负环;

Code

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>

using namespace std;

typedef long long ll;

const int N = 1010,M = 1e4 + 10;

double dist[N];
int head[N],tot;
bool vis[N];
struct Edge{
    int next,to,val;
}e[M];
void add(int u,int v,int w){
    ++tot;
    e[tot].to = v;
    e[tot].val = w;
    e[tot].next = head[u];
    head[u] = tot;
}
int n,m,f[N];
int cnt[N];//cnt(i)表示第i个点包含的边的数量
const double eps = 1e-4;
bool check(double mid){
    memset(cnt,0,sizeof cnt);
    queue<int> que;
    for(int i=1;i<=n;++i){
        que.push(i);
        vis[i] = 1;
    }
    while(!que.empty()){
        int u = que.front();
        que.pop();
        vis[u] = 0;
        for(int i=head[u];i;i=e[i].next){
            int to = e[i].to;
            double w = mid * e[i].val - f[u];
            if(dist[to] > dist[u] + w){
                dist[to] = dist[u] + w;
                cnt[to] = cnt[u] + 1;
                if(cnt[to] >= n) return 1;
                if(!vis[to]){
                    que.push(to);
                    vis[to] = 1;
                }
            }
        }
    }
    return 0;
}
void solve(){
    cin >> n >> m;
    for(int i=1;i<=n;++i) cin >> f[i];
    for(int i=1,u,v,w;i<=m;++i){
        cin >> u >> v >> w;
        add(u,v,w);
    }
    //(l,r]
    double l = 0,r = 1e3;
    while(r - l > eps){
        double mid = (l+r) / 2;
        if(check(mid)) l = mid;
        else r = mid;
    }
    printf("%.2f\n",r);
}

signed main(){
    solve();
    return 0;
}

单词环

传送门

题面

在这里插入图片描述
在这里插入图片描述

思路

假设我们将一个字符串看成一个点,然后首尾相通看成连边;

我们发现这样建图的话,最坏情况下有 1 0 5 10^5 105个长度为 1000 1000 1000的全是 a a a的字符串;

那么就有 1 0 5 10^5 105个点,以及任意两个点都有边,即 1 0 10 10^{10} 1010条边;

这样肯定是会爆空间以及时间的;


那我们将点和边反转一下,我们现在将一个字符串看成一条边,首尾两个字母看成一个点;

这样至多有 26 ∗ 26 26*26 2626个点, 1 0 5 10^5 105条边,这样建图的话就可以不爆时空;


然后我们发现,我们这样建图与原问题也是等价的;

即求 ∑ ( 字 符 串 长 度 ) ∑ ( 字 符 串 个 数 ) \frac{\sum(字符串长度)}{\sum(字符串个数)} ()()最大值;

这题也是0-1分数规划,那么我们按上一题的思路;

假设答案是 a n s ans ans,当前二分的为 m i d mid mid

m i d ≤ a n s mid ≤ ans midans,即 m i d ≤ ∑ ( 字 符 串 长 度 ) ∑ ( 字 符 串 个 数 ) mid ≤ \frac{\sum(字符串长度)}{\sum(字符串个数)} mid()()

化简一下, ∑ ( m i d ∗ 个 数 − 长 度 ) ≤ 0 \sum(mid *个数 - 长度) ≤ 0 (mid)0

同样将边权看成 m i d ∗ 个 数 − 长 度 mid *个数 - 长度 mid

也就是等价于存在负环;


那么如何判断无解呢?

要想存在负环,那么 m i d mid mid的取值是不是应该越小越好;

m i d mid mid的下限就是取 0 0 0,我们直接将 0 0 0代入,如果连 0 0 0都不行,那么随着 m i d mid mid增大只可能更不行;


按上面的思路走会TLE,因此加了一个上面提到的小技巧;

Code

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>

using namespace std;

typedef long long ll;

const int N = 26*26 + 10,M = 1e5 + 10;

double dist[N];
int head[N],tot;
bool vis[N];
struct Edge{
    int next,to,val;
}e[M];

void add(int u,int v,int w){
    ++tot;
    e[tot].to = v;
    e[tot].val = w;
    e[tot].next = head[u];
    head[u] = tot;
}
int n;
int cnt[N];//cnt(i)表示第i个点包含的边的数量
const double eps = 1e-4;
bool check(double mid){
    memset(cnt,0,sizeof cnt);
    queue<int> que;
    //将所有点入队
    for(int i=0;i<26*26;++i){
        que.push(i);
        vis[i] = 1;
    }
    int sum = 0;
    while(!que.empty()){
        int u = que.front();
        que.pop();
        vis[u] = 0;
        for(int i=head[u];i;i=e[i].next){
            int to = e[i].to;
            // * 1指的是个数
            double w = mid * 1 - e[i].val;
            if(dist[to] > dist[u] + w){
                //上面提到的技巧
                //当所有点的入队次数达到某一个值的时候,差不多就是有负环了
                if(++sum >= 4*26*26) return 1;
                dist[to] = dist[u] + w;
                cnt[to] = cnt[u] + 1;
                if(cnt[to] >= 676) return 1;
                if(!vis[to]){
                    que.push(to);
                    vis[to] = 1;
                }
            }
        }
    }
    return 0;
}
int change(char x){
    return x - 'a';
}
void solve(){
    memset(head,0,sizeof head);
    for(int i=1;i<=n;++i){
        string str;
        cin >> str;
        int w = str.size();
        if(w < 2) continue;
        //映射成26进制数
        int u = change(str[0]) * 26 + change(str[1]);
        int v = change(str[w-2]) * 26 + change(str[w-1]);
        add(u,v,w);
    }
    if(!check(0)){
        cout << "No solution\n";
        return;
    }
    //(l,r]
    double l = 0,r = 1e3;
    while(r - l > eps){
        double mid = (l+r) / 2;
        if(check(mid)) l = mid;
        else r = mid;
    }
    printf("%.2f\n",r);
}

signed main(){
    while(cin >> n,n)
        solve();
    return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值