spfa 判断负环正环

什么是SPFA:

SPFA是在Bellman-Ford的基础上进行的一种优化,Bellman-Ford的思路是进行n-1次循环,每次循环都遍历每一条边来更新最短路,复杂度是 O ( n ∗ m ) O(n*m)O(n∗m),不难发现每次遍历边(u-v)去更新dis[v]时,当且仅当dis[u]被更新过后才会更新dis[v],所以我们可以用个队列,存下来被修改过的点的id,挨个去更新,为了防止进一步优化,我们开一个vis数组,初始化都是0,在队列内则vis赋1,只有当dis能更短,且v不在队列内时才塞入,这样的思路就是SPFA
他与迪杰斯特拉的区别是,迪杰斯特拉不处理负权边,所以是个贪心,每个点只会更新一次dis,而SPFA会处理负权边,每个点可能因此更新很多次

spfa 算法(队列优化的Bellman-Ford算法) 
时间复杂度 平均情况下 O(m),最坏情况下 O(nm), n表示点数,m 表示边数

板子

int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

SPFA如何判负环

Bellman-Ford判环的思路很简单,再跑完n-1次循环后再跑一次,看有没有点能继续更新最短路,有的话就说明存在负环

SPFA和它的道理是一样的,都是图上任意两个点直接的最短路的长度绝对会 < n,所以我们只需要维护一个cnt数组,表示起点到i的最短路的长度,当任意一个cnt[i] >= n时,说明出现了负环

注意,只判环时,dis数组无需初始化

板子

struct Edge{
	int to,cost;
};
vector<Edge>G[N];
int d[N],cnt[N];
bool vis[N];
bool spfa()
{
  memset(d,0,sizeof(d)); //不用初始化
  memset(vis,false,sizeof(vis));
  memset(cnt,0,sizeof(cnt));
   queue<int>q;
   for(int i=1;i<=n;i++)
   {
	q.push(i);
	vis[i]=true;
   }
   while(!q.empty())
   {
	int now=q.front();
	q.pop();
	vis[now]=false;
    for(int i=0;i<G[now].size();i++)
	{
		Edge e=G[now][i];
		if(d[e.to]>d[now]+e.cost)
		{
			d[e.to]=d[now]+e.cost;
			cnt[e.to]=cnt[now]+1;
			if(cnt[e.to]>=n)return true;
			if(!vis[e.to])
			{
				q.push(e.to);
				vis[e.to]=true;
			}
		}
	}
   }
   return false;
}

SPFA如何判正环

和判负环一样,只需要在更新最短路时改成更新最长路即可(将‘>’改成‘<’即可),同样不需要初始化dis数组,只有在题目需要的时候再更新

例题:POJ - 1860

题目描述:
城市有n个货币兑换点,每个货币兑换点只针对两种货币的互相兑换,每种兑换都有一个独有的汇率和佣金,假设你手上有x个货币a,在汇率为p,佣金为c的情况下可以换成货币b,则可以得到 (x-c) * p的货币b,现在你仅有一个初始货币s,数量是v,你可以通过无数次兑换,问你能不能通过货币兑换的方式来盈利

思路:
如题目所描述的进行建图,判断图中有没有能包含源点s的正环

使用SPFA来解决
 

#include<iostream>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
#define endl '\n'
const int N=110;
int n,m,s;
double v;
struct Edge{
	int to;double p,c;
};
vector<Edge>G[N];
int cnt[N];double d[N];
bool vis[N];
bool spfa(int s,double v)
{
	memset(cnt,0,sizeof(cnt));
	memset(vis,false,sizeof(vis));
	memset(d,0,sizeof(d)); //需要初始化
	queue<int>q;
	q.push(s);
	d[s]=v;
	vis[s]=true;
	while(!q.empty())
	{
		int now=q.front();
		q.pop();
		vis[now]=false;
		for(int i=0;i<G[now].size();i++)
		{
			Edge e=G[now][i];
			if(d[e.to]<(d[now]-e.c)*e.p)//最长路判断正环
			{
				d[e.to]=(d[now]-e.c)*e.p;
				cnt[e.to]=cnt[now]+1;
				if(cnt[e.to]>=n)return true;
				if(!vis[e.to])
				{
					vis[e.to]=true;
					q.push(e.to);
				}
			}
		}
	}
	return false;
}
int main() {
	ios::sync_with_stdio(false);
	cout.tie(0);
	while(cin>>n>>m>>s>>v)
	{
		for(int i=1;i<=n;i++)G[i].clear();
		while(m--){
		int a,b;
		double pab,cab,pba,cba;
		cin>>a>>b>>pab>>cab>>pba>>cba;
		Edge e1={b,pab,cab};
		G[a].push_back(e1);
		Edge e2={a,pba,cba};
		G[b].push_back(e2);
		}
	  if(spfa(s,v))puts("YES");
	  else puts("NO");

	}
	return 0;
}



D-Link with Game Glitch_"蔚来杯"2022牛客暑期多校训练营2 (nowcoder.com)

题意:
用 a 个 b  物品可以生产 c  个 d 物品,询问最大的 w ,用 a  个 b 物品可以生产 w*c  个 d 物品,物品不能无限生产。

分析:

用spfa找负环即可。对于每一个转化我们存转化率,只有转化率<1才有不会无限制造,我们存转化率,再二分答案,通过找最长路我们可以写出 dist[j] < dist[t] * w[i] * mid的条件可以转化,判断有无环即可。也就是说找到找到一个最大的mid使得不存在一个乘积>1的环。但是乘法可能会爆炸,我们直接取log,问题就变成了找负环,有负环存在说明数量会减少,check返回true。

 由图像可以看出log(d1​d2​...dn​×w^n),当括号里的数小于1时,值为负数

不用log,直接乘只能过部分样例

 AC代码:

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'

const int N=1010,M=2010;
int n,m;
const double eps=1e-8;
struct Edge{
	int to;double cost;
};
vector<Edge>G[N];
int cnt[N];double d[N];
bool vis[N];
bool check(double mid)
{
    double k=log(mid);
	queue<int>q;
	for(int i=0;i<=n;i++)
	{
		q.push(i);
		vis[i]=true;
		cnt[i]=0;d[i]=0;
	}
	while(!q.empty())
	{
		int now=q.front();
		q.pop();
		vis[now]=false;
		for(int i=0;i<G[now].size();i++)
		{
			Edge e=G[now][i];
			if(d[e.to]<d[now]+e.cost+k)
			{
				d[e.to]=d[now]+e.cost+k;
				cnt[e.to]=cnt[now]+1;
				if(cnt[e.to]>=n)return true;
				if(!vis[e.to])
				{
					vis[e.to]=true;
					q.push(e.to);
				}
			}
		}
	}
	return false;
}
int main() {
	ios::sync_with_stdio(false);
	cout.tie(0);
	cin>>n>>m;
	while(m--)
	{
		int a,b,c,d;
		cin>>a>>b>>c>>d;
		G[b].push_back({d,log(1.0*c/a)});
	}
	double l=0,r=1;
	while(r-l>eps)
	{
		double mid=(l+r)/2;
		if(check(mid))r=mid;
		else l=mid;
	}
	printf("%lf\n",r);
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值