最短路径的概念
在一个无权图中,若从一个顶点到另一个顶点存在着一条路径(仅限于无回路的简单路径),则该路径上的边数即为路径长度,等于该路径上的顶点数减一。
从一个顶点到另一个顶点所有可能的路径中,路径长度最短(即经过的边数最少)的路径称为最短路径,其路径长度叫做最短路径长度或最短距离。
在一个带权图中,从一个顶点v到另一个顶点u的路径上所经过各边上的权值之和即为该路径的带权路径长度,从v到u可能不止一条路径,把带权路径长度最短(即其值最小)的那条路径称作是最短路径,其权值之和称作最短路径长度或最短距离。
求单源最短路径的Dijkstra算法
Dijstra算法用于求图中一个顶点到其余各顶点的最短路径。
算法设置
- 一个集合 S S S,记录已经求的最短路径的顶点;
- 一个辅助数组 d i s t [ ] dist[] dist[], d i s t [ i ] dist[i] dist[i]存放集合 S S S内顶点到集合 S S S外顶点 i i i的最短距离,即源点到其他各顶点的当前最短路径长度。
- 一个辅助数组
p
a
t
h
[
]
path[]
path[],
p
a
t
h
[
i
]
path[i]
path[i]记录集合
S
S
S外顶点
i
i
i距离集合
S
S
S内哪个顶点最近,即源点到顶点
i
i
i之间的最短路径的前驱结点,在算法结束时,可以根据其值追溯得到源点到顶点
v
i
v_i
vi的最短路径。
算法步骤:
- 假设从选择从顶点0出发,即 u u u=0,则S内最初只有一个顶点0, S [ 0 ] S[0] S[0] = 1。
- 再令 p a t h [ 0 ] path[0] path[0]= -1,其他 p a t h [ i ] path[i] path[i]= 0,表示集合S外各个顶点 i i i距离集合 S S S内顶点0最近。
- 数组
d
i
s
t
[
i
]
dist[i]
dist[i] = G.Edge
[
0
]
[
i
]
[0][i]
[0][i],表示源点0(集合内顶点)到顶点
i
i
i的最短距离。如果
d
i
s
t
[
i
]
dist[i]
dist[i] =
m
a
x
w
e
i
g
h
t
maxweight
maxweight,则表示没有到顶点
i
i
i的路径。然后重复以下工作:
- 在 d i s t [ ] dist[] dist[]中选择满足 S [ i ] S[i] S[i] = 0的 d i s t [ i ] dist[i] dist[i]最小的顶点 i i i,用 v v v标记它。则选中的路径长度最短的边为< p a t h [ v ] , v path[v], v path[v],v>,相应的最短路径为 d i s t [ v ] dist[v] dist[v]。
- 让 S [ v ] S[v] S[v] = 1,表示它已经加入集合 S S S。
- 取
d
i
s
t
[
i
]
=
m
i
n
{
d
i
s
t
[
i
]
,
d
i
s
t
[
v
]
dist[i] = min\{dist[i], dist[v]
dist[i]=min{dist[i],dist[v] + G.Edge
[
v
]
[
i
]
}
[v][i]\}
[v][i]}。即检查集合
S
S
S外各顶点
i
i
i,如果绕过顶点v到顶点i的距离
d
i
s
t
[
v
]
dist[v]
dist[v] + G.Edge
[
v
]
[
i
]
[v][i]
[v][i]比原来集合
S
S
S中顶点到顶点
i
i
i的的最短距离
d
i
s
t
[
i
]
dist[i]
dist[i]还要小,则修改到顶点
i
i
i的最短距离为
d
i
s
t
[
v
]
dist[v]
dist[v] + G.Edge
[
v
]
[
i
]
[v][i]
[v][i],同时修改
p
a
t
h
[
i
]
path[i]
path[i] =
v
v
v,表示集合
S
S
S内顶到
v
v
v到集合外顶点
i
i
i的当前距离最近。
代码实现:
void Dijkatra(MGraph &G, int u, int dist[], int path[]){
int S[maxvertexnum]; //S为已求最短路径的顶点集合
for(int i=0; i<G.vexnum; i++)S[i]=0; //集合初始化
S[u] = 1; //起始点进集合
for(int i=0; i<G.vexnum; i++){ //dist与path数组初始化
dist[i] = G.Edge[u][i]; //表示源点u到顶点i的最短距离
if(u!=i && dist[i]<maxweight) //源点u到i有路径
path[i] = u; //i的前驱结点为u
else
path[i] = -1; //否则i没有前驱结点
}
for(int i=0; i<G.vexnum; i++){ //对所有的顶点处理一次
if(i != u){ //排除源点
int min = maxweight; //选不属于S且具有最短路径的顶点v
int v = u;
for(int j=0; j<G.vexnum; j++){
if(!S[j] && dist[j]<min){ //找最短路径,就是找dist[i]的最小值
v = j; //取最小值的顶点下标
min = dist[j]; //最小值
}
}
S[v] = 1; //将顶点v加入集合
for(int j=0; j<G.vexnum; j++){
//如果源点到顶点j的距离 > 源点经过顶点v再到达j的距离
if(!S[j] && dist[j] > dist[v]+G.Edge[v][j]){
dist[j] = G.Edge[v][j] + dist[v]; //更新最小距离和前驱结点
path[j] = v; //j的前驱结点为v
}
}
}
}
}
输出最短路径及路径长度
void PrintPath(MGraph &G, int u, int dist[], int path[]){
cout<<"各顶点到顶点"<<u<<"的最短路径为:"<<endl;
for(int i=0; i<G.vexnum; i++){
int j=i;
while(j!=u){
cout<<G.Vertex[j]<<" ";
if(j!=u) j=path[j];
}
cout<<G.Vertex[j]<<" dist="<<dist[i]<<endl;
}
}
对于下图,运行结果为:
需要注意的是,边上带有负权值时,Dijkstra算法并不适用。
求各个顶点之间最短路径的Floyd算法
Floyd算法用于求所有顶点之间的最短路径。
求所有顶点之间的最短路径问题的提法是:已知一个带权有向图,对每一对顶点
v
i
≠
v
j
v_i≠v_j
vi=vj,要求求出
v
i
v_i
vi与
v
j
v_j
vj之间的最短路径和最短路径长度。
Floyd算法的基本思想:
设置一个
n
×
n
n×n
n×n的方阵
A
(
k
)
A^{(k)}
A(k),其中除对角线的元素都等于0外,其他元素
a
(
k
)
[
i
]
[
j
]
(
i
≠
j
)
a^{(k)}[i][j](i≠j)
a(k)[i][j](i=j)表示从顶点
v
i
v_i
vi到顶点
v
j
v_j
vj的路径长度,
k
k
k表示绕行第
k
k
k个顶点的运算步骤。
算法步骤:
<1>初始时,对于任意两个顶点
v
i
v_i
vi和
v
j
v_j
vj,若它们之间存在边,则以此边上的权值作为它们之间的最短路径长度;若它们之间不存在有向边,则以maxweight(机器可表示的在问题中不会遇到的最大数,表示∞)作为它们之间的最短路径长度。
<2>以后逐步尝试在原路径上加入顶点
k
(
k
=
0
,
1
,
…
…
,
n
−
1
)
k(k=0,1,……,n-1)
k(k=0,1,……,n−1)作为中间顶点。如果增加中间顶点后得到的路径比原来的路径长度减少了,则以此新路径代替原路径。
代码实现:
void Floyd(MGraph &G, int A[][maxvertexnum], int P[][maxvertexnum]){
//A[i][j]是顶点i和顶点j之间的最短路径长度
//初始化
for(int i=0; i<G.vexnum; i++){
for(int j=0; j<G.vexnum; j++){
A[i][j] = G.Edge[i][j]; //A矩阵初始时其实是邻接矩阵
P[i][j] = j; //记录路径的数组,记录对应点的最小路径的前驱点
}
}
//对每一个k,产生A(k)
for(int k=0; k<G.vexnum; k++){
for(int i=0; i<G.vexnum; i++){
if(i != k){
for(int j=0; j<G.vexnum; j++){
if(j!=k && A[i][k]+A[k][j] < A[i][j]){
A[i][j] = A[i][k]+A[k][j];
P[i][j] = P[i][k];
}
}
}
}
}
}
输出最短路径及路径长度:
void PrintPath(MGraph &G, int A[][maxvertexnum], int P[][maxvertexnum]){
for(int i=0; i<G.vexnum; i++){
for(int j=0; j<G.vexnum; j++){
if(i!=j){
cout<<i<<"->"<<j<<"的路径:";
int k = P[i][j];
cout<<i<<"->";
while(k!=j){
cout<<k<<"->";
k = P[k][j];
}
cout<<j<<setw(10)<<",dist = "<<A[i][j]<<endl;
cout<<endl;
}
}
}
}
输入及运行结果:
需要注意的是,Floyd算法允许图中有带负权值的边,但不允许有包含带负权值的边组成的回路。
完整代码
关于Dijkstra算法与Floyd算法的完整运行代码,由于文章篇幅原因不再给出,可到我的资源中免费下载:最短路径的Dijkstra算法及Floyd算法 。