找负环
WARNING
朴素SPFA的时间复杂度为\(ke\),\(k\)在极端情况下会趋近于\(n\),接近于Bellman-Ford算法。这种极端情况并不罕见,在近几年的竞赛中这种恶意数据很多,如NOI2018
的T1,SPFA死掉了。
所以,求最短路时,如果图没有负边权的话,建议使用队列优化的Dijkstra。
BFS_SPFA
之前讲朴素SPFA时谈到过,判断负环的方式是记录每个点入队的次数,如果大于N就说明有负环,但这种方法会很慢,考虑图就是一个大环(负),你需要绕n圈才能使得有点入队N次,时间复杂度就是\(NE\)了。
考虑另一种方法:对于一个不存在负环的图,从起点到任意一个点最短距离经过的点最多只有 n 个,这样的话,用 cnt[i]
表示从起点(假设就是1)到i
的最短距离包含点的个数,初始化cnt[1]=1
,那么当我们能够用点u
松弛点v
时,松弛时同时更新cnt[v]=cnt[u]+1
,若发现此时cnt[v]>n
,那么就存在负环。
可以这样理解,cnt[i]
代表当前结点i
在当前路径上的深度,因此每走一步深度就要加1,一旦超过N
,就发现了负环。考虑上面的例子,图是一个大环(负),你只需要绕1圈就可以发现负环,效率很高。
//朴素BFS的SPFA,使用了STL的queue,AC
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn=2020, maxm=3020, INF=0x3f3f3f3f;
struct edge{ int t, w, nxt; }E[maxm<<2];
int T, N, M, h[maxn], tot;
void add(int u, int v, int w){ E[++tot].t=v; E[tot].w=w; E[tot].nxt=h[u]; h[u]=tot; }
int read()
{
int s=0, w=1; char ch=getchar();
while(ch<'0' || ch>'9') { if(ch=='-') w=-1; ch=getchar(); }
while(ch>='0'&& ch<='9'){ s=s*10+ch-'0'; ch=getchar(); }
return s*w;
}
int inq[maxn], cnt[maxn], dis[maxn]; //inq标记点在不在queue中,cnt统计点入队的次数
bool SPFA(int s)
{
queue<int> q;
for(int i=1; i<=N; i++) dis[i]=(i==s)?0:INF;
memset(inq, 0, sizeof(inq));
memset(cnt, 0, sizeof(cnt));
q.push(s), inq[s]=1;
while(!q.empty())
{
int x=q.front(); q.pop(); inq[x]=false;
for(int p=h[x]; p; p=E[p].nxt)
{
int to=E[p].t, w=E[p].w;
if(dis[x]+w<dis[to])
{
dis[to]=dis[x]+w;
//判负环方法1: 593ms
cnt[to]=cnt[x]+1;
if(cnt[to]>N) return true;
//判负环方法2:if(++cnt[to]>=N) return true; 786ms
if(!inq[to])
{
q.push(to);
inq[to]=1;
}
}
}
}
return false;
}
int main()
{
T=read();
while(T--)
{
memset(h, 0, sizeof(h)); //清空链式前向星
tot=0;
N=read(), M=read(); //读入图的数据
for(int i=1, a, b, w; i<=M; i++)
{
a=read(), b=read(), w=read();
if(w<0) add(a, b, w); //看清题目要求
else add(a, b, w), add(b, a, w);
}
if(SPFA(1)) printf("YE5\n"); //SPFA判环
else printf("N0\n"); //注意输出的是YE5和N0,不是YES和NO
}
return 0;
}
//BFS的SPFA+SLF优化,使用了STL的deque,效率更高,但9号点莫名TLE,91分,目前原因不明。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn=2020, maxm=3020, INF=0x3f3f3f3f;
struct edge{ int t, w, nxt; }E[maxm<<1];
int T, N, M, h[maxn], tot;
void add(int u, int v, int w){ E[++tot].t=v; E[tot].w=w; E[tot].nxt=h[u]; h[u]=tot; }
int read()
{
int s=0, w=1; char ch=getchar();
while(ch<'0' || ch>'9') { if(ch=='-') w=-1; ch=getchar(); }
while(ch>='0'&& ch<='9'){ s=s*10+ch-'0'; ch=getchar(); }
return s*w;
}
int inq[maxn], cnt[maxn], dis[maxn]; //inq标记点在不在queue中,cnt统计点入队的次数
bool SPFA(int s)
{
for(int i=1; i<=N; i++) dis[i]=(i==s) ? 0 : INF;
deque<int> Q; //Q为双端队列
memset(inq, 0, sizeof(inq));
memset(cnt, 0, sizeof(cnt));
Q.push_back(s), inq[s]=1, cnt[s]=1;
while(!Q.empty())
{
int x=Q.front(); Q.pop_front(); inq[x]=false;
for(int p=h[x]; p; p=E[p].nxt){
int to=E[p].t, w=E[p].w;
if(dis[to]>dis[x]+w)
{
dis[to]=dis[x]+w;
cnt[to]=cnt[x]+1;
if(cnt[to]>N) return true;
if(!inq[to])
{
inq[to]=1;
if(Q.empty() || dis[to]>dis[Q.front()]) Q.push_back(to);
else Q.push_front(to);
}
}
}
}
return false;
}
int main()
{
T=read();
while(T--)
{
memset(h, 0, sizeof(h)); //清空链式前向星
tot=0;
N=read(), M=read(); //读入图的数据
for(int i=1, a, b, w; i<=M; i++)
{
a=read(), b=read(), w=read();
if(w<0) add(a, b, w); //看清题目要求
else add(a, b, w), add(b, a, w);
}
if(SPFA(1)) printf("YE5\n"); //SPFA判环
else printf("N0\n"); //注意输出的是YE5和N0,不是YES和NO
}
return 0;
}
DFS_SPFA
基于 dfs 版的 SPFA 相当于是把“先进先出”的队列换成了“先进后出”的栈,每次都以刚刚松弛过的点来松弛其他的点,如果能够松弛点 x 并且 x 还在栈中,那图中就有负环。
一般来说的话,若存在负环,那么 dfs 会比 bfs 快,但是如果不存在负环,dfs 可能会严重影响求最短路的效率,要谨慎使用
//DFS的SPFA,效率更高,但9号点莫名TLE,91分,目前原因不明,好多飞快的AC代码9号点都是打表过的。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=2020, maxm=3020, INF=0x3f3f3f3f;
struct edge{ int t, w, nxt; }E[maxm<<1];
int T, N, M, h[maxn], tot;
void add(int u, int v, int w){ E[++tot].t=v; E[tot].w=w; E[tot].nxt=h[u]; h[u]=tot; }
int read()
{
int s=0, w=1; char ch=getchar();
while(ch<'0' || ch>'9') { if(ch=='-') w=-1; ch=getchar(); }
while(ch>='0'&& ch<='9'){ s=s*10+ch-'0'; ch=getchar(); }
return s*w;
}
int instack[maxn], dis[maxn], flag; //instack标记点是否在当前的递归栈中,flags为是否有负环的标记
void SPFA(int x)
{
instack[x]=1;
for(int p=h[x]; p; p=E[p].nxt)
{
if(flag) return; //已经找到负环,停止继续搜索
int to=E[p].t, w=E[p].w;
if(dis[to]>dis[x]+w)
{
if(instack[to]) //to能被松弛且在递归栈中,图中有负环
{
flag=true;
return;
}
dis[to]=dis[x]+w;
SPFA(to);
}
}
instack[x]=0;
}
int main()
{
T=read();
while(T--)
{
memset(h, 0, sizeof(h)); //清空链式前向星
tot=0;
flag=0;
memset(instack, 0, sizeof(instack));
memset(dis, 0x3f, sizeof(dis));
dis[1]=0;
N=read(), M=read(); //读入图的数据
for(int i=1, a, b, w; i<=M; i++)
{
a=read(), b=read(), w=read();
if(w<0) add(a, b, w); //看清题目要求
else add(a, b, w), add(b, a, w);
}
SPFA(1);
if(flag) printf("YE5\n"); //SPFA判环
else printf("N0\n"); //注意输出的是YE5和N0,不是YES和NO
}
return 0;
}