浅谈SPFA判负环(依旧是对某一天思考的记录)以及对于SPFA的一点点扩展

前言

有一天,我正在做这道题目,做法就不讲了,在判负环的时候(当然这道题目0环也要判,后面再讲),我引发除了对SPFA这个队列优化的经过的边数的深入疑惑。

由于队列优化中已经插入的点不再插入,所以,这个会导致队列中的点的层数无法呈现阶段性增长,可能会导致判负环的时候经过的边数出一点问题,但在今天的思考中,我终于是证明不会有影响了。


首先讲讲如何判负环(有向图)吧,因为上篇浅谈SPFA中讲到,一个正常的SPFA路径最多只有 n − 1 n-1 n1条边,超过就是存在负环了(经过一个点两次以上)。那么只需要记录一个 d e p dep dep(最短路经过的边数)即可。

当然,可能会有人有疑惑,为什么一个环的总和是负数,就一定不会断开更新,而是会一直更新下去呢?如果中间存在一个很大的正数呢?

考虑环上的点: a 1 , a 2 , a 3 . . . a k a_1,a_2,a_3...a_k a1,a2,a3...ak,首先这些点肯定会有一个点加入队列(不然就不是联通的了),然后对于 d i s a 1 + b 1 dis_{a_1}+b_1 disa1+b1 b 1 b_1 b1 a 1 a_1 a1 a 2 a_2 a2的边),如果不能更新 a 2 a_2 a2,那么 d i s a 2 < d i s a 1 + b 1 dis_{a_2}<dis_{a_{1}}+b_1 disa2<disa1+b1(此时 a 2 a_2 a2是由外面的点扔进队列的),更新了就是 = = =(此时的 a 2 a_2 a2是由 a 1 a_1 a1扔进队列的),反正只要 a 1 a_1 a1更新完之后, a 2 a_2 a2都必然已经完成了其的任务或者是在队列里面(因为如果 a 2 a_2 a2是由外面的点扔进来的话,可能先于 a 1 a_1 a1更新的),而且因为 d i s a 2 < d i s a 1 + b 1 dis_{a_2}<dis_{a_{1}}+b_1 disa2<disa1+b1 a 1 a1 a1能够更新的点, a 2 a_2 a2也可以更新,然后用 a 2 a_2 a2证明 a 3 a_3 a3,不断证明下去,因为是负环,所以 a n a_n an最终会更新 a 1 a_1 a1(因为 a 1 a_1 a1能够更新 a 1 a_1 a1),然后不断这样更新下去,就断不了了。


回归本源,来讲讲为什么这个乱七八糟的 d e p dep dep为什么不会影响判断。

替代做法

如果你不想看这个做法跳了就行。

在此,给出一个减少了优化程度(但减少了修正的操作,本身也加大了一点优化程度,但是减少的应该更多吧),但是能更加直观的看到正确性的做法。

上次我们讲到其实存在了就不插入,本身是用前面代替后面,并等待修正,但是如果我们用后面代替前面的,如果已知后面存在新的 d i s x dis_x disx,就不找这个点了,这样 l i s t list list里面的 d e p dep dep是呈现阶段性的,而且由于最多存在两种不同的 d e p dep dep(因为阶段性),所以最多有两个 x x x在队列中(而且可以保证当 x x x被取出队列时,当前的 d i s x dis_x disx对于 d e p x dep_x depx而言绝对是最小的)。

这样的话其实真的就是普通优化的表面否的算法了,但是更加的直观,不是吗?

#include<cstdio>
#include<cstring>
#define  N  2100
#define  NN  4100
#define  M  6100
using  namespace  std;
int  n,nn,m;
struct  STD_EDGE
{
    
    
	int  x,y,c;
}st[M];
struct  node
{
    
    
	int  y,next;double  c;
}a[M];int  len,last[N];
inline  void  ins(int  x,int  y,double  c){
    
    len++;a[len].y=y;a[len].c=c;a[len].next=last[x];last[x]=len;}

int  list[NN],head,tail,dep[N],v[N];
double  dis[N];
bool  check(double  val)
{
    
    
	len=0;memset(last,0,sizeof(last));
	for(int  i=1;i<=m;i++)ins(st[i].x,st[i].y,st[i].c*val);
	for(int  i=1;i<=n;i++)ins(st[i+m].x,st[i+m].y,-st[i+m].c);
	for(int  i=nn;i>=1;i--)dep[i]=0,dis[i]=999999999.0,v[i]=0;
	head=1;tail=2;list[1]=1;v[1]=1;dis[1]=0;
	//重点
	while(head!=tail)
	{
    
    
		int  x=list[head++];if(head==2*nn+1)head=1;
		if(dep[x]>=nn)return  1;
		v[x]--;if(v[x])continue;//后面存在一个x可以更新的更快 
		for(int  k=last[x];k;k=a[k].next)
		{
    
    
			int  y=a[k].y;
			if(dis[x]+a[k].c<=dis[y])
			{
    
    
				dis[y]=dis[x]+a[k].c;
				if(dep[x]>=dep[y])
				{
    
    
					dep[y]=dep[x]+1;v[y]++;
					list[tail++]=y;if(tail==2*nn+1)tail=1;
				}
			}
		}
	}
	//
	return  0;
}
int  main()
{
    
    
	scanf("%d%d",&n,&m);nn=n<<1;
	for(int  i=1;i<=n;i++)
	{
    
    
		int  x;scanf("%d",&x);
		st[m+i].x=i*2-1;st[m+i].y=i<<1;st[m+i].c=x;
	}
	for(int  i=1;i<=m;i++){
    
    scanf("%d%d%d",&st[i].x,&st[i].y,&st[i].c);st[i].x*=2;st[i].y=st[i].y*2-1;}
	double  l=1e-2,r=20,mid,ans;
	while(r-l>=1e-4)
	{
    
    
		mid=(l+r)/2;
		if(check(mid)==1)ans=mid,l=mid;
		else  r=mid;
	}
	printf("%.2lf\n",ans);
	return  0;
}

