引言:单条路径的标配算法
一个普通、标配、差强人意的Dijkstra
算法包含以下过程(任意数据结构教材均有售):
// Dijkstra:
//
// 图采用存储结构:邻接矩阵
int G[MAX_NODES][MAX_NODES]; // G[i][j],表示i到j有路径,其值为路径长度。没有路径则设为INF
int N; //结点个数
int D[MAX_NODES]; //表示从源点v0开始,到各个结点的路径长度
int P[MAX_NODES]; //用来描述路径。P[j] = i,即表示:经过i才能到达j。
bool finale[MAX_NODES]; //finale[i] 为 true 则表示通往i结点的最短路径已经确定,加入了最短路径集合。
// 算法主体
void DIJ(int N, int v0, int D[], int P[], bool finale[]) {
int v, w; //循环指针
for (v=0; v<N; ++v) { // 初始化过程
D[v] = G[v0][v]; P[v] = v0; finale[v] = false;
}
finale[v0] = true; D[v0] = 0; P[v0] = -1; //对源点无需求最短路径
for (int i=1; i<N; ++i) { //主循环,共循环N-1次
int min = INF;
for (w=0; w<N; ++w)
if (finale[w] == false && D[w] < min)
{v = w; min = D[w];}
finale[v] = true; //当前最近结点加入集合。
for (w=0; w<N; ++w) {
if (finale[w] == false && min + G[v][w] < D[w]) {
// 更新最短路径
D[w] = min + G[v][w];
P[w] = v;
}
}
}
}
// end
输出路径的算法:
// 这里要用到上面的P数组, 起点v0,终点v
// 因为P数组是倒着存放路径的,所以需要反向输出,即利用栈的特性。
void print_path(int v0, int v, int P[]) {
STACK st;
int c = v; st.push(v);
while(c != v0) {
c = P[c];
st.push(c);
}
while(st不空) {
printf("%d", st.top());
st.pop();
}
}
然而,很多时候我们面对的不仅仅只有“最短路径”。客户有时需要我们给出多条代价相近且最小的方案。比如下面这张图:
这里每一条start -> end
的路径都是最短路径。下面就介绍如何应对这一情形。
进阶:记录多条路径的改进版Dijkstra算法:
我们基于之前所给出的算法,其中有一个P[]
数组,其用来记录路径Path。
P[j] = i
,当且仅当:弧(i, j)在j的最短路径上,即最短路径上j的前一个结点是i
。
1. 对P[]
数组进行扩充
现在,将P扩充为一个二维数组。P[j]
存储的不再是一个数,而是一个新的数组,在这个数组里存储的每一个数,都是j
的某一条最短路径的直接前驱i
。
P数组现在的定义如下:
#define MAX_NODES ...
#define MAX_PATHS ...
int P[MAX_NODES][MAX_PATHS];
// int P[MAX_NODES]; //以前的P数组,只能存储一条最短路径
或者是动态数组:(强烈推荐!!)
#include <vector>
vector<int> P[MAX_NODES];
这时,每当通往j
有一条新的最短路径时,我们都在P[j]
这个一位数组里再添上一个新的成员。
这里强烈推荐动态数组的原因是:
P[]
数组里每个一维数组的长度,都是不一样的,如果是普通数组,那么对P[j]
添加成员时,需要先求出当前数组的长度,或者将数组的下标0
位置空出,已存储当前的数组长度。- 每个结点所在最短支路的条数,相差甚大:有的处于枢纽结点,有10+条最短路径,有的只是过渡结点,只有一条最短路径。如果采用静态数组统一分配空间,会造成普遍的浪费现象。
- 如果是动态数组,就不必考虑这些问题,可以直接使用
push_back
或者是append
方法。
此时,算法中初始化数组、以及更新最短路径的那部分代码,应该修改如下:
P[v] = v0
,修改为:P[v].clear(); P[v].push_back(v0);
P[v0] = -1
,修改为:P[v].clear();
P[w] = v
,修改为:P[w].clear(); P[w].push_back(v);
同时还要增加下列语句(最短路径相等):
if (finale[w] == false && min + G[v][w] == D[w]) {
D[w] = min + G[v][w]; P[w].push_back(v); }
除了最短路径相等的情况,其余每次更新路径都必须清空一维数组clear()
,原因在于每次更新最短路径后,之前的所有路径,就成为错误的路径了。
最后的算法如下:
// Dijkstra: 加强版
//
// 图采用存储结构:邻接矩阵
int G[MAX_NODES][MAX_NODES]; // G[i][j],表示i到j有路径,其值为路径长度。没有路径则设为INF
int N; //结点个数
int D[MAX_NODES]; //表示从源点v0开始,到各个结点的路径长度
vector<int> P[MAX_NODES]; //用来描述路径。P[j] 包含了 i,即表示:
//至少存在一条最短路径,使得经过i才能到达j。
bool finale[MAX_NODES]; //finale[i] 为 true 则表示通往i结点的最短路径已经确定,加入了最短路径集合。
// 算法主体
void DIJ(int N, int v0, int D[], vector<int> P[], bool finale[]) {
int v, w; //循环指针
for (v=0; v<N; ++v) { // 初始化过程
D[v] = G[v0][v]; P[v].clear(); P[v].push_back(v0); finale[v] = false;
}
finale[v0] = true; D[v0] = 0; P[v0].clear(); //对源点无需求最短路径
for (int i=1; i<N; ++i) { //主循环,共循环N-1次
int min = INF;
for (w=0; w<N; ++w)
if (finale[w] == false && D[w] < min)
{v = w; min = D[w];}
finale[v] = true; //当前最近结点加入集合。
for (w=0; w<N; ++w) {
if (finale[w] == false && min + G[v][w] < D[w]) {
// 更新最短路径
D[w] = min + G[v][w];
P[w].clear(); P[w].push_back(v);
} else
if (finale[w] == false && min + G[v][w] == D[w]) {
//增添新的最短路径
D[w] = min + G[v][w];
P[w].push_back(v);
}
}
}
}
// end
2. 求最短路径条数
求最短路径的条数,就是从终点end
往回返,数组P[end]
的长度,就是最终通往end
的最短路径条数,然后再依次遍历end
的所有最短路径的直接前驱,以递归的方式,求各个中间结点的最短路径的支路的条数,直到回到起点v0
:
// 求以v0为起点,o为终点,的最短路径的条数:get_paths_num
int get_paths_num(int v0, int o, vector<int> P[]) {
if (o == v0) return 1; // 递归终止的条件: 起点==终点
int count = 0;
for (int i = 0; i < P[o].size(); ++i) //遍历终点的直接前驱,支路条数累加起来
count += get_paths_num(v0, P[o][i], P);
return count;
}
3. 输出所有最短路径
单条最短路径算法,利用栈结构来输出路径,这是因为P[]
数组是从终点开始,存储最短路径上每一个结点的直接前驱。也可以不使用栈的结构,而是利用reverse
将字符串或者容器进行倒序。
对于加强后的多条最短路径算法,P[]
数组这个特性并没有发生变化,所以利用递归的思想,其输出算法大同小异。
我们建立一个字符串数组,每当一轮递归结束,都把生成的路径字符串在屏幕上打印、换行,并推入数组:
vector<int> P[MAX_NODES];
vector<string> V;
// 函数功能:打印并保存 v0->o 的所有最短路径
// cur字符串作递归辅助用,首次调用务必使cur为空串
void print_path(int v0, int o, string cur = "") {
cur += o + '0';
if (o == v0) { //退出递归的条件:起点==终点,输出路径,保存至V数组
reverse(cur.begin(), cur.end());
cout << cur << endl;
V.push_back(cur);
return;
}
for (int i = 0; i < P[o].size(); ++i)
print_path(v0, P[o][i], cur);
}