SPFA的拓展应用

spfa的拓展应用——负环

理论

  • 01分数规划
  • 负环:一个环边权之和小于零

求负环的基本方法,基于SPFA:

都是基于抽屉原理,如果超过n条边,那一定有两个点相同,那就一定存在一个环

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

  • 本质是bellman_ford——O( n 2 n^2 n2)

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

(3) 取巧做法trick:

  • 当所有点入队次数超过一定阈值时, 2 n 2n 2n,则我们认为图中有很大可能是存在负环的

初始化tips

  • 将起始点全部入队
  • 建立虚拟原点

基本模版

int d[N];

void spfa(){
  memset(d,0x3f,sizeof d);
  d[1]=0;
  st[1]=1;
  queue<int> q;
  
  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(!st[j]){
        d[j]=min(d[t]+g[t][j],d[j]);
        st[j]=true;
      }
    }
  }
}

拓展模版

在基础之上增加一个cnt[N]用来判断负环

题单

1.虫洞

思考:

  • “表示存在一条从田地 𝑆 走到田地 𝐸 的虫洞,走过这条虫洞,可以回到 T T T 秒之前。”回到起始点之前,这里是将时间当作边的长度,比起始的长度还小,表明存在负环
  • 所以直接spfa开打
  • 打完单点spfa发现并没有告诉出发点,并且是说几片农场,有些农场可能并没有连通,全部点先入队一遍吧
#include<bits/stdc++.h>

using namespace std;
int f,n,m,w;
const int N=510,M=1e4+10;
int h[N],e[M],ne[M],weight[M],idx;
int d[N],st[N],cnt[N];

