SPFA的BFS与DFS实现及判负环问题

Posted gosick

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SPFA的BFS与DFS实现及判负环问题相关的知识,希望对你有一定的参考价值。

经过笔者的多次实践(失败),在此温馨提示:用SPFA判负环时一定要特别小心!

首先SPFA有BFS和DFS两种实现方式,两者的判负环方式也是不同的。

DFS是如果找到一个节点已经在递归栈中则表示出现负环(即你层层向下搜索后又回到了起点,很明显有负环),BFS是用一个数组记录每一个节点的入队次数,如果某个节点入队次数超过n(节点数)次则表示出现负环(根据SPFA的原理,每一个点最多被扩展n次就会出结果的,如果超过了n次算法还没结束,自然是有负环)。看起来是简单,但有以下注意事项:

  1. 如果只是判负环,使用DFS比BFS一般要快得多。
  2. 判断负环时,dis数组应该都设为0,这样可以使扩展节点时优先走负边,减少了很多冗余的运算,提高速度。当然这样dis数组也实际上失去了记录最短路的意义,因此鱼和熊掌,不可兼得!判负环与求最短路不能同时进行,如果既要判负环又要求最短路,须先用dis数组全部置0的SPFA判负环,如果没有负环再重新初始化dis等数组,然后再跑一遍正常的SPFA(下面就有一道例题,笔者被坑的不轻)。
  3. 用DFS判断负环,不能只把一个源点跑一次,而要把1-n每个都作为源点跑一遍SPFA,才能保证结果的正确。
  4. 还有一种比较玄学的判负环方式,就是正常地跑BFS的SPFA,如果扩展了MAXN个节点还没出结果,就判定有负环(MAXN为根据题目规模自拟的常量),原理简单易懂:跑了这么久还没出结果,当然是有负环咯~~NB的是经实测正确率还相当高!当然相当高还是牺牲了算法的正确性的,因此不到万不得已之时不建议使用(玄学你懂的)。

就只贴上DFS判负环的代码了(部分):

 1 void SPFA_DFS(int h)
 2  2 {
 3  3     if(flag) return ;
 4  4     register int p,v,w;//p为当前边,v为终点,w为边权
 5  5     vis[h]=true;
 6  6     for(p=tail[h];p;p=e[p].last)
 7  7     {
 8  8         v=e[p].v,w=e[p].w;
 9  9         if(dis[v]>dis[h]+w)
10 10         {
11 11             if(vis[v]){flag=true;return ;}//如果此节点已在递归栈中,则有负环
12 12             dis[v]=dis[h]+w;
13 13             SPFA_DFS(v);
14 14             if(flag) return ;
15 15         }    
16 16     }
17 17     vis[h]=false;//这一步不要忘了,相当于回溯
18 18 }
19 19 int main()
20 20 {   
21     memset(dis,0,sizeof dis);//全部置为0
22 21   for(register int i=1;i<=n;++i)
23 22   {
24 23 `    SPFA_DFS(i);//一定要把每个点都跑一次
25 24      if(flag) break;//flag代表有无负环
26 25   }
27 26   flag?printf("YES"):printf("NO");
28 27   return 0;
29 28 }

来一道很模板的例题:https://loj.ac/problem/10086

 技术分享图片

这怎么可以忍???从未见过如此猖狂的出题人!此题的特点是先判负环,如无负环求单源最短路。一开始我是小看这道题了,想用一次SPFA_DFS解决问题,结果有一个测试点负环没判到,还有一个点T了(说好的不必未超时担心呢=-=)。果然鱼和熊掌不可兼得,修改策略:先用SPFA_DFS判负环,如果没有再用正常的SPFA_BFS求最短路,成功AC,代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#define INF 0x7ffffffffffffff
using namespace std;
const int MAXN=1000+5,MAXM=100000+5;
struct edge{
    int v,w,last;
}e[MAXM];
int n,m,s,tot,tail[MAXN];
long long dis[MAXN];
bool flag,vis[MAXN];
inline void add(int x,int y,int ww)
{
    tot++;
    e[tot]=(edge){y,ww,tail[x]};
    tail[x]=tot;
}
void DFS(int h)
{
    if(flag) return ;
    register int p,v,w;
    vis[h]=true;
    for(p=tail[h];p;p=e[p].last)
    {
        v=e[p].v,w=e[p].w;
        if(dis[v]>dis[h]+w)
        {
            if(vis[v]){flag=true;return ;}
            dis[v]=dis[h]+w;
            DFS(v);
            if(flag) return ;
        }    
    }
    vis[h]=false;
}
void SPFA(int s)
{
    queue<int> q; 
    register int h,p,v,w;
    vis[s]=true;
    q.push(s);
    do
    {
        h=q.front();q.pop();vis[h]=false;
        for(p=tail[h];p;p=e[p].last)
        {
            v=e[p].v,w=e[p].w;
            if(dis[v]>dis[h]+w)
            {
                dis[v]=dis[h]+w;
                if(!vis[v])
                {
                    q.push(v);vis[v]=true;
                }
            }    
        }
    }while(!q.empty());
}
int main()
{
    register int i,x,y,z;
    scanf("%d%d%d",&n,&m,&s);
    for(i=1;i<=m;++i)
    {
        scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);    
    }
    for(i=1;i<=n;++i) DFS(i);
    if(flag){printf("-1");return 0;}
    
    fill(dis+1,dis+n+1,INF);dis[s]=0;
    memset(vis,0,sizeof vis);
    SPFA(s);
    for(i=1;i<=n;++i){
        if(dis[i]==INF)
            printf("NoPath
");
        else printf("%lld
",dis[i]);
    }
    return 0;
}

要注意vis数组对于DFS和BFS的SPFA意义是不太一样的,判断负环与求最短路时对dis数组的初始化赋值也不一样。

来自某蒟蒻的强烈建议:用DFS判负环,用BFS求最小路!

 

希望能帮到大家,如有错误,敬请指正.

2018-08-18

 

以上是关于SPFA的BFS与DFS实现及判负环问题的主要内容,如果未能解决你的问题,请参考以下文章

洛谷P3385 模板负环 DFS-SPFA 判负环 图论

论判短负环回路算法的存活率

P3385 模板负环

[luogu3385]dfs_spfa判负环模板

Bellman-Ford

BZOJ1486HNOI2009最小圈 分数规划 dfs判负环。