SPFA
一、对于spfa本身的理解
在判环之前,要先简单了解一下SPFA算法
(以下的内容部分摘自 https://blog.csdn.net/forever_dreams/article/details/81161527 和 百度)
-
SPFA 算法是 Bellman-Ford算法 的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环。SPFA 最坏情况下复杂度和朴素 Bellman-Ford 相同,为 O(VE)。
-
松弛操作,它的原理是著名的定理:“三角形两边之和大于第三边”,也可以说是三角不等式。若用 u 来松弛 v,就是判断是否 dis[ v ] > dis[ u ] + w[ u , v ],如果该式成立则将 dis[ v ] 更新到 dis[ u ] + w[ u , v ],否则不变。
-
SPFA相当于是Bellman-Ford算法的一个优化版本,在Bellman-Ford算法中,很多松弛操作其实都是没有必要的,例如:对于一条从 x 到 y 的边,如果连 x 都还没被松弛,那 y 肯定也还不能被 x 松弛,为了避免“用一个还没有被松弛的点去松弛另外的点”的情况,我们用一个队列来存储已经被松弛过的点,然后用队列里的点去松弛其他点,这就是SPFA算法的基本思想
-
下面是具体的操作过程:
我们先把起点加入队列中,每次取出队首元素 u,然后尝试松弛点 u 能到达的点 v,若点 v 能被松弛且点 v 此时还没有被加进队列里,那么我们就把点 v 入队,不断地这样操作,直到队列为空。这样的话,只要最短路存在,那么我们就一定能求出最短路,与dijkstra不同的是spfa是基于遍历边的思想,只要可以被更新,可能一条边会被遍历很多次,从而一个点也可能会进队和出队很多次。
但是,SPFA有时会被恶意数据卡掉,为了避免最坏情况的出现,在正权图上应使用效率更高的Dijkstra算法。
二、spfa判负环
首先理解一下,负环:一个权值和为负数的环
<思路>
1.上图是一个有向的负权图,由第一部分我们可以知道,spfa是基于遍历边的思想,也就是说只要这一条边可以被更新,那么这条边上的点就会进入队列。
2.其次,要跑最短路,一定是找到最小的边权值,然后就不断更新,直到找到最小的答案。但是,在一个负权图中,只要跑到负权边,那么这个答案就会被不断的更新,也就是说,负权边会被不断地更新,负权边上的点就会不断的进入队列。
3.对于一个正常的正权图而言,每个点最多被更新n-1次(最坏情况,每个点都可以把它更新)
,如果说是,有一个点被更新大于等于n次的话,也就是说,这里有负环。
<代码>
题目:P3385 【模板】负环
【bfs版】
#include<bits/stdc++.h>
using namespace std;
const int nn=2010;
const int mm=3010;
int t,n,m;
int num=0;
int d[nn],vis[nn],v[nn],r[nn];
int read()
{
int x=0,f=1; char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0'; ch=getchar();}
return x*f;
}
struct edge
{
int fr,to,ver,nex;
}e[2*mm];
int last[nn];
void add(int x,int y,int z)
{
e[++num].fr=x; e[num].to=y; e[num].ver=z;
e[num].nex=last[x]; last[x]=num;
}
bool spfa(int xx)
{
queue<int> q;
memset(d,0x3f3f3f3f,sizeof(d));
memset(vis,0,sizeof(vis));
d[xx]=0,vis[xx]=1;
q.push(xx);
while(q.size())
{
int x=q.front(); q.pop();
vis[x]=0;
for(int i=last[x];i;i=e[i].nex)
{
int y=e[i].to;
int z=e[i].ver;
v[y]=1;
if(d[y]>d[x]+e[i].ver)
{
d[y]=d[x]+e[i].ver;
r[y]=r[x]+1;//r[i]数组保存的是i节点被更新的次数
if(!vis[y])
vis[y]=1,q.push(y);
}
if(r[y]>=n) return true;
}
}
return false;
}
int main()
{
t=read();
while(t)
{
t--;
n=read(); m=read();
num=0;
memset(e,0,sizeof(e));
memset(last,0,sizeof(last));
memset(v,0,sizeof(v));
memset(r,0,sizeof(r));
for(int i=1;i<=m;++i)
{
int a=read();
int b=read();
int c=read();
if(c<0) add(a,b,c);
else
{
add(a,b,c);
add(b,a,c);
}
}
for(int i=1;i<=n;++i)
{
if(v[i]) continue;
v[i]=1;
if(spfa(i))
{
cout<<"YE5"<<endl;
break;
}
else
{
cout<<"N0"<<endl;
break;
}
}
}
return 0;
}
听说还有【dfs版】,但是笔者没有打过
【dfs版】详见于:(https://blog.csdn.net/forever_dreams/article/details/81161527 )
基于 dfs 版的 SPFA 相当于是把"先进先出"的队列换成了"先进后出"的栈
也就是说,每次都以刚刚松弛过的点来松弛其他的点,如果能够松弛点 x 并且 x 还在栈中,那图中就有负环
一般来说的话,若存在负环,那么 dfs 会比 bfs 快
但是如果不存在负环,dfs 可能会严重影响求最短路的效率,要谨慎使用
三、spfa判正环
题目:P1938 [USACO09NOV]找工就业Job Hunt
题目:P1984 [差分约束]奖学金(差分约束+spfa跑最长路+spfa判正环)
题目:P1227关系运算图(差分约束+spfa跑最长路+spfa判正环)
正环:顾名思义,是一个环的边权值和为正数。
思路
思路基本和上面的是一样的,就是三角不等式要换一下,当 dis[ v ] < dis[ u ] + w[ u , v ]的时候更新,然后和上面一样,统计更新过的次数,判断。【正解的简洁明了??】
代码
#include<bits/stdc++.h>
using namespace std;
const int nn=230;
const int mm=500;
int D,p,c,f,s;
int num=0;
int d[nn],vis[nn],r[nn];
int maxx=-100;
queue<int> q;
inline int read()
{
int x=0,f=1; char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0'; ch=getchar();}
return f*x;
}
struct edge
{
int fr,to,ver,nex;
}e[mm];
int last[nn];
void add(int x,int y,int z)
{
e[++num].fr=x; e[num].to=y; e[num].ver=z;
e[num].nex=last[x]; last[x]=num;
}
bool spfa(int xx)
{
memset(vis,0,sizeof(vis));
memset(d,0xcf,sizeof(d));
d[xx]=D;
q.push(xx);
vis[xx]=1;
while(q.size())
{
int x=q.front(); q.pop();
vis[x]=0;
for(int i=last[x];i;i=e[i].nex)
{
int y=e[i].to,z=e[i].ver;
if(d[x]+z>d[y])/*这里这里,要换一下三角不等式*/
{
d[y]=d[x]+z;
maxx=max(maxx,d[y]);
if(!vis[y])
q.push(y),vis[y]=1;
r[y]++;
if(r[y]>c) return true;
}
}
}
return false;
}
void in()//输入和建图
{
D=read(); p=read(); c=read(); f=read(); s=read();
for(int i=1;i<=p;++i)
{
int a=read();
int b=read();
add(a,b,D);
}
for(int i=1;i<=f;++i)
{
int a=read();
int b=read();
int c=read();
add(a,b,-c+D);
}
}
int main()
{
in();
if(spfa(s)) cout<<-1<<endl;
else cout<<maxx<<endl;
return 0;
}
习题:
- 狡猾的商人:考虑一下如何建图,如何建边, 建完之后我们可以用SPFA跑环吗?跑什么环?正环还是负环?
完……