主要内容
基本概念
最简单的最短路径是求中转次数最少的路径,而不考虑每条边的权值。而在实际问题中,路径长度的度量就不再是路径上的边数,而是路径上所有边的权值之和。
在有向网中,习惯上称路径的第一个顶点为源点(Source),最后一个顶点为终点(Destination)。
下面主要讨论两种最常见的最短路径问题:
一、从某个源点到其余顶点的最短路径;
二、求每一对顶点之间的最短路径。
迪杰斯特拉(Dijkstra)算法用于求解第一种问题,时间复杂度为O(n²);弗洛伊德(Floyd)算法用于求解第二种问题,时间复杂度为O(n³)。
从实现形式上来说,弗洛伊德算法比迪杰斯特拉算法更为简洁。
迪杰斯特拉算法(单源点)
<逻辑思路>
(1)从单源点出发,求到各个顶点的最短路径。该问题类似于就有向连通网的最小生成树;
(2)因此同样需要辅助数组来记录顶点是否被归并和最短路径长度;
(3)迪杰斯特拉算法的巧妙之处就在于设计了三个辅助数组:
1. 一维数组S[]:记录顶点vi是否被确定最短路径长度,即该顶点是否被归并到终点集合中。初始化:S[v0-1] = TRUE;
2. 一维数组Path[]:记录顶点vi的直接前驱。譬如存在最短路径<v1,v4,v3>,v4的直接前驱是v1,v3的直接前驱是v4,这样一来,依靠直接前驱数组就能将路径连接起来。初始化:若源点v0到vi有弧,则Path[vi-1]为v0-1(编号/下标);否则为-1。
3. 一维数组D[]:记录从源点v0到终点vi的最短路径长度。初始化:若源点v0到vi有弧,则D[vi-1]为弧上的权值;否则为∞。
(4)算法开始时,先找到D[]上的最小值,然后并入第一个终点v1,并将S[v1-1]的值设为TRUE;
(5)由于加入了终点v1,相当于v0多了一个中转点,所以需要对D[]上的值进行更新;
(6)更新后继续找D[]上的最小值,继而找到了顶点v2,因为S[v2-1]的值为FALSE,即v2未被归并到终点集合,所以可以将它并入;
(7)若到v2的最短路径是<v0,v1,v2>,则将v2的直接前驱改为v1,即Path[v2-1] = v1-1;
(8)反复执行以上过程(n-1次),直至所有顶点被并入终点集合。
<实现思路>——以邻接矩阵为存储结构的有向网
(1)对应<逻辑思路>步骤(3)的初始化:
1. S[v0-1] = TRUE;
2. D[vi-1] = G.arcs[v0-1][vi-1];
3. if(G.arcs[v0-1][vi-1] != MaxInt) Path[vi-1] = v0-1; /*MaxInt表示无穷∞,值为326767*/
(2)对应<逻辑思路>步骤(8)的n-1次循环
1. 步骤(4):D[v1] = Min(D);
2. 步骤(6):if(S[v1-1] = FALSE) S[v1-1] = TRUE;
3. 步骤(5)和步骤(7): if(D[v1-1] + G.arcs[v1-1][v2-1] < D[v2-1]) {D[v2-1] = D[v1-1] + G.arcs[v1-1][v2-1]; Path[v2-1] = v1-1;}
#define TRUE 1
#define FALSE 0
void ShortestPath_DIJ(AMGraph G, int v0)
{
int S[vexnum];
int Path[vexnum];
int D[vexnum];
int i = LocateVex(G, v0);
for(int vi = 1; vi <= G.vexnum; vi++) /*初始化*/
{
int j =LocateVex(G, vi);
S[j] = FALSE;
D[j] = G.arcs[i][j];
if(D[j] < MaxInt) Path[j] = i; /*小于MaxInt即v0与vi间存在弧*/
else Path[j] = -1; /*若vi的直接前驱不是v0,则置为-1*/
}
S[i] = TRUE;
D[i] = 0; /*源点到源点的路径长度为0*/
/*----------初始化结束-----------*/
for(int t = 1; t < G.vexnum; t++) /*循环n-1次*/
{
int v; /*下一个终点*/
for(vi = 1; vi <= G.vexnum; vi++)
{
int min = MaxInt; /*min保存最小值*/
int j = LocateVex(G, vi);
if(S[j] = FALSE && D[j] < min)/*找出最小权值的边*/
{v = vi; min = D[j];}
}
S[v-1] = TRUE; /*将v加入终点集合*/
int k = LocateVex(G, v);
for(vi = 1; vi <= G.vexnum; vi++) /*更新最短路径和直接前驱数组*/
{
int j = LocateVex(G, vi);
if(S[j] = FALSE && (D[k] + G.arcs[k][j] < D[j])
{
D[j] = D[k] + G.arcs[k][j];
Path[j] = k;
}
}
}
}
弗洛伊德算法(顶点对)
求解每一对顶点之间的最短路径有两种方法:
一种是n次调用迪杰斯特拉算法;
另一种是采用下面介绍的弗洛伊德算法。
两种算法的时间复杂度均为O(n³),但弗洛伊德算法的形式更为简洁。
<逻辑思路>
(1)如果说迪杰斯特拉算法是一个“加边”的过程,那么弗洛伊德算法就是一个“加点”的过程;
(2)先确定两个目标顶点,源点为vs,终点为ve;
(3)若vs和ve之间存在弧,初始化为arcs[vs-1][ve-1](编号),反之置为∞;
(4)依次判断剩余顶点vi,若在两个顶点间插入vi后,使得<vs,vi>+<vi,ve>小于<vs,ve>,则置换为vs和ve间的最短路径;
(5)直至所有剩余顶点判断完毕,最短路径确定;
(6)其余的每一对顶点都重复上述过程。
<实现思路>——以邻接矩阵为存储结构的有向网
(1)该算法的核心思想是设置了两个二维辅助数组:
1. 二维数组Path[][]:行表示顶点vi的直接前驱,列表示顶点vi;
2. 二维数组D[]][]:记录vs和ve间的最短路径。
(2)弗洛伊德算法的代码思路比较清晰,插入、比较、更新。
void ShortestPath_Floyd(AMGraph G)
{
/*-----------初始化------------*/
for(int vs = 1; vs <= G.vexnum; vs++) /*源点vs*/
{
int i = LocateVex(G, vs);
for(int ve = 1; ve <= G.vexnum; ve++) /*终点ve*/
{
int j = LocateVex(G, ve);
D[i][j] = G.arcs[i][j];
if(D[i][j] < MaxInt && i != j)
Path[i][j] = i;
else Path[i][j] = -1;
}
/*---------初始化结束--------*/
for(int vi = 1; vi <= G.vexnum; vi++) /*插入点vi*/
for(int vs = 1; vs <= G.vexnum; vs++) /*源点vs*/
for(int ve = 1; ve <= G.vexnum; ve++) /*终点ve*/
{
int k = LocateVex(G, vi);
int i = LocateVex(G, vs);
int j = LocateVex(G, ve);
if(D[i][k] + D[k][j] < D[i][j]) /*更新*/
{
D[i][j] = D[i][k] + D[k][j];
Path[i][j] = k;
}
}
}