图论——spfa算法判断负权回路

最短路径模板——spfa算法中的模板只适用于不存在负权回路的图,否则就会死循环。
接下来做一下改动,实现通过spfa算法判断是否存在负环。

求负环的常用方法,基于SPFA:

  1. 统计每个点入队的次数,如果某个点入队n次,则说明存在负环;
  2. 统计当前每个点的最短路中所包含的边数即路径长度,如果某点的最短路所包含的边数大于等于n,则说明存在环。

我们下面以方法2为思路解决问题:
   n个点的路径长度最大为n-1,所以当以某个顶点结尾的最短路径长度>=n时,就说明存在回路。

一、那么我们是以哪个顶点开始的最短路径长度呢?

上面求最短路径时的模板是某一个起点s出发到其它所有顶点的最短路径,不一定会遍历到所有的点。如果是个连通图,那就所有顶点都可达,可以全部遍历到,而且如果还不存在负环的话,就都有最短路径;如果是个非连通图,就有些顶点不可达,dis[k]=INF,不能全部遍历到。

当判断是否存在负环时,需要把整个图都遍历到,且没有固定的起点。如果是个连通图,从任意一个点开始都行,最终都能遍历整个图的顶点和边;如果是非连通图,最少需要把每个连通子图的其中一个点都当作起点。
由于是最终每个点都会遍历到,所以可以提前把所有顶点都入队列,作为起点,这样就可以避免判断是不是连通图,有几个连通图的问题了。

以从起点s找到其它所有顶点的最短路径为背景,分析负环的一般走向:假设有负环(i,k1,k2,k3,……,kn,j)
s可达i,则s可达i所在的环的所有顶点,i点是图中的任意位置,dis[i]表示s->i的最短路径权值,那么当其转到kn时,会再次更新i,使新的dis[i]小于原来的值,以此不断循环。

因此可以知道,只要从一个连通图内任一点开始,由于可以到达任意一点,所以就可以判断该连通图中负环是否存在。

当我们只求 是否存在负环 时,dis的意义就不重要了,不需要像求最短路径那样初始化为无穷,然后规定起点为0了,也就是说对dis的初值就没有限制了,
如果不存在负环时,队列早晚会空,最终都会趋于某种状态下的最短路径,虽然不是我们想要的dis答案
但是如果存在负环时,队列就一直不会空,一直在负环上循环,负环存在的路径上的最短路径就会最终是负无穷,所以可以判断负环存在。

判断负环时不需要求出最短路的具体值,只需判断是否更新次数太多。如果存在负环,那么必然存在某些点的最短路长度是负无穷,那么必然会被更新无限次,所以不赋初值也可以。

可以给dis数组全部初始化为0时规定一种含义:

在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值规定为0,长度也规定为0的有向边那么新的图一定是个连通图,那么原图有负环等价于新图有负环,此时把这个虚拟源点作为起点即可(自身到自身距离为0,dis值均为INF),将虚拟源点加入队列中,然后在进行第一次迭代,这时会将dis数组从原来的INF全部更新为0,并将图中所有点插入到队列中。

这是dis数组的含义就是图中所有的点到虚拟源点之间的距离。

二、怎么判断是否存在负环呢?
可以维护一个cnt数组,初始时是把所有点都放入队列,假设都作为起点,所以,所有的cnt[i]=0,cnt为0的点表示是起点,允许存在多个起点的情况。从起点走,每连一个顶点,就多一条边,所以cnt就加1,某个点一旦加1就不是0了,就说明它不是起点了,所以cnt[k]=x记录的就是以k结尾的点最短路径经过的边数为x,起点就是cnt为0的某个点,不过不知道是哪个罢了。
当不存在回路时,n个点之间最多只有n-1条边,所以当cnt[x] >= n时,则表示该图中一定存在负环。

结合前面说的dis初始化为0的含义,可以理解cnt为某个点到虚拟源点之间经过的边数,不过特殊规定:虚拟源点和图中的点直接相连的边,长度为0(即不算边),权值为0。

题目描述

给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你判断图中是否存在负权回路。

输入格式
第一行包含整数n和m。

接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

输出格式
如果图中存在负权回路,则输出“Yes”,否则输出“No”。

数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过10000。

输入样例:

3 3
1 2 -1
2 3 4
3 1 -4

输出样例:

Yes

代码实现

#include <iostream>
#include <cstring>

using namespace std;

const int N=2010,M=1e4+10;
int h[N],e[M],ne[M],w[M],idx;
int dis[N],cnt[N];
bool vis[N];
int q[N*N],hh,tt=-1;   队列的大小不能和顶点数一样,tt是一直加的,要开大点,不然空间不够
int n,m;               或者考虑写成循环队列,tt从0开始,hh==tt时队列为空。

void add(int a,int b,int c)
{
    
    
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

bool spfa()
{
    
    
    //不用初始化dis数组了,默认是0就行
    for (int i=1;i<=n;i++) q[++tt]=i,vis[i]=true; 
    //结点编号从1号开始的
    while (hh<=tt) {
    
    
        int v=q[hh++];
        vis[v]=false;
        for (int i=h[v];i!=-1;i=ne[i]) {
    
    
            int j=e[i];//v->j的边
            if (dis[j] > dis[v]+w[i]) {
    
    
                dis[j]=dis[v]+w[i];
                if (!vis[j]) q[++tt]=j,vis[j]=true;
                cnt[j]=cnt[v]+1;  //v->j,j经过的边数是是v经过的边数+1,注意别用自加,改变cnt[v]的值
                if (cnt[j]>=n) return true;  //发现负环,可以退出。
            }
        }
    }
    return false;//如果存在负环,肯定退出不出while循环,既然能退出while循环,说明不存在负环。
}

int main()
{
    
    
    memset(h,-1,sizeof h);
    scanf("%d%d",&n,&m);
    int a,b,c;
    for (int i=0;i<m;i++) {
    
    
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    spfa()==true?puts("Yes"):puts("No");
      //调用函数的同时使用其返回值
    return 0;
}

Question:
熟练书写数组模拟循环队列,初始化时:hh=tt=0
或者直接用stl容器queue即可。
普通队列是先++,再存数,tt的位置就是存数的最后一个位置;栈也是先++,再存储。
循环队列是先存数,再++,tt的位置就是将要存数的位置。

int q[N],hh,tt;
bool spfa()
{
    
    
    
    for (int i=1;i<=n;i++) {
    
    
        q[tt++]=i,vis[i]=true;
        if(tt==N) tt=0;        一、或者写成取模的形式也行:tt=(tt+1)%N; 下面同理
    } 
  
    while (hh!=tt) {
    
    
        int v=q[hh++];
        vis[v]=false;
        if (hh==N) hh=0;     二、
        
        for (int i=h[v];i!=-1;i=ne[i]) {
    
    
            int j=e[i];//v->j的边
            if (dis[j] > dis[v]+w[i]) {
    
    
                dis[j]=dis[v]+w[i];
                if (!vis[j]) {
    
    
                    q[tt++]=j,vis[j]=true;
                    if(tt==N) tt=0;   三、
                }    
                cnt[j]=cnt[v]+1;  //v->j,j经过的边数是是v经过的边数+1,注意别用自加,改变cnt[v]的值
                if (cnt[j]>=n) return true;
            }
        }
    }
    return false;
}

猜你喜欢

转载自blog.csdn.net/HangHug_L/article/details/113996364
今日推荐