【算法提高课】图论:SPFA找负环


title: 【算法提高课】图论:SPFA找负环
katex: true
tags:

  • Acwing
  • medium
  • 图论
    categories: 算法提高课

关于负环

  • 定义: 负环指一个图中边权和为负数的环
  • 求法(SPFA):
    • 统计每个点的入队次数,如果某个点的入队次数为 n n n ,则存在负环(容易被一个环卡爆)
    • 统计每个点的最短路包含的边数,如果某个点的最短路包含的边数大于等于 n n n ,也说明存在负环
  • 为什么要一开始把所有点都入队: 假想一个虚拟原点,将虚拟原点和所有点连一条权值为 0 0 0 的有向边,而导致的结果是所有点都会入队,故省去这一操作直接把所有点入队
  • 为什么dis数组的初值可以 0: 由于 S P F A SPFA SPFA 算法是正确的,那么对于是有限值的边,对于一个负环来说,迭代无数次最终的结果还是负无穷,所以初值无影响
  • 相关: 01分数规划
  • trick做法(基于经验,可能不正确但可以降低复杂度避免卡O(nm)超时): 当所有点的入队次数都超过 2 n 2n 2n 时,我们认为图中存在负环

Acwing.904 虫洞

  • 题意: SPFA找负环板子,未使用trick
  • C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int head[N],nxt[N],w[N],idx,to[N];
int dis[N],cnt[N];
bool st[N];
int n,m,M;

void add(int u,int v,int val){
    to[++idx]=v,w[idx]=val,nxt[idx]=head[u],head[u]=idx;
}

bool spfa(){
    queue<int>q;
    memset(dis,0,sizeof dis);
    memset(cnt,0,sizeof cnt);
    memset(st,false,sizeof st);
    
    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=head[t];i;i=nxt[i]){
            int j=to[i];
            
            if(dis[j]>dis[t]+w[i]){
                dis[j]=dis[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;
}

void solve(){
    memset(head,0,sizeof head);

    cin>>n>>m>>M;
    for(int i=1;i<=m;i++){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c),add(b,a,c);
    }
    for(int i=1;i<=M;i++){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,-c);
    }
    
    if(spfa())  puts("YES");
    else    puts("NO");
    
}

int main(){
    int t;
    cin>>t;
    while(t--){
        solve();
    }
    return 0;
}

Acwing.361 观光奶牛

  • 题意: 给定一张 L ( 1000 ) L(1000) L(1000) 个点, P ( 5000 ) P(5000) P5000) 条边的有向图,每个点都有一个权值 f [ i ] f[i] f[i] ,每条边都有一个权值 t [ i ] t[i] t[i] 。求图中的一个环,使环上各点的权值之和除以环上各边的权值之和最大
  • 01分数规划: 类似于上面加粗字体的问题描述便是01分数规划问题,解决之类问题可以考虑使用二分答案求解。如对于本题来说: ∑ n i = 1 f [ i ] ∑ n i = 1 t [ i ] \sum_{n}^{i=1} {f[i]} \over \sum_{n}^{i=1} {t[i]} ni=1t[i]ni=1f[i] > m i d >mid >mid ,有一个巧妙操作:将点权转化为边权,即把环中的点权视为其出边的边权。由此我们可以把式子变形: ∑ n i = 1 f [ i ] − m i d ∗ t [ i ] \sum_{n}^{i=1} {f[i]-mid*t[i]} ni=1f[i]midt[i] > 0 >0 >0,由此我们便把问题转化为了是否有正环。
  • 如何处理正环? 将求最短路变成求最长路即可
  • 浮点二分: 对于本题保留两位小数那么只需要l-r>1e-4即可,每增加一位小数向右移动一位
  • C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;

int head[N],nxt[N],to[N],idx;
double wt[N],wf[N];
double dis[N];
bool st[N];
int cnt[N];
int n,m;

void add(int u,int v,double val){
    to[++idx]=v,wt[idx]=val,nxt[idx]=head[u],head[u]=idx;
}