void add(int a,int b,int c){
  e[idx]=b,weight[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

bool spfa(){
  memset(d,0x3f,sizeof d);
  memset(st,0,sizeof st);
  memset(cnt,0,sizeof cnt);
  queue<int> q;
  d[0]=0;
  for(int i=1;i<=n;i++){
    add(0,i,0);
    add(i,0,0);
  }
  q.push(0);
  
  while(q.size()){
    int t=q.front();
    q.pop();
    st[t]=0;
    for(int i=h[t];i!=-1;i=ne[i]){
      int j=e[i];
      if(d[j]>d[t]+weight[i]){
        d[j]=d[t]+weight[i];
        cnt[j]=cnt[t]+1;
        //cout<<t<<' '<<j<<' '<<d[j]<<endl;
        if(cnt[j]>=n+1){
            
            return 1;
        }
        if(!st[j]){
          q.push(j);
          st[j]=1;
        }
      }
    }
  }
  return 0;
}

signed main(){
  cin>>f;
  while(f--){
    cin>>n>>m>>w;
    memset(h,-1,sizeof h);
    idx=0;
    for(int i=0;i<m;i++){
      int x,y,z;
      cin>>x>>y>>z;
      add(x,y,z),add(y,x,z);
    }
    for(int i=0;i<w;i++){
      int a,b,c;
      cin>>a>>b>>c;
      add(a,b,-c);
    }
    int res=spfa();
    if(!res) puts("NO");
    else puts("YES");
  }
  return 0;
}

不能直接用虚拟原点建边,这样会引入多一个点,很容易出现负环,虚拟原点到其他点的变长不好定义

如图:

image-20240629012754545

#include<bits/stdc++.h>

using namespace std;
int f,n,m,w;
const int N=510,M=1e4+10;
int h[N],e[M],ne[M],weight[M],idx;
int d[N],st[N],cnt[N];

void add(int a,int b,int c){
  e[idx]=b,weight[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

bool spfa(){
  memset(d,0,sizeof d);
  memset(st,0,sizeof st);
  memset(cnt,0,sizeof cnt);
  queue<int> q;
  
  for(int i=1;i<=n;i++){
    q.push(i);
    st[i]=1;
  }
  
  while(q.size()){
    int t=q.front();
    q.pop();
    st[t]=0;
    for(int i=h[t];i!=-1;i=ne[i]){
      int j=e[i];
      if(d[j]>d[t]+weight[i]){
        d[j]=d[t]+weight[i];
        cnt[j]=cnt[t]+1;
        //cout<<t<<' '<<j<<' '<<d[j]<<endl;
        if(cnt[j]>=n){
            return 1;
        }
        if(!st[j]){
          q.push(j);
          st[j]=1;
        }
      }
    }
  }
  return 0;
}

signed main(){
  scanf("%d",&f);
  while(f--){
    cin>>n>>m>>w;
    memset(h,-1,sizeof h);
    idx=0;//别忘了初始化idx
    for(int i=0;i<m;i++){
      int x,y,z;
      cin>>x>>y>>z;
      add(x,y,z),add(y,x,z);
    }
    for(int i=0;i<w;i++){
      int a,b,c;
      cin>>a>>b>>c;
      add(a,b,-c);
    }
    int res=spfa();
    if(!res) puts("NO");
    else puts("YES");
  }
  return 0;
}
2. 观光奶牛

第一眼:

  • “环上各点的权值之和”除以“环上各边的权值之和”第一次见

听y说:

  • 01分数规划问题

思考:

  • 怎么去体现最大?
    • 用mid去划分,因为 ∑ ( f i ) ∑ ( w i ) \frac{\sum(f_i)}{\sum(w_i)} (wi)(fi)有一个取值范围
  • 那要怎么找到满足大于mid的环?
    • 确切的说是如何找到环
    • 这个mid是用来生成新的边找到正环的
  • 注意”把点权放进边中“这句话在求最长路中的写法
#include<bits/stdc++.h>

using namespace std;
typedef pair<int,int> PII;
int n,m;
const int N=1e3+10,M=5e3+10;

int fa[N],f[N];
int h[N],hr[N],e[M],ne[M],w[M],idx;
int cnt[N],st[N];
double d[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){
  //其实d初不初始化都没太大关系,比较的只是相对关系,已经在
  //同一层变化的相对关系是不会发生变化的
  //memset(d,0,sizeof d);
  memset(cnt,0,sizeof cnt);
  memset(st,0,sizeof st);

  
  int q[N];
  int hh=0,tt=0;
 	for(int i=1;i<=n;i++){
    q[tt++]=i;
    st[i]=1;
  }
  while(hh!=tt){
    int t=q[hh++];
    if(hh==N) hh=0;
    
    st[t]=0;
    for(int i=h[t];i!=-1;i=ne[i]){
      int j=e[i];
      if(d[j]<d[t]+f[t]-mid*w[i]){
        d[j]=d[t]+f[t]-mid*w[i];
        cnt[j]=cnt[t]+1;
        if(cnt[j]>=n) return true;
        if(!st[j]){
          st[j]=1;
          q[tt++]=j;
          if(tt==N) tt=0;
        }
      }
    }
  }
  return false;
}

signed main(){
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>f[i];
  memset(h,-1,sizeof h);
  for(int i=0;i<m;i++){
    int a,b,c;
    cin>>a>>b>>c;
    add(a,b,c);
  }
  
  double l=1,r=1001;
  while(r-l>1e-4){
    double mid=(l+r)/2;
    if(check(mid)) l=mid; 	//如果存在则说明答案在mid右边
    else r=mid;
  }
  printf("%.2f",l);
  return 0;
}
3. 单词环

第一眼:

  • 不太清楚为什么和求负环有关系
  • 和单词接龙会不会有关系
    • 相同的处理方式:处理两个串之间的?。。。。感觉又不行

思路:

  • 建图用对偶的方式,把首尾各两个字符组合成一个点,这样考虑最多情况也只有26*26种点,而边则依据字符串的长度来创建。
  • 同时当mid==0时可以做一层优化
    • 相当于减去零,环剩余边权之和如果仍不是正数,则说明一定没有正环
#include<bits/stdc++.h>

using namespace std;
const int N=700,M=1e5+10;
int h[N],e[M],ne[M],w[M],idx;
int n;
int st[N],cnt[N];
double d[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);
  memset(st,0,sizeof st);
  
	int q[N];
  int hh=0,tt=0;
  for(int i=0;i<26*26;i++){
    q[tt++]=i;
    st[i]=1;
  }
  int count=0;
  while(hh!=tt){
    int t=q[hh++];
    if(hh==N) hh=0;
    st[t]=0;
    
    for(int i=h[t];i!=-1;i=ne[i]){
      int j=e[i];
      if(d[j]<d[t]+w[i]-mid){
        d[j]=d[t]+w[i]-mid;
        cnt[j]=cnt[t]+1;
    		if(cnt[j]>=N) return true;
        if(++count > 10000) return true;
        if(!st[j]){
          q[tt++]=j;
          if(tt==N) tt=0;
          st[j]=1;
        }
      }
    }
  }
  return false;
}

signed main(){
  while(cin>>n,n){
    memset(h,-1,sizeof h);
    idx=0;
    for(int i=0;i<n;i++){
      string s;
      cin>>s;
      int len=s.length();
      int a,b;
      if(len>=2){
        a=(s[0]-'a')*26+(s[1]-'a');
        b=(s[len-2]-'a')*26+(s[len-1]-'a');
        add(a,b,len); 
      }

    }
    if(!check(0)) puts("No solution");
    else{
      double l=1,r=1001;
      while(r-l>1e-4){
        double mid=(l+r)/2;
        if(check(mid)) l=mid;
        else r=mid;
      }
      printf("%lf\n",r);
    }
  }
  
  
  return 0;
}
  • 16
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值