Floyd再思考
——by ThinkofBlank
一.序言
Floyd,是一个十分常用的图论算法,其作用是在O(n^3)的时间内计算出全源最短路。其实现原理是利用的dp,然而,刚开始接触Floyd的时候,并没有去尝试理解,思路此算法,仅仅记了下打法就跑了,最近无聊时思考了下,得出了些有用的东西,特此来分享
注意:以下说的图都是无负环的,不然,求甚么最短路。。。
当然,给出的图是联通且点的个数至少为3
二.引入
1、路径合成可行性
给定一个图,假设我们已知i->j的最短路径为a1,i->k的最短路径为a2,(a1<a2),那么j->k的最短路径是多少?
答案是a2-a1?不!并不是,比如下图:
很明显的,j->k的最短路径应该是a1+a2才对,当然,也有满足最短路径为a1-a2的情况,如下图:
所以,如果我们单纯地知道i->j和i->k的最短路径,我们是无法求出正确的j->k的最短路径的
那么,我们来换一换,假设我们已知i->k的最短路径为a1,k->j的最短路径为a2,那么i->j的最短路径是不是就可求呢?
初步画个图:
由此可以判断出i->j的最短路径是a1+a2,于是就又错啦!
以下给出两个hack的情况:
当然,对于第一个hack的情况,要j和k之间的是双相边
于是,我们真的无法求出来了吗?不!
我们注意到,对于两种hack的情况,其实它都可以看出i->k,k->j的行走方式,只是某些时候,k=i或j罢了!
所以,就有:对于一条路径i->j,它可以由i->k,k->j合成,那么我们只需要合成就好了。这就是路径的合成可行性
感性的理解:由i到j,我们可以先从i到k,再从k到j,如果直接由i到j中途不停留的话,那么相当于此时k=i或者j
那么,这里就有一个方法了!
我们枚举所有的k!这样,我们就可以枚举出所有的路径出来!然后把这些权值取最小,即一定为最小路径!
首先,我们设dis(i,j)表示i->j的最短路径(初始无限大),那么如果有一条边权为w从i->j的边,我们就令dis(i,j)=min(dis(i,j),w)
这样我们就求出了对于所有i->k,k->j中k=i或j的时候的路径了
那么之后怎么求呢?
简单啊!
枚举k嘛,我就老老实实枚举k和i,j就好了嘛
for:i=1->n
for:j=1->n
for:k=1->n
dis(i,j)=min(dis(i,j),dis(i,k)+dis(k,j))
看,这不就好了嘛!
然而,这是错的。。。
为啥?
因为我们回过去看枚举所有k得到最短路径的条件:dis(i,k)与dis(k,j)一定也是最短路径!
但是,我们目前,我们对于部分dis(i,k)和dis(k,j)并没有枚举,所以,我们的答案不一定正确!
那么该如何改进呢?
传统的Floyd是使用松弛操作进行的:
for:k=1->n
for:i=1->n
for:j=1->n
dis(i,j)=min(dis(i,j),dis(i,k)+dis(k,j))
咋看一下没啥不同,但事实上,它是在进行dp,具体的我就不说多了,现在来讨论下Floyd的Ex算法:
先来引入一个定理:
2、一定存在两个点最短路径为它们两个点的直接连边
先者有言:没有证明的理论,都是猜想,不能直接用
那么,我们就来证明一波
我使用的是反证法:
假设所有点两两之间的最短路都间隔了至少经过了一个点
我们设i->j的最短路为i->k->j
那么,不难发现,如果i->k的最短路不是i,k的直接连边,而是i->t->k的话,那么明显有i->j的最短路中
i->t->k->j更优
那么i->j的最短路不止经过一点呢,我们直接假设为i->t->k->j,我们可以同理去分析i->t
所以,这与假设不相符
所以,命题得证。
而且,同理去分析k->j,还会再多得一对点,所以对于任意一对最短路为直接连边的两点A,B
我们一定可以找到另外的一个点C,使得那个点与两点中的其中一个点(假设为B)的直接连边是最小路径
简单分析下便可发现,这样的点对至少有n-1个
那么,如果我们找到所有这样的点对,我们就可以再推出至少n-2条最短路径,然后再用这n-2条路径继续推...
循环n-1次便可以推完所有的最短路径了!
三、算法
那,我们怎么找到这n-1条边呢?对于一个图,我们可以采用玄学的方法——枚举法!
这样,我们就有了一个算法的雏形:
一开始的赋值就不说了。。。
首先,我们将所有边加入对应的表中(使用数组即可~),比如u->v,我们就令s(u,++siz(u))=v,表示,我们有一条从
u->v的确定了权值的边(不一定最小)
然后,我们进行一下操作:
1.枚举i
2.访问与i相连的边,找到一个k
3.访问与k相连的边,找到一个j
4.如果dis(i,k)+dis(k,j)<dis(i,j),我们令dis(i,j)=dis(i,k)+dis(k,j),并把j加入i的表中
5.回到步骤1,重复n-2次即可(第一次是赋值那里)
两个优化:
1.对于每个点来说,我们维护一个bool数组ys(i,j)表示j是否在i的表里面,如果是,我们就无需把j加入表中,否则,我们就将j加入表中,并使ys(i,j)=1
2.对于不在起作用的表,我们将它砍掉,无需再访问(详细见代码的kil数组)
于是我们有了代码:
//#pragma GCC optimize()//手动Ox优化 #include<bits/stdc++.h> #define int long long using namespace std; const int M=1001; int dis[M][M]; int s[M][M],siz[M]; bool kil[M][M]; bool ys[M][M]; inline void Ex_Floyd(int n){ int tim=n-2; while(tim--){ bool flag=0; for(int i=1;i<=n;++i){ for(int j=1;j<=siz[i];++j){ int v=s[i][j]; if(kil[i][j]){ continue; } kil[i][j]=1; for(int k=1;k<=siz[v];++k){ int e=s[v][k]; if(dis[i][e]>dis[i][v]+dis[v][e]){ dis[i][e]=dis[i][v]+dis[v][e]; if(!ys[i][e]){ ys[i][e]=1; s[i][++siz[i]]=e; } flag=1; kil[i][j]=0; } } } } if(!flag){ break; } } } signed main(){ int n,m; scanf("%lld%lld",&n,&m); for(int i=1;i<=n;++i){ for(int j=1;j<=n;++j){ dis[i][j]=1e16; } dis[i][i]=0; } for(int i=1;i<=m;++i){ int u,v,w; scanf("%lld%lld%lld",&u,&v,&w); if(dis[u][v]>w){ if(!ys[u][v]){ ys[u][v]=1; s[u][++siz[u]]=v; } dis[u][v]=w; } } Ex_Floyd(n); return 0; }
我们把它称作Ex_Floyd算法,为什么呢?我们通过一道题来说明:
http://ybt.ssoier.cn:8088/problem_show.php?pid=1421
我们惊奇的发现,传统的Floyd算法竟然无法通过此题,而此算法可以
经过尝试发现,Floyd错的点中,存在负权边
事实证明,传统的Floyd似乎不能跑负权边啊。。。
而此算法可以,故命名为Ex_Floyd(经过一番尝试后,发现,此算法应与Floyd的复杂度接近,不过常数可能会大点)