单源最短路径Dijkstra算法升级:出现多条最短路径,输出之?

引言:单条路径的标配算法

一个普通、标配、差强人意的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]这个一位数组里再添上一个新的成员。

这里强烈推荐动态数组的原因是:

  1. P[]数组里每个一维数组的长度,都是不一样的,如果是普通数组,那么对P[j]添加成员时,需要先求出当前数组的长度,或者将数组的下标0位置空出,已存储当前的数组长度。
  2. 每个结点所在最短支路的条数,相差甚大:有的处于枢纽结点,有10+条最短路径,有的只是过渡结点,只有一条最短路径。如果采用静态数组统一分配空间,会造成普遍的浪费现象。
  3. 如果是动态数组,就不必考虑这些问题,可以直接使用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);
}
  • 9
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

弦乐四重奏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值