7.7最短路径
【最短路径问题】 如果将交通网络画成带权图,结点代表地点,边代表城镇间的路,边权表示路的长度, 则经常会遇到如下问题:两给定地点间是否有通路?如果有多条通路,哪条路最短?还可以 根据实际情况给各个边赋以不同含义的值。例如,对司机来说,里程和速度是他们最感兴趣 的信息;而对于旅客来说,可能更关心交通费用。有时,还需要考虑交通图的有向性,如航行时,顺水和逆水的情况。带权图的最短路径是指两点间的路径中边权和最小的路径。
1. 求某一顶点到其它各顶点的最短路径
设有带权的有向图 D=(V,{E}), D 中的边权为 W(e)。已知源点为 v0,求 v0到其它各顶点的最短路径。例如,在下图所示的带权有向图中,v0为源点,则 v0到其他各顶点的最短路径如表 7-3 所示,其中各最短路径按路径长度从小到大的顺序排列。
下面介绍由迪杰斯特拉(Dijkstra)提出的一个算法,来求解从顶点 v0出发到其余顶点的最短路径。该算法按照最短路径长度递增的顺序产生所有最短路径。
对于图 G=(V, E),将图中的顶点分成两组:
- 第一组 S:已求出的最短路径的终点集合(开始为{v0})。
- 第二组 V-S:尚未求出最短路径的顶点集合(开始为 V-{v0}的全部结点)。
算法将按最短路径长度的递增顺序逐个将第二组的顶点加入到第一组中,直到所有顶点都被加入到第一组顶点集 S 为止。
[定理]:
下一条最短路径或者是弧(v0,vx),或者是中间经过 S 中的某些顶点,而后到达 vx的 路径。
[证明]:
可用反证法:假设下一条最短路径上有一个顶点 vy不在 S 中,即此路径为(v0, …, vy, …, vx)。显然,(v0,…,vy)的长度小于(v0,…,vy,… ,vx)的长度,故下一条最短路径应 为(v0,…,vy),这与假设的下一条最短路径(v0,…,vy,…, vx)相矛盾!因此,下一条 最短路径上不可能有不在 S 中的顶点 vy,即假设不成立。
算法中使用了辅助数组 dist[ ], dist[i]表示目前已经找到的、从开始点 v0到终点 vi的当 前最短路径的长度。它的初值为:如果从 v0到 vi 有弧,则 dist[i]为弧的权值;否则 dist[i]为 ∞。
根据上述定理,长度最短的一条最短路径必为 ( v0, vk ),vk满足如下条件: dist[k]=Min{dist[i] | vi∈V-S}
求得顶点 vk的最短路径后,将 vk加入到第一组顶点集 S 中。
每加入一个新的顶点 vk到顶点集 S,则对第二组剩余的各个顶点而言,多了一个“中转” 结点,从而多了一个“中转”路径,所以要对第二组剩余的各个顶点的最短路径长度 dist[i] 进行修正。
原来 v0到 vi 的最短路径长度为 dist[i],加进 vk之后,以 vk作为中间顶点的“中转”路 径长度为:dist[k] + wki,( wki 为弧<vk, vi>上的权值),若“中转”路径长度小于 dist[i],则将 顶点 vi的最短路径长度修正为“中转”路径长度。
修正后,再选择数组 dist[ ]中值最小的顶点加入到第一组顶点集 S 中,如此进行下去, 直到图中所有顶点都加入到第一组顶点集 S 中为止。
另外,为了记录从 v0 出发到其余各点的最短路径(顶点序列),引进辅助数组 path[ ],path[i]表示目前已经找到的、从开始点 v0 到终点 vi 的当前最短路径顶点序列。它的初值为: 如果从 v0到 vi有弧,则 path[i]为(v0, vi);否则 path[i]为空。
【算法思想】
g 为用邻接矩阵表示的带权图。
- (1) S←—{v0} ; dist[i]= g.arcs[v0][vi].adj; (vi∈V-S) (将 v0到其余顶点的最短路径长度初始化为权值)
- (2)选择 vk,使得: dist[k]=min(dist[i] | vi∈V-S) vk 为目前求得的下一条从 v0出发的最短路径的终点。
- (3)将 vk加入 S;
- (4)修正从 v0出发到集合 V-S 上任一顶点 vi的最短路径的长度: 从 v0 出发到集合 V-S 上任一顶点 vi的当前最短路径的长度为 dist[i], 从 v0 出发,中间经过新加入 S 的 vk,然后到达集合 V-S 上任一顶点 vi的路径长度为: dist[k] + g.arcs[k][i].adj
如果: dist[k] + g.arcs[k][i].adj <dist[i] 则: dist[i] = dist[k]+ g.arcs[k][i].adj - (5)重复(2) ~ (4) n-1 次,即可按最短路径长度的递增顺序,逐个求出 v0到图中其它每个 顶点的最短路径。
【算法描述】 图的最短路径算法
#define INFINITY 32768 /*表示极大值,即∞*/
typedef unsigned int WeightType;
typedef WeightType AdjType;
typedef SeqList VertexSet;
ShortestPath_DJS(AdjMatrix g, int v0, WeightType dist[MAX_VERTEX_NUM], VertexSet path[MAX_VERTEX_NUM] ) /* path[i]中存放顶点 i 的当前最短路径。dist[i]中存放顶点 i 的当前最短路径长度*/
{
VertexSet s; /* s 为已找到最短路径的终点集合 */
for ( i =0;i<g.vexnum ;i++) /* 初始化 dist[i]和 path [i] */
{
InitList(&path[i]);
dist[i]=g.arcs[v0][i].adj;
if ( dist[i] < INFINITY)
{
AddTail(&path[i], g.vertex[v0]); /* AddTail 是表尾添加操作*/
AddTail(&path[i], g.vertex[i]);
}
}
InitList(&s);
AddTail(&s, g.vertex[v0]); /* 将 v0 看成第一个已找到最短路径的终点*/
for ( t = 1; t<=g.vexnum-1; t++) /*求 v0 到其余 n-1 个顶点的最短路径(n= g.vexnum )*/
{
min= INFINITY;
for ( i =0; i<g.vexnum;i++)
if (! Member(g.vertex[i], s) && dist[i]<min )
{
k =i;
min=dist[i];
}
AddTail(&s, g.vertex[k]);
for ( i =0; i<g.vexnum;i++) /*修正 dist[i], i∈V-S*/
if (!Member(g.vertex [i], s) && g.arcs[k][i].adj!= INFINITY && (dist[k]+ g.arcs [k][i].adj<dist[i]))
{
dist[i]=dist[k]+ g.arcs [k][i].adj;
path[i]=path[k];
AddTail(&path[i], g.vertex [i]); /* path[i]=path[k]∪{Vi} */
}
}
}
算法前半部分完成了对向量最短路径长度 dist[ ],路径 path[],顶点集 s[]的初始化工作。 算法后半部分通过 n-1 次循环,将第二组顶点集 V-S 中的顶点按照递增有序方式加入到集合 S 中,并求得从顶点 v0 出发到达图中其余顶点的最短路径。
显然,算法的时间复杂度为 O(n²)
2. 求任意一对顶点间的最短路
上述方法只能求出源点到其它顶点的最短路径,欲求任意一对顶点间的最短路径,可以 用每一顶点作为源点,重复调用狄杰斯特拉算法 n 次,其时间复杂度为 O(n³)。下面介绍一 种形式更简洁的方法,即佛罗伊德算法,其时间复杂度也是 O(n³)。
【算法思想】:
设图 g 用邻接矩阵法表示,求图 g 中任意一对顶点 vi、、vj 间的的最短路径。
- (-1)将 vi 到 vj 的最短的路径长度初始化为 g.arcs[i][j].adj,然后进行如下 n 次比较和修正:
- (0) 在 vi、vj 间加入顶点 v0,比较(vi,v0,vj)和(vi,vj)的路径的长度,取其中较短的路 径作为 vi到 vj 的且中间顶点号不大于 0 的最短路径。
- (1) 在 vi、vj 间加入顶点 v1,得到(vi,…, v1)和(v1,…, vj),其中(vi,…,v1)是 vi 到 v1 的且中间顶点号不大于 0 的最短路径,(v1,…,vj) 是 v1到 vj 的且中间顶点号不大于 0 的最短路径,这两条路径在上一步中已求出。将(vi,…,v1,…,vj)与上一步已 求出的且 vi到 vj 中间顶点号不大于 0 的最短路径比较,取其中较短的路径作为 vi到 vj 的且中间顶点号不大于 1 的最短路径。
- (2) 在 vi、vj间加入顶点 v2,得(vi,…,v2)和(v2,…,vj),其中(vi,…,v2)是 vi到 v2 的且 中间顶点号不大于 1 的最短路径,(v2,…,vj) 是 v2到 vj 的且中间顶点号不大于 1 的最 短路径,这两条路径在上一步中已求出。将(vi,…,v2,…,vj)与上一步已求出的且 vi 到 vj 中间顶点号不大于 1 的最短路径比较,取其中较短的路径作为 vi到 vj 的且中间 顶点号不大于 2 的最短路径。
………
依次类推,经过 n 次比较和修正,在第(n-1)步,将求得 vi到 vj 的且中间顶点号不大 于 n-1 的最短路径,这必是从 vi到 vj的最短路径。
图 g 中所有顶点偶对 vi、vj间的最短路径长度对应一个 n 阶方阵 D。在上述 n+1 步中, D 的值不断变化,对应一个 n 阶方阵序列。
弗洛伊德算法可以描述如下
【算法描述】 弗洛伊德算法
typedef SeqList VertexSet;
ShortestPath_Floyd(AdjMatrix g, WeightType dist [MAX_VERTEX_NUM] [MAX_VERTEX_NUM], VertexSet path[MAX_VERTEX_NUM] [MAX_VERTEX_NUM] ) /* g 为带权有向图的邻接矩阵表示法, path [i][j]为 vi到 vj的当前最短路径,dist[i][j]为 vi到 vj的当前最短路径长度*/
{
for (i=0; i<g.vexnumn; i++) /* 初始化 dist[i][j]和 path[i][j] */
for (j =0;j<g.vexnum; j++)
{
InitList(&path[i][j]);
dist[i][j]=g.arcs[i][j].adj;
if (dist[i][j]<INFINITY)
{
AddTail(&path[i][j], g.vertex[i]);
AddTail(&path[i][j], g.vertex[j]);
}
}
for (k =0;k<g.vexnum;k++)
for (i =0;i<g.vexnum;i++)
for (j=0;j<g.vexnum;j++)
if (dist[i][k]+dist[k][j]<dist[i][j])
{
dist[i][j]=dist[i][k]+dist[k][j];
paht[i][j]=JoinList(paht[i][k], paht[k][j]);
} /* JoinList 是合并线性表操作 */
}