什么是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(d1d2...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;
}