找出所有点对的最短路径,经典算法是Floyd-Warshall,关于该算法,《算法导论》等书籍给了充分的推理介绍,但均显得过于理论化,阅读起来不容易理解。
以下我以一个例子,详细阐述该算法的解题过程,力图将跳跃性降至最低,所以只阐述算法实现过程,详细证明过程请还是参考书籍。
例图选择《算法导论》的图25-1:
首先要明确,这里的最短路径均为简单路径,即:一条路径最多每个顶点通过一次。所以一条最短路径最多含有n-1条边。
那个希腊字母拼写实在不方便,所以设最短路径表示为Min(i,j)。 w(i,j) 表示边(Vi,Vj)的权
设D(k,i,j) 为从vi到vj只使用v1,v2,...,vk作为中间顶点的最短路径的权。
比如v5到v2
D(1,5,2) = 只能使用{v1}中的点作为中间顶点 : NULL (表示不存在这样的最短路径,权值为空)
D(2,5,2) = 只能使用{v1,v2}中的点作为中间顶点 : NULL
D(3,5,2) = 只能使用{v1, v2, v3}中的点作为中间顶点: NULL
D(4,5,2) = 只能使用{v1, v2, v3, v4}中的点作为中间顶点: 存在三条路径,最短路径的权为 5
v5->v4->v3->v2 5
v5->v4->v1->v2 11
v5->v4->v1->v3->v2 20
D(5,5,2) = 只能使用{v1, v2, v3, v4,v5}中的点作为中间顶点: 与D(4,5,2)一样,存在三条路径,最短路径的权为 5
v5->v4->v3->v2 5
v5->v4->v1->v2 11
v5->v4->v1->v3->v2 20
由D的定义,不难看出,D(0,i,j) = w(i,j) 若不存在边(vi,vj)则 D(0,i,j) = INFINIT
D(|V|,i,j) = Min(i,j) 即vi到vj的最短路径。
对于k>0 vi到vj只能使用{v1,v2,...vk}作为中间顶点的最短路径 Min(i,j) 存在两种情况
1. 使用vk作为中间顶点
即有vi->vk->vj,
Min(i,j) = Min(i,k) + Min(k,j)
D(k,i,j) = D(k,i,k) + D(k,k,j)
由于vi->vj是一条最短路径,所以vi->vk也是一条vi到vk的最短路径(一条定理,反正法很容易证明),
则vi到vk为只能使用{v1,v2,...,v(k-1)}作为中间顶点的最短路径,因为vk已经被用了且最短路径上顶点只能用一次,所以D(k,i,k) = D(k-1,i,k)。
同理
vk到vj为只能使用{v1,v2,...,v(k-1)}作为中间顶点的最短路径,所以D(k,k,j) = D(k-1,k,j)。
即:D(k,i,j) = D(k-1,i,k) + D(k-1,k,j)
因为D(k,i,j) 为vi到vj只能使用{v1,v2,...,vk}作为中间顶点的最短路径Min(i,j)的权值
D(k-1,i,k) 为vi到vk只能使用{v1,v2,...,v(k-1)}作为中间顶点的最短路径Min(i,k)的权值
D(k-1,k,j) 为vk到vj只能使用{v1,v2,...,v(k-1)}作为中间顶点的最短路径Min(k,j)的权值
比如上面的k=4时
Min(5,2) = Min(5,4) + Min(4,2)
v5->v4->v3->v2 = v5->v4 + v4->v3->v2
仿照最开始的分析可以求得
D(3,5,4) = v5->v4, D(3,4,2) = v4->v3->v2
D(4,5,2) = D(3,5,4) + D(3,4,2)
2. 根本不使用vk作为中间顶点
即只能使用{v1,v2,...,v(k-1)}作为中间顶点
则D(k,i,j) = D(k-1,i,j) 因为肯定不用vk为中间顶点了,所以相当于只能使用{v1,v2,...,v(k-1)}作为中间顶点的最短路径。
比如上面的k=5时
Min(5,2) = D(5,5,2) = D(4,5,2)
v5->v4->v3->v2 v5没有出现在中间顶点,而是第一个顶点。
综合上面便可以得到递归式:
D(k,i,j) = min{ D(k-1,i,j), D(k-1,i,k) + D(k-1,k,j) }
按照自底向上计算最短路径权值:
伪码: FloydWarshall(W)
{
n = W.rows;
D(0,i,j) = W;
for k = 1 to n
{// D(k,i,j) = min{ D(k-1,i,j), D(k-1,i,k) + D(k-1,k,j) }
for i = 1 to n
{
for j = 1 to n
{
d(k,i,j) = min{ d(k-1,i,j), d(k-1,i,k) + d(k-1,k,j) };
}
}
}
}
根据公式,明显:
D(0,i,j) = W(i,j) //边信息
D(0,i,j) = 0 3 8 INF -4
INF 0 INF 1 7
INF 4 0 INF INF
2 INF -5 0 INF
INF INF INF 6 0
D(1,i,j) = min{ D(0,i,j), D(0,i,1) + D(0,1,j) },
即此时D(0,i,j)中的每对点只允许通过{v1}进行最短路径的改善,从图中很容易发现Min(4,2),Min(4,5)会因此得到改善
D(1,i,j) = 0 3 8 INF -4
INF 0 INF 1 7
INF 4 0 INF INF
2 5 -5 0 -2
INF INF INF 6 0
e.g.对于Min(4,2) = D(1,4,2) = D(0,4,1) + D(0,1,2) = 2 + 3 = 5
从图上看更明显,v4->v2本来没有边即w(4,2)=INF,由于{v1}的引入,才建立了路径v4->v1->v2
D(2,i,j) = min{ D(1,i,j), D(1,i,2) + D(1,2,j) },
即此时D(0,i,j)中的每对点只允许通过{v1,v2}进行最短路径的改善,
即D(1,i,j)中的每对点只允许通过{v2}进行最短路径的改善, 从图中很容易发现Min(1,4),Min(3,4),Min(3,5)会因此得到改善,
D(2,i,j) = 0 3 8 4 -4
INF 0 INF 1 7
INF 4 0 5 11
2 5 -5 0 -2
INF INF INF 6 0
e.g.对于Min(1,4) = D(2,1,4) = D(1,1,2) + D(1,2,4) = 3 + 1 = 4
从图上看更明显,v1->v4本来没有边即w(1,4)=INF,由于{v1,v2}的引入,才建立了路径v1->v2->v4, 或者说引入{v1}没有缩短路径(1,4),引入v2后达到了效果。
D(3,i,j) = min{ D(2,i,j), D(2,i,3) + D(2,3,j) },
即此时D(0,i,j)中的每对点只允许通过{v1,v2,v3}进行最短路径的改善,
即D(2,i,j)中的每对点只允许通过{v3}进行最短路径的改善, 从图中发现Min(4,2)会因此得到改善,
D(3,i,j) = 0 3 8 4 -4
INF 0 INF 1 7
INF 4 0 5 11
2 -1 -5 0 -2
INF INF INF 6 0
e.g.对于Min(4,2) = D(3,4,2) = D(2,4,3) + D(2,3,2) = -5 + 4 = -1
从图上看更明显,由于{v1,v2}的引入,v4->v2建立了路径为v4->v1->2,Min(4,2)=5,但当引入了{v3}后能再一次改善最短路径Min(4,2) v4->v3->v2。
D(4,i,j) = min{ D(3,i,j), D(3,i,4) + D(3,4,j) },
即此时D(0,i,j)中的每对点只允许通过{v1,v2,v3,v4}进行最短路径的改善,
即D(3,i,j)中的每对点只允许通过{v4}进行最短路径的改善, 从图中或根据公式发现以下点对会因此得到改善,
D(4,i,j) = 0 3 -1 4 -4
3 0 -4 1 -1
7 4 0 5 3
2 -1 -5 0 -2
8 5 1 6 0
e.g.对于Min(3,1) = D(4,3,1) = D(3,3,4) + D(3,4,1) = 5 + 2 = 7
从图上看更明显,{v1,v2,v3}的引入,并没有帮助v3->v1建立路径,Min(3,1)=INF,但当引入了{v4}后能改善最短路径Min(3,1) v3->v2->v4->v1。
D(5,i,j) = min{ D(4,i,j), D(4,i,5) + D(3,5,j) },
即此时D(0,i,j)中的每对点只允许通过{v1,v2,v3,v4,v5}进行最短路径的改善,
即D(4,i,j)中的每对点只允许通过{v5}进行最短路径的改善, 从图中或根据公式发现以下点对会因此得到改善,
D(5,i,j) = 0 1 -3 2 -4
3 0 -4 1 -1
7 4 0 5 3
2 -1 -5 0 -2
8 5 1 6 0
e.g.对于Min(1,3) = D(5,1,3) = D(4,1,5) + D(4,5,3) = -4 + 1 = -3
从图上看更明显,由于{v1,v2,v3,v4}的引入,v1->v3建立了路径为 v1->v2->v4->v3,Min(1,3)=-1,但当引入了{v5}后能再一次改善最短路径Min(1,3) v1->v5->v4->v3
上面的分析看似需要n+1个矩阵D来存储点对间对于不同k值的最短路径D(k,i,j)。
由递归公式可以明显看出D(k)只依赖于D(k-1)所以至多需要两个矩阵就够了。
接下来看,能不能再进一步,只保留一个矩阵进行迭代更新呢?
先设有两个矩阵D(k),D(k-1)其中D(k)对于D(k-1)做了一些最短路径权值更新,
只要D(k)更新的元素都保证不被用于计算更新其他元素,这样就可以在D(k-1)矩阵上进行原地更新,即只需要一个矩阵。
比如D(1)相对D(0)更新了Min(4,2), Min(4,5) 只要Min(4,2) 不被用来计算Min(4,5)则在D(0)上直接做元素更新是能得到D(1)的。
根据公式看,如果D(k,i,j) = D(k-1,i,j) 则显然不会影响,因为元素等于上一次的值,根本不用更新。
剩下 D(k,i,j) = D(k-1,i,k) + D(k-1,k,j) = D(k,i,k) + D(k,k,j), 前面绿色公式已经证明。
也就是说对于D(k)的更新目前只依赖D(k)本身的元素,所以可以在其基础上进行更新。
亦即公式D(k,i,j) = min{ D(k-1,i,j), D(k-1,i,k) + D(k-1,k,j) } 右边的项均不改变值,所以不需要进行额外存储,完全可以利用其对该矩阵其他元素进行更新。
综上,我们可以只利用一个矩阵完成迭代计算。
于是有下面的算法:
FloydWarshall(W)
{
int n = W.rows;
int D[n][n];
D = W; //D(0,i,j)
for k = 1 to n
{
for i = 1 to n
{
for j = 1 to n
{
if (D[i][k] + D[k][j] < D[i][j]) //D(k-1,i,j) > D(k-1,i,k) + D(k-1,k,j)
{
D[i][j] = D[i][k] + D[k][j]; //D(k,i,j) = D(k-1,i,k) + D(k-1,k,j)
}
}
}
}
print(D);
}
很明显,该算法的时间复杂度为O(n^3) 至此问题算是解决一半了。
得到了点对最短路径权值,但如何记录最短路径是什么呢?分析雷同,就不详述了,下面的代码实现中含有路径信息,有兴趣的可以阅读下。
代码实现:
注意:这次用的图的存储方式为邻接矩阵 ,而不是前面的邻接表形式了。
该程序最终会打印出最短路径权值矩阵,同时以一对顶点为例打印其最短路径。
该代码主要以实现功能,验证此算法为目的,对于一些细节问题,如错误保护机制等并为考虑。比如要判断图是否有回路,可以通过对d(k,i,j)的值来判断。
C++语言 :
Codee#2582
#include <iostream>
using
namespace
std ;
const
int
MAX_VERTEX_NUM
=
20 ;
const
int
INFINIT
=
65535 ;
struct
VertexNode
{
char
data
[
2
];
int
op ;
};
struct
Graph
{
VertexNode
VertexNode
[
MAX_VERTEX_NUM
];
int
**
Matrix ;
int
vertexNum ;
int
edgeNum ;
};
void
CreateGraph (
Graph
&
g )
{
g
.
Matrix
=
NULL ;
int
i
,
j
,
edgeStart
,
edgeEnd
,
weight ;
cout
<<
"Please input vertex and edge num (vnum enum):"
<<
endl ;
cin
>>
g
.
vertexNum
>>
g
.
edgeNum ;
cout
<<
"Please input vertex information (v1)
/n
note: every vertex info end with Enter"
<<
endl ;
for (
i
=
0 ;
i
<
g
.
vertexNum ;
i
++ )
{
cin
>>
g
.
VertexNode
[
i
].
data ;
// vertex data info.
}
int n
=
g
.
vertexNum ;
// in order to use like D[i][j]
g
.
Matrix
=
new
int
*
[ n
];
for (
i
=
0 ;
i
< n;
++
i )
{
g
.
Matrix
[
i
]
=
new
int
[ n
];
}
for (
i
=
0 ;
i
< n;
i
++ )
{
for (
j
=
0 ;
j
< n;
j
++ )
{
if (
j
==
i )
g
.
Matrix
[
i
][
j
]
=
0 ;
else
g
.
Matrix
[
i
][
j
]
=
INFINIT ;
}
}
cout
<<
"Please input edge information(start end weight):"
<<
endl ;
for (
j
=
0 ;
j
<
g
.
edgeNum ;
j
++ )
{
cin
>>
edgeStart
>>
edgeEnd
>>
weight ;
g
.
Matrix
[
edgeStart
-
1
][
edgeEnd
-
1
]
=
weight ;
}
}
void
PrintAdjMatrix (
const
Graph
&
g )
{
int
i
,
j
, n
,
weight ;
n
=
g
.
vertexNum ;
cout
<<
"Adjacent matrix for this graph is:
/n
" ;
for (
i
=
0 ;
i
< n;
i
++ )
{
for (
j
=
0 ;
j
< n;
j
++ )
{
weight
=
g
.
Matrix
[
i
][
j
];
if (
weight
==
INFINIT )
cout
<<
"INF
/t
" ;
else
{
cout
<<
weight
<<
"
/t
" ;
}
}
cout
<<
"
/n
" ;
}
cout
<<
"The original edge info is:
/n
" ;
for (
int
i
=
0 ;
i
< n;
i
++ )
{
for (
j
=
0 ;
j
< n;
j
++ )
{
if (
g
.
Matrix
[
i
][
j
]
<
INFINIT
&&
i
!=
j )
cout
<<
g
.
VertexNode
[
i
].
data
<<
"->"
<<
g
.
VertexNode
[
j
].
data
<<
"
/n
" ;
}
}
}
void
DeleteGraph (
Graph
&
g )
{
if (
g
.
Matrix
!=
NULL )
delete
[]
g
.
Matrix ;
g
.
Matrix
=
NULL ;
}
void
PrintShortestMatrix (
int
**
matrix
,
int
size )
{
cout
<<
"The shortest path matrix is:
/n
" ;
int
weight ;
for (
int
i
=
0 ;
i
<
size ;
++
i )
{
for (
int
j
=
0 ;
j
<
size ;
++
j )
{
weight
=
matrix
[
i
][
j
];
if (
weight
==
INFINIT )
cout
<<
"INF
/t
" ;
else
{
cout
<<
weight
<<
"
/t
" ;
}
}
cout
<<
"
/n
" ;
}
}
void
PrintShortestPath (
int
**
path
,
int
i
,
int
j )
{
static
int
fi
=
0 ;
static
int
fj
=
0 ;
if (
fi
==
0 )
{
cout
<<
"The shortest path from v"
<<
i
+
1
<<
" to v"
<<
j
+
1
<<
" is:
/n
" ;
cout
<<
"v"
<<
i
+
1
<<
"->" ;
fi
=
1 ;
}
if (
path
[
i
][
j
]
==
INFINIT )
return ;
int
k
=
path
[
i
][
j
];
cout
<<
"v"
<<
k
+
1
<<
"->" ;
PrintShortestPath (
path
,
i
,
k );
PrintShortestPath (
path
,
k
,
j );
if (
fj
==
0 )
{
cout
<<
"v"
<<
j
+
1
<<
endl ;
fj
=
1 ;
}
}
void
FloydWarshall (
Graph
&
g )
{
int
i
,
j
,
k
, n;
n
=
g
.
vertexNum ;
int
**
d
=
new
int
*
[ n
];
int
**
path
=
new
int
*
[ n
];
for (
i
=
0 ;
i
< n;
++
i )
{
d
[
i
]
=
new
int
[ n
];
path
[
i
]
=
new
int
[ n
];
}
for (
i
=
0 ;
i
< n;
++
i )
{
for (
j
=
0 ;
j
< n;
++
j )
{
d
[
i
][
j
]
=
g
.
Matrix
[
i
][
j
];
path
[
i
][
j
]
=
INFINIT ;
}
}
for (
k
=
0 ;
k
< n;
k
++ )
{
for (
i
=
0 ;
i
< n;
i
++ )
{
for (
j
=
0 ;
j
< n;
j
++ )
{
if (
d
[
i
][
k
]
+
d
[
k
][
j
]
<
d
[
i
][
j
])
{
d
[
i
][
j
]
=
d
[
i
][
k
]
+
d
[
k
][
j
];
path
[
i
][
j
]
=
k ;
}
}
}
}
PrintShortestMatrix (
d
, n);
//input i-1, j-1
PrintShortestPath (
path
,
3
,
1 );
delete
[]
d ;
delete
[]
path ;
}
int
main (
int
argc
,
const
char
**
argv )
{
Graph
g ;
CreateGraph (
g );
PrintAdjMatrix (
g );
FloydWarshall (
g );
DeleteGraph (
g );
return
0 ;
}
输出结果:
上面对于该算法的分析,求解过程实际上是典型的动态规划解题过程,
1. 描述最优子结构:那个公式的推倒,思考过程。
2. 得到一个递归解:那个公式。
3. 按自底向上的方式计算最优解:最短路径权值矩阵
4. 有计算结果构造一个最优解:给出最短路径 path
之所以开始没提动态规划,是避免有人看了这四个字就被吓跑了,因为这个算法不是很好理解,需要反复研读,练习。
希望Floyd算法能提供一个很好的动态规划样例。