- package com.xh.Dijkstra;
- //这个算法用来解决无向图中任意两点的最短路径
- public class ShortestDistanceOfTwoPoint_V5 {
- public static int dijkstra(int[][] W1, int start, int end) {
- boolean[] isLabel = new boolean[W1[0].length];// 是否标号
- int[] indexs = new int[W1[0].length];// 所有标号的点的下标集合,以标号的先后顺序进行存储,实际上是一个以数组表示的栈
- int i_count = -1;//栈的顶点
- int[] distance = W1[start].clone();// v0到各点的最短距离的初始值
- int index = start;// 从初始点开始
- int presentShortest = 0;//当前临时最短距离
- indexs[++i_count] = index;// 把已经标号的下标存入下标集中
- isLabel[index] = true;
- while (i_count
0].length) { - // 第一步:标号v0,即w[0][0]找到距离v0最近的点
- int min = Integer.MAX_VALUE;
- for (int i = 0; i < distance.length; i++) {
- if (!isLabel[i] && distance[i] != -1 && i != index) {
- // 如果到这个点有边,并且没有被标号
- if (distance[i] < min) {
- min = distance[i];
- index = i;// 把下标改为当前下标
- }
- }
- }
- if (index == end) {//已经找到当前点了,就结束程序
- break;
- }
- isLabel[index] = true;//对点进行标号
- indexs[++i_count] = index;// 把已经标号的下标存入下标集中
- if (W1[indexs[i_count - 1]][index] == -1
- || presentShortest + W1[indexs[i_count - 1]][index] > distance[index]) {
- // 如果两个点没有直接相连,或者两个点的路径大于最短路径
- presentShortest = distance[index];
- } else {
- presentShortest += W1[indexs[i_count - 1]][index];
- }
- // 第二步:将distance中的距离加入vi
- for (int i = 0; i < distance.length; i++) {
- // 如果vi到那个点有边,则v0到后面点的距离加
- if (distance[i] == -1 && W1[index][i] != -1) {// 如果以前不可达,则现在可达了
- distance[i] = presentShortest + W1[index][i];
- } else if (W1[index][i] != -1
- && presentShortest + W1[index][i] < distance[i]) {
- // 如果以前可达,但现在的路径比以前更短,则更换成更短的路径
- distance[i] = presentShortest + W1[index][i];
- }
- }
- }
- //如果全部点都遍历完,则distance中存储的是开始点到各个点的最短路径
- return distance[end] - distance[start];
- }
- public static void main(String[] args) {
- // 建立一个权值矩阵
- int[][] W1 = { //测试数据1
- { 0, 1, 4, -1, -1, -1 },
- { 1, 0, 2, 7, 5, -1 },
- { 4, 2, 0, -1, 1, -1 },
- { -1, 7, -1, 0, 3, 2 },
- { -1, 5, 1, 3, 0, 6 },
- { -1, -1, -1, 2, 6, 0 } };
- int[][] W = { //测试数据2
- { 0, 1, 3, 4 },
- { 1, 0, 2, -1 },
- { 3, 2, 0, 5 },
- { 4, -1, 5, 0 } };
- System.out.println(dijkstra(W1, 0,4));
- }
- }
- package com.xh.Dijkstra;
- //这个程序用来求得一个图的最短路径矩阵
- public class ShortestDistance_V4 {
- public static int dijkstra(int[][] W1, int start, int end) {
- boolean[] isLabel = new boolean[W1[0].length];// 是否标号
- int min = Integer.MAX_VALUE;
- int[] indexs = new int[W1[0].length];// 所有标号的点的下标集合
- int i_count = -1;
- int index = start;// 从初始点开始
- int presentShortest = 0;
- int[] distance = W1[start].clone();// v0到各点的最短距离的初始值
- indexs[++i_count] = index;// 把已经标号的下标存入下标集中
- isLabel[index] = true;
- while (true) {
- // 第一步:标号v0,即w[0][0]找到距离v0最近的点
- min = Integer.MAX_VALUE;
- for (int i = 0; i < distance.length; i++) {
- if (!isLabel[i] && distance[i] != -1 && i != index) {
- // 如果到这个点有边,并且没有被标号
- if (distance[i] < min) {
- min = distance[i];
- index = i;// 把下标改为当前下标
- }
- }
- }
- if (index == end) {
- break;
- }
- isLabel[index] = true;
- indexs[++i_count] = index;// 把已经标号的下标存入下标集中
- if (W1[indexs[i_count - 1]][index] == -1
- || presentShortest + W1[indexs[i_count - 1]][index] > distance[index]) {
- presentShortest = distance[index];
- } else {
- presentShortest += W1[indexs[i_count - 1]][index];
- }
- // 第二步:奖distance中的距离加入vi
- for (int i = 0; i < distance.length; i++) {
- // 如果vi到那个点有边,则v0到后面点的距离加
- // 程序到这里是有问题滴! 呵呵
- if (distance[i] == -1 && W1[index][i] != -1) {// 如果以前不可达,则现在可达了
- distance[i] = presentShortest + W1[index][i];
- } else if (W1[index][i] != -1
- && presentShortest + W1[index][i] < distance[i]) {
- // 如果以前可达,但现在的路径比以前更短,则更换成更短的路径
- distance[i] = presentShortest + W1[index][i];
- }
- }
- }
- return distance[end] - distance[start];
- }
- public static int[][] getShortestPathMatrix(int[][] W) {
- int[][] SPM = new int[W.length][W.length];
- //多次利用dijkstra算法
- for (int i = 0; i < W.length; i++) {
- for (int j = i + 1; j < W.length; j++) {
- SPM[i][j] =dijkstra(W, i, j);
- SPM[j][i] = SPM[i][j];
- }
- }
- return SPM;
- }
- public static void main(String[] args) {
- /* 顶点集:V={v1,v2,……,vn} */
- int[][] W = { { 0, 1, 3, 4 }, { 1, 0, 2, -1 }, { 3, 2, 0, 5 },
- { 4, -1, 5, 0 } };
- int[][] W1 = { { 0, 1, 4, -1, -1, -1 }, { 1, 0, 2, 7, 5, -1 },
- { 4, 2, 0, -1, 1, -1 }, { -1, 7, -1, 0, 3, 2 },
- { -1, 5, 1, 3, 0, 6 }, { -1, -1, -1, 2, 6, 0 } };// 建立一个权值矩阵
- ;// 建立一个权值矩阵
- int[][] D = getShortestPathMatrix(W1);
- //输出最后的结果
- for (int i = 0; i < D.length; i++) {
- for (int j = 0; j < D[i].length; j++) {
- System.out.print(D[i][j] + " ");
- }
- System.out.println();
- }
- }
- }
最短路径问题—Bellman-Ford算法
一、算法思想
1.Dijkstra算法的局限性
上节介绍了Dijkstra算法,该算法要求网络中各边上得权值大于或等于0.如果有向图中存在带负权值的边,则采用Dijkstra算法求解最短路径得到的结果有可能是错误的。
例如,对下图所示的有向图,采用Dijkstra算法求得顶点v0到顶点v2的最短距离是dist[2],即v0到v2的直接路径,长度为5.但从v0到v2的最短路径应该是(v0,v1,v2),其长度为2。
如果把图1(a)中的边<1,2>的权值由-5改成5,则采用Dijkstra算法求解最短路径,得到的结果是正确的(这里不再求解)。
为什么当有向图中存在带负权值的边时,采用Dijkstra算法求解得到的最短路径有时是错误的?答案是:Dijkstra算法在利用顶点u的dist[]去递推T集合各顶点的dist[k]值时,前提是顶点u的dist[]值时当前T集合中最短路径长度最小的。如果图中所有边的权值都是正的,这样推导是没有问题的。但是如果有负权值的边,这样推导是不正确的。例如,在图1(d)中,第1次在T集合中找到dist[]最小的是顶点2,dist[2]等于5;但是顶点0距离顶点2的最短路径是(v0,v1,v2),长度为2,而不是5,其中边<1,2>是一条负权值边。
2.Bellman-Ford算法思想
为了能够求解边上带有负权值的单源最短路径问题,Bellman(贝尔曼)和Ford(福特)提出了从源点逐次途经其他顶点,以缩短到达终点的最短路径长度的方法。该方法也有一个限制条件:要求图中不能包含权值总和为负值的回路。
例如图2(a)所示的有向图中,回路(v0,v1,v0)包括了一条具有负权值的边,且其路径长度为-1。当选择的路径为(v0,v1,v0,v1,v0,v1,v0,v1,…)时,路径的长度会越来越小,这样顶点0到顶点2的路径长度最短可达-∞。如果存在这样的回路,则不能采用Bellman-Ford算法求解最短路径。
如果有向图中存在由带负权值的边组成的回路,但回路权值总和非负,则不影响Bellman-Ford算法的求解,如图2(b)所示。
权值总和为负值的回路我们称为负权值回路,在Bellman-Ford算法中判断有向图中是否存在负权值回路的方法,见后面。
假设有向图中有n个不存在负权值回路,从顶点v1和到顶点v25如果存在最短路径,则此路径最多有n-1条边。这是因为如果路径上得边数超过了n-1条时,必然会重复经过一个顶点,形成回路;而如果这个回路的权值总和为非负时,完全可以去掉这个回路,使得v1到v2的最短路径长度缩短。下面将以此为依据,计算从源点v0到其他每个顶点u的最短路径长度dist[u]。
Bellman-Ford算法构造一个最短路径长度数组序列:dist1[u],dist2[u],dist3[u],…,distn-1[u]。其中:
dist1[u]为从源点v0到终点u的只经过一条边的最短路径的长度,并有dist1[u]=edge[v0,u]。
dist2[u]为从源点v0出发最多经过不构成负权值回路的两条边到达终点u的最短路径长度。
dist3[u]为从源点v0出发最多经过不构成负权值回路的3条边到达终点u的最短路径长度。
……
distn-1[u]为从源点v0出发最多经过不构成负权值回路的n-1条边到达终点u的最短路径长度。
算法的最终目的是计算出distn-1[u],为源点v0到顶点u的最短路径长度。
采用递推方式计算distk[u]。
设已经求出distk-1[u],u=0,1,…,n-1,此即从源点v0最多经过不构成负权值回路的k-1条边到达终点u的最短路径的长度。
从图的邻接矩阵可以找出各个顶点j到达顶点u的(直接边)距离edge[j,u],计算min{distk-1[j]+edge[j,u]},可得从源点v0途经各个顶点,最多经过不构成回路的k条边到达终点u的最短路径的长度。
比较distk-1[u]和min{distk-1[j]+edge[j,u]},取较小者作为distk[u]的值。
因此Bellman-Ford算法的递推公式(求源点v0到各顶点u的最短路径)为:
初始:dist1[u]=edge[v0,u],v0是源点
递推:distk[u]=min{distk-1[u],min{distk-1[j]+edge[j,u]}}
j=0,1,...,n-1,j<>u; k=2,3,4,...,n-1
二、算法实现
Bellman-Ford算法在实现时,需要使用以下两个数组。
(1) 使用同一个数组dist[n]来存放一系列的distk[n],其中k=1,2,...,n-1;算法结束时dist[u]数组中存放的是distn-1[u];
(2) path[n]数组含义同Dijkstra算法中的path数组。
【例题1】利用Bellman-Ford算法求解下图(a)中顶点0到其他各顶点的最短路径长度,并输出对应的最短路径。
输入:首先输入顶点个数n,然后输入每条边的数据。每条边的数据格式为:u v w,分别表示这条边的起点、终点和边上的权值。顶点序号从0开始计起。最后一行为-1 -1 -1,表示输入数据的结束。
样例输入: 样例输出:
7 1 0->3->2->1
0 1 6 3 0->3->2
0 2 5 5 0->3
0 3 5 0 0->3->2->1->4
1 4 -1 4 0->3->5
2 1 -2 3 0->3->2->1->4->6
2 4 1
3 2 -2
3 5 -1
4 6 3
5 6 3
-1 -1 -1
【分析】
如图3(c)所示,k=1时,dist数组各元素的值dist[u]就是edge[0,u](见图3(b))。在Bellman-Ford算法执行过程中,dist数组各元素的变化如图3(c)所示。在图3(c)中,dist[u]的值如有更新,则用粗体,红色标明,u=1,2,3,4,5,6。以k=2,u=1加以解释。求dist2[1]的递推公式是:
dist2[1]=min{ dist1[1],min{dist1[j]+edge[j,1]}} j=0,2,3,4,5,6
所以,在程序中k=2时,dist[1]的值为:
dist[1]=min{6,min{dist[0]+edge[0,1],dist[2]+edge[2,1], dist[3]+edge[3,1], dist[4]+edge[4,1],
dist[5]+edge[5,1], dist[6]+edge[6,1]}}
=min{6,min{0+6, 5+(-2), 5+∞, ∞+∞, ∞+∞, ∞+∞}}
=3
此时dist[1]的值为从源点v0出发,经过不构成负权值回路的两条边到达顶点v1的最短路径长度,其路径为(v0,v2,v1)。
在Bellman-Ford算法执行过程中,path[n]数组的变化与Dijkstra算法类似,所以在图3中没有列出path[n]数组的变化过程。当顶点0到其他各顶点的最短路径长度求解完毕后,如果根据path数组求解顶点0到其他各顶点vk的最短路径?方法跟Dijkstra算法中的方法完全一样:从path[k]开始,采用“倒向追踪”方法,一直找到源点v0。
在下面的代码中,bellman(v0)函数实现了求源点v0到其他各顶点的最短路径。在主函数中调用bellman(0),则求解的是从顶点0到其他各顶点的最短路径。另外,主函数中的shortest数组用来保存最短路径上各个顶点的序号,其作用和上一讲中Dijkstra程序代码中的shortest数组的作用一样。
【参考程序】
var n:longint; //顶点个数
i,j,k,u,v,w:longint;
edge:array[0..8,0..8]of longint; //邻接矩阵,这里的8为顶点个数最大值,可修改
dist,path,shortest:array[0..8]of longint; //shortest数组是输出最短路径上的各个顶点时存放各个顶点的序号
procedure bellman(v0:longint); //求顶点v0到其他顶点的最短路径
var i,j,k,u:longint;
begin
for i:=0 to n-1 do //初始化
begin
dist[i]:=edge[v0,i];
if (i<>v0)and(dist[i]<1000000) then path[i]:=v0 else path[i]:=-1;
end;
for k:=2 to n-1 do //从dist(1)[u]递推出dist(2)[u],...,dist(n-1)[u]
begin
for u:=0 to n-1 do //修改每个顶点的dist[u]和path[u]
if u<>v0 then
for j:=0 to n-1 do //考虑其他每个顶点
if (edge[j,u]<1000000)and(dist[j]+edge[j,u]0 do
begin inc(k);shortest[k]:=path[shortest[k-1]];end;
inc(k);shortest[k]:=0;
for j:=k downto 1 do write(shortest[j],'->');
writeln(shortest[0]);
end;
end.
三、关于Bellman-Ford算法的进一步讨论
(1)本质思想
在从dist(k-1)[]递推到dist(k)[]的过程中,Bellman-Ford算法的本质是对每条边进行判断:设边的权值为w(u,v),如图4所示,如果边的引入会使得dist(k-1)[v]的值再减小,则要修改dist(k-1)[v],即:如果dist(k-1)[u]+w(u,v)
按照这样的思想,Bellman-Ford算法的递推公式应该改为(求源点v0到各顶点v的最短路径):
初始:dist(0)[v]=∞,dist(0)[v0]=0,v0是源点,v<>v0
递推:对每条边(u,v),dist(k)[v]=min{dist(k-1)[v],dist(k-1)[u]+w(u,v)} k=1,2,3,...,n-1
理解了这点,就能理解Bellman-Ford算法的复杂度分析、Bellman-Ford算法的优化等。
(2)时间复杂度分析
在例题1的Bellman-Ford算法代码中,有一个三重嵌套的for循环,如果使用邻接矩阵存储有向图,最内层的if语句的总执行次数为n^3,所以算法的时间复杂度为o(n^3)。
如果使用邻接表来存储有向图,内层的两个for循环可以改成一个while循环,可以使算法的时间复杂度降为o(n*m),其中n为有向图中顶点个数,m为边的数目。这时因为,邻接表里直接存储了边地信息,浏览完所有的边,复杂度为o(m)。而邻接矩阵是间接存储边,浏览完所有的边,复杂度为o(n^2)。
使用邻接表存储思想实现Bellman-Ford算法的具体过程为:
将每条边的信息(两个顶点u,v和权值w,可以使用记录record来实现)存储到一个数组edges中,从dist(k-1)[]递推到dist(k)[]的过程中,对edges数组中的每条边,判断一下边的引入,是否会缩短源点v0到顶点v的最短路径长度。
根据上面的分析,可以将例题1中的bellman函数简化成如下代码:
procedure bellman(v0:longint); //求顶点v0到其他顶点的最短路径
vari,k:longint;
begin
for i:=0 to n-1 do //初始化dist数组
begin dist[i]:=1000000;path[i]:=-1; end;
dist[v0]:=0;
for k:=1 to n-1 do //从dist(0)[u]递推出dist(1)[u],...,dist(n-1)[u]
//判断第i条边的引入,是否会缩短源点v0到顶点v的最短路径长度
for i:=0 to m-1 do //m为边的数目,即edges数组中元素个数
if(dist[edges[i].u]<.1000000)and(edges[i].w+dist[edges[i].u]
其中,dist数组各元素中,除源点v0外,其他顶点的dist[]值都初始化为∞,这样Bellman-Ford算法需要多递推一次,详见后面。
(3)Bellman-Ford算法负权值回路的判断方法
如果存在从源点可达的负权值回路,则最短路径不存在,因为可以重复走这个回路,使得路径无穷小。在Bellman-Ford算法中,判断是否存在从源点可达的负权值回路的方法如下。在求出dist(n-1)[]后,再对每条边判断一下:加入这条边是否会使得顶点v的最短路径值再缩短。即判断:
dist[u]+edge[u,v] 是否成立,如果成立,则说明存在从源点可达的负权值回路。代码如下: for i:=0 to n-1 do //采用邻接矩阵 for j:=0 to n-1 do if (edge[i,j]<1000000)and(dist[i]+edge[i,j] exit(1); //不存在从源点可达的负权值回路 或者: for i:=0 to m-1 do if(dist[edges[i].u]<>1000000)and(edges[i].w+dist[edges[i].u] exit(1); //不存在从源点可达的负权值回路 (4)Bellman-Ford算法中数组dist的初始值 dist数组的初始值为邻接矩阵中源点v0所在的行,实际上还可以采用以下方式对dist数组初始化:除源点v0外,其他顶点的最短距离初始为∞(在程序实现时可以用一个权值不会达到的一个大数表示):源点dist[v0]=0。这样Bellman-Ford算法的第1重for循环要多执行一次,即要执行n-1次;且执行第1次后,dist数组的取值为邻接矩阵中源点v0所在的行。 (5)Bellman-Ford算法的改进 Bellman-Ford算法是否一定要循环n-2次,n为顶点个数,即是否一定需要从dist(1)[u]递推到dist(n-1)[u]? 答案是未必!其实只要在某次循环过程中,考虑每条边后,都没能改变当前源点到所有顶点的最短路径长度,那么Bellman-Ford算法就可以提前结束了。