文章目录
前言
有一天,我正在做这道题目,做法就不讲了,在判负环的时候(当然这道题目0环也要判,后面再讲),我引发除了对SPFA这个队列优化的经过的边数的深入疑惑。
由于队列优化中已经插入的点不再插入,所以,这个会导致队列中的点的层数无法呈现阶段性增长,可能会导致判负环的时候经过的边数出一点问题,但在今天的思考中,我终于是证明不会有影响了。
首先讲讲如何判负环(有向图)吧,因为上篇浅谈SPFA中讲到,一个正常的SPFA路径最多只有 n − 1 n-1 n−1条边,超过就是存在负环了(经过一个点两次以上)。那么只需要记录一个 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的值):
-
重复修正
这里讲一下我所说的修正的定义,对于 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](depx′≤depx),并加入了队列,那么就称 x x x被修正,但是一个 x x x有可能被重复修正多次(想想就知道)。 -
不正确性
在 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]不一定是最小的,但是最后一次一定是。 -
无法修正
如果深度小于等于 d e p dep dep的点在队列中完全消失了,那么意味着现在小于等于 d e p + 1 dep+1 dep+1的 d i s dis dis都是最小的。 -
队列中最小的深度是 d e p dep dep,那么接下来不会再有 d e p dep dep深度的点插入进来
-
将 x x x每次刚插入时的 d e p dep dep记录下来,不会有两个 d e p dep dep是相同的。
当然,上面的性质只是让你在用 d e p dep dep的时候更加的灵活罢了。