以下所有讨论,都是基于有向无负权回路的图上的。因为这一性质,任何最短路径都不会含有环,所以也不讨论路径中包含环的情形!并且为避免混淆,将“最短路径”称为权值最小的路径,将路径经过的点数-1称为路径的长度。
先列出算法的c语言代码实现,后面将用这段代码来辅助证明。
int n;//从1..n共n个点
int dis[maxn][maxn];//权值邻接矩阵
init();//初始化数据
for(int k=1;k<=n;++k)
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
先用比较形象的语言来简单叙述一遍。
- 以下“每次更新路径”指k=k_i时内层的两重循环,k是每次更新路径采用的“中间点”。
- 每个点k在被用作中间点时,相关路径的最小权值,已经被继承到本次更新的路径里了。其他没有被更新的点想通过k进行连接,就一定包含本次更新的路径。并且后续只保留任意两个端点间权值最小的那条路径进行计算。
- 因此在更新n次后,与所有点的相关路径的最小权值,都已经更新完毕。也就是说,所有的路径都已经考虑并更新过了。
下面是比较符号化的严谨证明。
- 设共有(1..n)共(n)个点,初始化所有长度为1的路径集合为(R),(x)与(y)的当前最小路径权值为(dis[x][y]),这个最小权值对应(R)中的一条路径(x,r_1,r_2,...,y)。
- 现在采用数学归纳法,以(k=1..n)进行(n)次路径扩展,并更新(R和dis[][])。
- 当(k=1)时,(R)中已经包含所有顶点两两连接的路径。选取所有的路径(x,k和k,y),进行连接得到(G{d|d=x,k,y})。(R=Rcup G),更新(dis[][])。
这样,(R)中就包含了:起点与终点之间(不包含起点、终点),仅含点1的所有路径。因为没有负权回路,所以后续更新的多次经过点1的路径都不影响最小权值性质,并且也可以被R中去除路径中回路部分的路径替代(不影响其连接作用且权值更小,以下将不再讨论有回路的路径情况)。 - 假设当(k=1..n)时,(R)中已经包含:起点与终点之间(不包含起点、终点),仅含(1..k-1)的所有路径。令(r_k=k),选取R中的所有起点为(k)的路径(S{d|d=x,r_1,r_2,...r_k}),和所有以(k)为终点的路径(T{d|d=r_k,r_k+1,...y}),让(S)与(T)中的路径两两连接,得到(G{d|d=x,...,r_k,...,y})。然后令(R=Rcup G),更新(dis[][])。这样,(R)中就已经包含了:起点与终点之间(不包含起点、终点),仅含(1..k)的所有路径。原假设成立。
- 上述做法进行到(k=n)结束,(R)中就已经包含了这个图所有的路径连接可能。而更新(dis[][])的步骤,因为所有的(R)中的路径对应的(dis[i][k])和(dis[k][j])都在之前计算过了,所以实际上每轮就只需计算(dis[i][j]=min{dis[i][j],dis[i][k]+dis[k][j]})。
在做作业的时候遇到这个算法,想起来好像一直在用但并不理解它的正确性,所以尝试证明了一下。正好也作为我写博客的一个开头吧。