最短路径
在生活中,例如,当我们坐公交或轻轨时,都会看一下交通图,找到在哪个站下是最快能达到目的地的,也就是路径最小。考虑到交通图的有向性,例如汽车的上山下山、轮船的顺水逆水,所花费的时间或代价就不相同,所以交通网往往是用带权的有向网表示。在带权的有向网中,习惯上称路径上的第一个顶点称为源点(Source),最后一个顶点称为终点(Destination)。
下面介绍两种最常见的最短路径问题:
- 从某个源点到其余各顶点 的最短路径
- 每一对顶点之间的最短路径
源点 → \rightarrow →其余各顶点(迪杰斯特拉算法)
迪杰斯特拉(Dijkstra)提出了一个按路径长度递增的次序产生最短路径 的算法。
1. 求解过程
对于网
N
=
(
V
,
E
)
N=(V,E)
N=(V,E),将N中的顶点分为两组(同MST性质一样):
S:已求出最短路径所包含的顶点
V-S:未求出的最短路径的顶点
该求解过程也就是按各顶点与 v 0 v_{0} v0间最短路径长度递增的次序,逐个将V-S中的顶点加入到S中。
例如,对于下图求解其
v
0
v_{0}
v0的最小路径
根据迪杰斯特拉算法的求解过程,首先求出
v
0
v_{0}
v0到
v
2
v_{2}
v2的路径
<
v
0
,
v
2
>
<v_{0},v_{2}>
<v0,v2>,然后按路径长度递增的次序依次得到
v
0
v_{0}
v0到
v
4
v_{4}
v4的路径
<
v
0
,
v
4
>
<v_{0},v_{4}>
<v0,v4>,
v
0
v_{0}
v0到
v
3
v_{3}
v3的路径
<
v
0
,
v
4
,
v
3
>
<v_{0},v_{4},v_{3}>
<v0,v4,v3>,
v
0
v_{0}
v0到
v
5
v_{5}
v5的路径
<
v
0
,
v
4
,
v
3
,
v
5
>
<v_{0},v_{4},v_{3},v_{5}>
<v0,v4,v3,v5>,而
v
0
v_{0}
v0到
v
1
v_{1}
v1没有路径。
或许用下面的表格来表示更加清晰:
源点 | 终点 | 最短路径 | 路径长度 |
---|---|---|---|
v 0 v_{0} v0 | v 2 v_{2} v2 | < v 0 , v 2 > <v_{0},v_{2}> <v0,v2> | 10 |
v 0 v_{0} v0 | v 4 v_{4} v4 | < v 0 , v 4 > <v_{0},v_{4}> <v0,v4> | 30 |
v 0 v_{0} v0 | v 3 v_{3} v3 | < v 0 , v 4 , v 3 > <v_{0},v_{4},v_{3}> <v0,v4,v3> | 50 |
v 0 v_{0} v0 | v 5 v_{5} v5 | < v 0 , v 4 , v 3 , v 5 > <v_{0},v_{4},v_{3},v_{5}> <v0,v4,v3,v5> | 60 |
v 0 v_{0} v0 | v 1 v_{1} v1 | 无 | ∞ \infty ∞ |
熟悉了该算法的求解过程后,下面来实现该算法的代码
2. 算法实现
算法分析:
在这里我们对于无向网仍用邻接矩阵的存储表示,源点为
v
0
v_{0}
v0。
- 首先,我们想到的是最短路径一定有一个值,那么我们可以用一维数组来存储当前 v 0 v_{0} v0到其余各顶点的最短路径的值,命名该数组为 D [ i ] D[i] D[i]。若就 v 0 v_{0} v0和 v i v_{i} vi之间没有弧,则 D [ i ] = ∞ D[i]=\infty D[i]=∞。!!注意:该数组的初始化也被赋于就是 v 0 v_{0} v0和 v i v_{i} vi之间的弧的权值,且默认该值就是当前的最小路径,对于后续操作,我们会对该数组中的值进行更新,通过比较来取得更小的路径。
- 其次,我们对于这些算法大都有循环结构,那么我们就需要直到 v 0 v_{0} v0到 v i v_{i} vi的最短路径是否已被确定。我们对此也可以用一个一维数组 S [ i ] S[i] S[i]来记录,若已确定,则 S [ i ] = t u r e S[i]=ture S[i]=ture,否则 S [ i ] = f a l s e S[i]=false S[i]=false。
- 那么还有一个问题需要我们考虑的是,若遇到上图 v 0 v_{0} v0到 v 1 v_{1} v1没有路径的情况该怎么办。我们需要记录其余顶点的直接前趋是否存在,也可以用一个一维数组 P a t h [ i ] Path[i] Path[i]来存储该顶点的直接前趋的顶点序号。例如,若 v 0 v_{0} v0到 v i v_{i} vi若有弧,则 P a t h [ i ] = v 0 Path[i]=v_{0} Path[i]=v0,否则 P a t h [ i ] = − 1 Path[i]=-1 Path[i]=−1。
- 每当加入一个新的顶点到S中,则该顶点可作为一个“中转站”。例如,对于上述无向网,假如此时S中已有 v 0 v_{0} v0和 v 2 v_{2} v2,若要求 v 0 v_{0} v0到 v 3 v_{3} v3的最小路径,那么首先我们已有 v 0 v_{0} v0直接到 v 3 v_{3} v3的路径(也就是只有一条弧的路径) D [ 3 ] D[3] D[3],然后再将 v 0 → v 2 → v 3 v_{0}\rightarrow v_{2}\rightarrow v_{3} v0→v2→v3的路径 D [ 2 ] + a r c s [ 2 ] [ 3 ] D[2]+arcs[2][3] D[2]+arcs[2][3]与 D [ 3 ] D[3] D[3]比较,若中转后的路径比原来的路径小,则将中转后的路径替换原来的 D [ 3 ] D[3] D[3]。
- 直到所有顶点都加入到S中为止。
算法步骤:
- 首先是各数组的初始化,从源点 v 0 v_{0} v0出发,将 v 0 v_{0} v0加入到S中,并使 S [ v 0 ] = t r u e S[v_{0}]=true S[v0]=true
- 对 v 0 v_{0} v0到其余各顶点的最短路径长度初始化为权值,即 D [ i ] = G . a r c s [ v 0 ] [ v i ] D[i]=G.arcs[v_{0}][v_{i}] D[i]=G.arcs[v0][vi]
- 再判断 v 0 v_{0} v0和 v i v_{i} vi之间是否有弧,若有,则将 v i v_{i} vi的前趋置为 v 0 v_{0} v0,即 P a t h [ i ] = v 0 Path[i]=v_{0} Path[i]=v0,否则 P a t h [ i ] = − 1 Path[i]=-1 Path[i]=−1。
- 初始化结束之后,我们就该用循环结构来寻求最短路径(循环n-1次):
- 选择下一条最短路径的终点 v k v_{k} vk,将 v k v_{k} vk加入到S中,并且使 S [ v k ] = t r u e S[v_{k}]=true S[vk]=true
- 根据条件更新从 v 0 v_{0} v0出发到剩下的任一顶点(V-S中的顶点)的最短路径,若 D [ k ] + G a r c s [ k ] [ i ] < D [ i ] D[k]+Garcs[k][i]<D[i] D[k]+Garcs[k][i]<D[i]成立,则更新 D [ i ] = D [ k ] + G . a r c s [ k ] [ i ] D[i]=D[k]+G.arcs[k][i] D[i]=D[k]+G.arcs[k][i],同时更改 v i v_{i} vi的前趋为 v k v_{k} vk,即 P a t h [ i ] = k Path[i]=k Path[i]=k
有了前面的分析和步骤,下面给出具体的代码:
3. 具体代码
这里以上述的有向网G6为例(默认 v 0 → v 5 v_{0}\rightarrow v_{5} v0→v5的下标为0~5):
#define MaxInt 32767
void ShortestPath_DIJ(AMGraph G,int v0)
{
int v=0;
//初始化
for(int v=0;v<G.vexnum;v++)
{
S[v]=false;
D[v]=G.arcs[v0][v];
if(D[v]<MaxInt)
Path[v]=v0;
else
Path[v]=-1;
}
S[v0]=true;
D[v0]=0;
//开始求最短路径
for(int i=0;i<G.vexnum;i++)
{
min=MaxInt;
for(int w=0;w<G.vexnum;w++) //找出当前的最短路径
if(!S[w]&&D[w]<min)
{
v=w;
min=D[w];
}
S[v]=true;
for(int w=0;w<G.vexnum;w++) //更新D[w]
if(!S[w]&&(D[v]+G.arcs[v][w]<D[w]))
{
D[w]=G.arcs[v][w];
Path[w]=v;
}
}
}
简单概括一下:首先我们从D[i]中选出最短的路径,将该顶点加入到S中,此时S中有 v 0 , v 2 v_{0},v_{2} v0,v2(假设新加入的顶点是 v 2 v_{2} v2),然后进行D[i]更新,此时 v 2 v_{2} v2作为中转站,若经过 v 2 v_{2} v2再到 v 3 v_{3} v3(剩余顶点中的一个,这里只是举一个具体的例子,全部顶点都要进行比较)的路径比 v 0 v_{0} v0直接到 v 3 v_{3} v3的路径短,则更新D[3]。然后又从更新后的D[i]中选出路径最小的,又将该顶点(假定是 v 4 v_{4} v4)加入S中,此时S中有 v 0 , v 2 , v 4 v_{0},v_{2},v_{4} v0,v2,v4,然后又更新D[i],此时 v 4 v_{4} v4作为中转站(注意,此时D[i]中的值可能已经在 v 2 v_{2} v2中转过),假如 v 3 v_{3} v3已在 v 2 v_{2} v2中转过,那么此时D[3]的值就为路径 v 0 → v 2 → v 3 v_{0}\rightarrow v_{2}\rightarrow v_{3} v0→v2→v3的权值和,若再在 v 4 v_{4} v4中转后到 v 3 v_{3} v3的路径(即 v 0 → v 2 → v 4 → v 3 v_{0}\rightarrow v_{2}\rightarrow v_{4}\rightarrow v_{3} v0→v2→v4→v3)比D[3]小,那么就对其进行更新…
顶点 → \rightarrow →顶点(弗洛伊德算法)
求解各顶点间的最短路径在前面的基础上就比较简单了,我们可以调用n次迪杰斯特拉算法求得n个顶点分别到其余顶点的最短路径。但这个方法形式上比较复杂。下面介绍 弗洛伊德(Floyd) 算法来解决这一问题,两种算法的时间复杂度都为 O ( n 3 ) O(n^3) O(n3),但弗洛伊德算法在形式上更为简单,更容易理解。
1. 算法实现
我们仍用带权的邻接矩阵来表示有向网,同时引入一下辅助数组:
- P a t h [ i ] [ j ] : Path[i][j]: Path[i][j]:最短路径上顶点 v j v_{j} vj的前一顶点的序号。
- D [ i ] [ j ] : D[i][j]: D[i][j]:记录顶点 v i v_{i} vi和 v j v_{j} vj之间的最短路径长度。
算法分析:
- 该算法实际上十分容易理解,第一步还是先对最短路径长度数组和前趋数组进行初始化
- 初始化完成后,就是比较并更新了,对于两个不相同的顶点 v i v_{i} vi和 v j v_{j} vj,若在它们之间路径增加一个顶点 v k v_{k} vk,若 v i → v k v_{i}\rightarrow v_{k} vi→vk加上 v k → v j v_{k}\rightarrow v_{j} vk→vj的路径小于 v i → v j v_{i}\rightarrow v_{j} vi→vj的路径,则对 D [ i ] [ j ] D[i][j] D[i][j]进行更新,并更新 P a t h [ i ] [ j ] = P a t h [ k ] [ j ] Path[i][j]=Path[k][j] Path[i][j]=Path[k][j]。当在 v i v_{i} vi和 v j v_{j} vj已试过增加其余各个顶点后,那么就改变 j j j的值,再进行一轮循环增加中间顶点。当 j j j已全部试过后,就开始改变 i i i的值,综上,也就是说这是一个循环含有一个内循环和一个内循环的内循环,也就是三重循环。
2. 具体代码
void ShortestPath_Floyd(AMGraph G)
{
//初始化
for(int i=0;i<G.vexnum;i++)
for(int j=0;i<G.vexnum;j++)
{
D[i][j]=G.arcs[i][j];
if(D[i][j]<MaxInt&&i!=j) //两部相同顶点之间有弧
Path[i][j]=i;
else
Path[i][j]=-1;
}
//更新D[i][j]数组
for(int k=0;k<G.vexnum;k++)
for(int i=0;i<G.vexnum;i++)
for(int j=0;j<G.vexnum;j++)
if(D[i][k]+D[k][j]<D[i][j])
{
D[i][j]=D[i][k]+D[k][j];
Path[i][j]=Path[k][j]; //j的前趋变成k
}
}
总结
关于最短路径的两种算法,各自都有方便之处,假设我们在玩王者荣耀,每次复活后,从水晶出发,都想以最快的速度到达敌方的任一一座防御塔,那么此时我们就可以用迪杰斯特拉算法来实现这个想法。如果我们在不死亡的情况,可以出现在地图任一一座防御塔旁,此时我们需要最快地到达另一座防御塔,此时也可以用迪杰斯特拉算法,但弗洛伊德算法相较于迪杰斯特拉算法结构上更简单,更方便实现。