当然你直接用表面否的也是没有问题的啦。

探究正常做法的正确性

仔细的思考,我们其实可以发现,我们不能把目光放到 d e p dep dep上面,而是放到更新上面,如果存在一条路径: x − > . . . − > x x->...->x x>...>x,不管其是不是对的,都至少说明存在一个环可以重复更新 x x x,那么就是存在环的。

证毕。

当然,对于 d e p dep dep的记录,你需要明白:
对于相同长度的路径而言,我们可以记录任意一个 d e p dep dep,反正只要是最短路就可以了,不难发现,不管是存在还是不存在负环,记录任何的 d e p dep dep都不会影响判断。

如果要判断 0 0 0环只需要把更新条件从 < < <改成 ≤ ≤ 即可,证法不变(如果你还是不能理解,我们对于每条边加一个无限接近于0的负数,作为偏移量,这样就是求负环了吗,而且作用等价于 ≤ ≤ )。

#include<cstdio>
#include<cstring>
#define  N  2100
#define  NN  4100
#define  M  6100
using  namespace  std;
int  n,nn,m;
struct  STD_EDGE
{
    
    
	int  x,y,c;
}st[M];
struct  node
{
    
    
	int  y,next;double  c;
}a[M];int  len,last[N];
inline  void  ins(int  x,int  y,double  c){
    
    len++;a[len].y=y;a[len].c=c;a[len].next=last[x];last[x]=len;}

int  list[NN],head,tail,dep[N],v[N];
double  dis[N];
bool  check(double  val)
{
    
    
	len=0;memset(last,0,sizeof(last));
	for(int  i=1;i<=m;i++)ins(st[i].x,st[i].y,st[i].c*val);
	for(int  i=1;i<=n;i++)ins(st[i+m].x,st[i+m].y,-st[i+m].c);
	for(int  i=nn;i>=1;i--)dep[i]=0,dis[i]=999999999.0,v[i]=0;
	head=1;tail=2;list[1]=1;v[1]=1;dis[1]=0;
	//重点
	while(head!=tail)
	{
    
    
		int  x=list[head++];if(head==nn+1)head=1;
		if(dep[x]>=nn)return  1;
		v[x]=0;
		for(int  k=last[x];k;k=a[k].next)
		{
    
    
			int  y=a[k].y;
			if(dis[x]+a[k].c<=dis[y])
			{
    
    
				dis[y]=dis[x]+a[k].c;
				dep[y]=dep[x]+1;
				if(!v[y])
				{
    
    
					v[y]=1;
					list[tail++]=y;if(tail==nn+1)tail=1;
				}
			}
		}
	}
	//
	return  0;
}
int  main()
{
    
    
	scanf("%d%d",&n,&m);nn=n<<1;
	for(int  i=1;i<=n;i++)
	{
    
    
		int  x;scanf("%d",&x);
		st[m+i].x=i*2-1;st[m+i].y=i<<1;st[m+i].c=x;
	}
	for(int  i=1;i<=m;i++){
    
    scanf("%d%d%d",&st[i].x,&st[i].y,&st[i].c);st[i].x*=2;st[i].y=st[i].y*2-1;}
	double  l=1e-2,r=20,mid,ans;
	while(r-l>=1e-4)
	{
    
    
		mid=(l+r)/2;
		if(check(mid)==1)ans=mid,l=mid;
		else  r=mid;
	}
	printf("%.2lf\n",ans);
	return  0;
}

扩展

假如我们按照常规的SPFA做,然后在更新的时候同时更新 d e p dep dep(表示最短路径经过的边数)那么会有以下性质(接下来的 d i s [ d e p x ] [ x ] dis[dep_x][x] dis[depx][x]表示经过 d e p x dep_x depx条边时 d i s x dis_x disx的值):

  1. 重复修正
    这里讲一下我所说的修正的定义,对于 d i s [ d e p x ] [ x ] dis[dep_x][x] dis[depx][x],如果未来出现了 d i s [ d e p x ‘ ] [ x ] < d i s [ d e p x ] [ x ] ( d e p x ′ ≤ d e p x ) dis[dep_{x‘}][x]<dis[dep_x][x](dep_{x'}≤dep_x) dis[depx][x]dis[depx][x](depxdepx),并加入了队列,那么就称 x x x被修正,但是一个 x x x有可能被重复修正多次(想想就知道)。

  2. 不正确性
    在这里插入图片描述
    x x x的时候出现了混乱,在上篇文章中讲到,这样子正确性并没有变,因为可以由后面的 d e p dep dep深度的点进行修正,重新产生 x x x,也就是说,第一次产生的 d i s [ d e p x ] [ x ] dis[dep_x][x] dis[depx][x]不一定是最小的,但是最后一次一定是。

  3. 无法修正
    如果深度小于等于 d e p dep dep的点在队列中完全消失了,那么意味着现在小于等于 d e p + 1 dep+1 dep+1 d i s dis dis都是最小的。

  4. 队列中最小的深度是 d e p dep dep,那么接下来不会再有 d e p dep dep深度的点插入进来

  5. x x x每次刚插入时的 d e p dep dep记录下来,不会有两个 d e p dep dep是相同的。

当然,上面的性质只是让你在用 d e p dep dep的时候更加的灵活罢了。

猜你喜欢

转载自blog.csdn.net/zhangjianjunab/article/details/108653719