bool check(double x){
    memset(st,false,sizeof st);
    memset(dis,0,sizeof dis);
    memset(cnt,0,sizeof cnt);
    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=head[t];i;i=nxt[i]){
            int j=to[i];
            
            if(dis[j]<dis[t]+wf[t]-wt[i]*x){
                dis[j]=dis[t]+wf[t]-wt[i]*x;
                cnt[j]=cnt[t]+1;
                if(cnt[j]>=n)   return true;
                if(!st[j]){
                    q.push(j);
                    st[j]=true;
                }
            }
        }
    }
    return false;
}

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)   cin>>wf[i];
    for(int i=1;i<=m;i++){
        int a,b;
        double c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    
    double l=0,r=1010;
    double mid;
    while(r-l>1e-4){
        mid=(l+r)/2;
        if(check(mid))  l=mid;
        else    r=mid;
    }
    
    printf("%.2lf",r);
    
    
    return 0;
}

Acwing.1165 单词环

  • 题意: 给定 n ( 1 e 5 ) n(1e5) n(1e5) 个字符串,每个字符串都由小写字母组成,如果一个字符串的前两个单词能和另一个字符串的后两个单词相匹配,那么我们称这两个字符串能相连。求给定字符串能连成的环的最大平均长度。
  • 思路: 涉及到除法那么我们可以考虑01分数规划,使用二分答案来解决,步骤类似上题
  • 如何建图? 可以看到字符串数量非常大,如果是以字符串为点的话 n 2 n^2 n2 显然是不能接受的,而且还要额外处理能不能连接的关系。可以这样考虑:将每两个小写字母的组合作为一个点,那么这样只有 26 × 26 = 676 26×26=676 26×26=676 个点,将字符串作为边,那么这个复杂度是勉强是可以接受的。我们把两个组合哈希成一个数字即可:
int left=(str[0]-'a')*26+str[1]-'a';  
int right=(str[str.size()-2]-'a')*26+str[str.size()-1]-'a';`
  • 时间优化: 本题有多个测试数据,如果不优化的话很可能超时。那么就需要用到一开始的玄学优化大法。本来取值通常取两倍的点数,但是此题点少边多,经验失效,取5000
int count=0;
while(---){
---
---
	for(int i=head[t];i;i=nxt[i]){
	---
	if(++count>5000)	return true;
	}
	---
}
  • 雷点: dis数组并不需要重置,在本题重置了dis会超时
  • C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int head[N],nxt[N],to[N],idx;
double w[N];
int cnt[N];
double dis[N];
bool st[N];
int n,m;

void add(int u,int v,double val){
    to[++idx]=v,w[idx]=val,nxt[idx]=head[u],head[u]=idx;
}

bool check(double x){
    memset(dis,0,sizeof dis);
    memset(cnt,0,sizeof cnt);
    memset(st,false,sizeof st);
    queue<int>q;
    for(int i=0;i<676;i++){
        q.push(i);
        st[i]=true;
    }
    
    int count=0;
    
    while(q.size()){
        int t=q.front();
        q.pop();
        st[t]=false;
        
        for(int i=head[t];i;i=nxt[i]){
            int j=to[i];
            
            if(dis[j]<dis[t]+w[i]-x){
                dis[j]=dis[t]+w[i]-x;
                cnt[j]=cnt[t]+1;
                if(++count>1000)   return true;
                if(cnt[j]>=676)  return true;
                if(!st[j]){
                    q.push(j);
                    st[j]=true;
                }
            }
        }
    }
    return false;
}

int main(){
   // std::ios::sync_with_stdio(false);
   cin.tie(0);
    //cout.tie(0);
    while(scanf("%d",&n),n){
     //   cin>>n;
        memset(head,0,sizeof head);
        idx=0;
        string str;
        for(int i=1;i<=n;i++){
            cin>>str;
            if(str.size()<2)    continue;
            int left=(str[0]-'a')*26+str[1]-'a';
            int right=(str[str.size()-2]-'a')*26+str[str.size()-1]-'a';
            add(left,right,str.size());
        }
        
        if(!check(0))   puts("No solution");
        else{
            double l=0,r=1010;
            double mid;
            while(r-l>1e-4){
                mid=(l+r)/2;
                if(check(mid))  l=mid;
                else    r=mid;
            }
            cout<<r<<endl;;
        }
    }

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值