找负环

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
");			//SPFA判环
        else			printf("N0
");				//注意输出的是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
");			//SPFA判环
        else			printf("N0
");				//注意输出的是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
");			//SPFA判环
        else			printf("N0
");				//注意输出的是YE5和N0,不是YES和NO 
    }
    return 0;
}
原文地址:https://www.cnblogs.com/lfyzoi/p/10640695.html