对于权值非负的图求解单源最短路径,第一想法是使用dijkstra算法。最短路径问题是满足最优子结构的:父问题一定会使用子问题的最优解。问题在于子问题的计算次序。dijkstra算法思想建立在我们为无负权图定义的子问题计算顺序基础上:即离源点最近点不会变成其他问题的子问题,其他问题只能成为他的子问题。
本次实验在实现dijkstra算法的基础上:
Graph.class,便于以后实验复用。本次实现额外实现部分:
- 实现基于邻接表的Graph数据结构
- 堆优化:将时间复杂度优化到 n l g ( n ) nlg(n) nlg(n)
- 输出【所有的最短路径】:需要花费额外的时间复杂度(一般的dijkstra不需要输出所有的最短路径,只需要输出一条,本实验输出了所有的最短路径)
程序如下:
//
// Created by BlancheSun on 2022/4/10.
//
#include
#include
#include
#include
using namespace std;
class ArcNode{
public:
int adjvec;
double weight;
ArcNode(int adjvec, double weight) : adjvec(adjvec), weight(weight){};
ArcNode() {};
friend bool operator<(const ArcNode& arc1, const ArcNode& arc2) {
return arc1.weight > arc2.weight;
}
};
class Graph{
private:
int vexnum; // 顶点数
int arcnum; // 边数
vector<vector<ArcNode>> arcList; // 矩阵存储结点之间的连接关系
public:
Graph(int vexnum, int arcnum);
Graph();
void add_edge(int start, int end, double weight);
void dijkstra(int source, int target);
void recordShortestPath(vector<vector<int>>& pre, int target, vector<vector<int>>& path, vector<int> temp_path);
void printAllPath(vector<vector<int>>& path, vector<double>& dis, int source, int target);
};
void Graph::add_edge(int start, int end, double weight){
arcList[start].push_back(ArcNode(end, weight));
arcList[end].push_back(ArcNode(start, weight));
}
void Graph::dijkstra(int source, int target) {
// 初始化操作
int n = this->vexnum;
int inf = 99999.0;
vector<double> dis(n, inf);
dis[source] = 0;
vector<vector<int>> pre(n,vector<int>(0));
priority_queue<ArcNode> nodeQueue;
nodeQueue.push(ArcNode(source, 0));
// 队列非空时进行检索
while(!nodeQueue.empty()) {
int node = nodeQueue.top().adjvec; // 获得目前队列中距离源点最近的结点
double weight = nodeQueue.top().weight; // 或者写成 weight = nodeQueue.top().weight
nodeQueue.pop(); // 取出队列中的元素
if(node == 3||node == 5) {
int m = 1;
}
// 从该结点出发,更新该结点能直接相连的结点
for(int i = 0; i < arcList[node].size(); i++) {
int v = arcList[node][i].adjvec;
if (weight + arcList[node][i].weight < dis[v]) { // 越界了!
dis[v] = weight + arcList[node][i].weight;
pre[v] = {node}; // 记录上一步的结点
nodeQueue.push(ArcNode(v, dis[v]));
}else if(weight + arcList[node][i].weight == dis[v]) {
pre[v].push_back(node);
}
}
}
vector<vector<int>> path; // 初始大小的为0,
vector<int> temp_path;
recordShortestPath(pre, target, path, temp_path); // 运行后path_index数值即为路径条数,不用加一,程序中已经加一
printAllPath(path, dis, source, target);
}
Graph::Graph(int vexnum, int arcnum) : vexnum(vexnum), arcnum(arcnum) {
// 做容量的初始化
vector<vector<ArcNode>> V(vexnum, vector<ArcNode>());
this->arcList = V;
}
Graph::Graph() {}
/** 第k条路径的记录 **/
void Graph::recordShortestPath(vector<vector<int>> &pre, int target, vector<vector<int>>& path, vector<int> temp_path) {
vector<int> now_pre = pre[target]; // 获得当前节点的前驱结点集合
temp_path.push_back(target);
if(now_pre.empty()) { // 没有前驱结点
path.push_back(temp_path);
return;
}
// 不为空,那么深搜前驱结点,得到路径
for(int i = 0; i < now_pre.size(); i++) {
int target2 = now_pre[i]; // 集合中的一个点
recordShortestPath(pre, target2, path, temp_path);
}
}
/** 打印所有的路径 **/
void Graph::printAllPath(vector<vector<int>> &path, vector<double>& dis, int source, int target) {
cout << "The shortest path(es) from node " << source << " to node " << target << " are/is as follows: " << endl;
int k = path.size(); // 路径的条数
for(int i = 0; i<k; i++) {
cout << "Path" << i << ": " ;
for(int j = path[i].size()-1; j > 0 ; j--) {
cout << path[i][j] << " -> ";
}
cout << path[i][0] << endl;
}
cout << "The length of path(es) is " << dis[target] <<endl;
}
int main() {
// 多条最短路径的测试
Graph graph = Graph(9, 14); // 9个顶点,14条边
// 边的添加
graph.add_edge(0,1,1);
graph.add_edge(0,7,8);
graph.add_edge(1,2,8);
graph.add_edge(1,7,11);
graph.add_edge(2,3,3);
graph.add_edge(2,5,4);
graph.add_edge(2,8,2);
graph.add_edge(3,4,9);
graph.add_edge(3,5,1);
graph.add_edge(4,5,10);
graph.add_edge(5,6,2);
graph.add_edge(6,7,1);
graph.add_edge(6,8,6);
graph.add_edge(7,8,7);
int source = 0, target = 4;
graph.dijkstra(source, target);
// Graph graph = Graph(16, 30); // 16个顶点,30条边
//
// // 边的添加
// graph.add_edge(0,2,3);
// graph.add_edge(1,3,1);
// graph.add_edge(1,4,3);
// graph.add_edge(1,5,6);
// graph.add_edge(2,4,8);
// graph.add_edge(2,5,7);
// graph.add_edge(2,6,6);
// graph.add_edge(3,7,6);
// graph.add_edge(3,8,8);
// graph.add_edge(4,7,3);
// graph.add_edge(4,8,5);
// graph.add_edge(5,8,3);
// graph.add_edge(5,9,3);
// graph.add_edge(6,8,8);
// graph.add_edge(6,9,4);
// graph.add_edge(7,10,2);
// graph.add_edge(7,11,2);
// graph.add_edge(8,11,1);
// graph.add_edge(8,12,2);
// graph.add_edge(9,11,3);
// graph.add_edge(9,12,3);
// graph.add_edge(10,13,3);
// graph.add_edge(10,14,5);
// graph.add_edge(11,13,5);
// graph.add_edge(11,14,2);
// graph.add_edge(12,13,6);
// graph.add_edge(12,14,6);
// graph.add_edge(13,15,4);
// graph.add_edge(14,15,3);
//
//
// // 求解最短路径
// int source = 0, target = 15;
// graph.dijkstra(source, target);
return 0;
}
以下图为例进行执行

结果如下:可见所有的最短路径都被输出

基础的邻接矩阵也能作为dijkstra算法的数据结构,但是对于稀疏图或者结点很多的情况下输入数据较为复杂且容易超出内存。本次实验使用邻接表实现图的数据结构,主要设置如下:
ArcNode.class:邻接表的表结点。含有表结点的终点编号adjvec和边权重weight两个属性。
vector:
ArcList[i]表示结点i为起点的所有表结点的集合Graph.class:
含有三个属性:
vexnum:结点数arcnum:边数vector> ArcList :邻接表含有四个方法:
add_edge(int start, int end):向邻接表中增加表结点
dijkstra(int source, int target):计算最短路径和所有结点的前驱结点集合,以求得所有最短路径
recordShortestPath(vector:根据dijksra中的前驱结点集合pre,进行深度优先搜索,得到所有的最短路径,存入path数组中。
printAllPath(vector:打印所有的最短路径
dikstra算法的核心过程很简单,主要特点是使用广度优先搜索。算法主要过程如下:
{
v
i
s
i
t
e
d
[
s
j
]
=
=
f
a
l
s
e
:
s
j
为被用来松弛过
d
i
s
[
s
k
]
+
w
e
i
g
h
t
(
s
k
,
s
j
)
<
d
i
s
[
s
j
]
满足条件即更新 d i s [ s j ] = d i s [ s k ] + w e i g h t ( s k , s j ) dis[s_j] = dis[s_k]+weight(s_k,s_j) dis[sj]=dis[sk]+weight(sk,sj)。更新完后将 s k s_k sk加入集合T,从U中移出 s k s_k sk。
从动态规划的角度看,上式也可以理解成动态规划的递推式。
在(1)的步骤二循环n轮中,每次循环需要寻找距离源点 s 0 s_0 s0最近的结点。若是采取循环扫描一遍的过程,那么dijkstra算法的时间复杂度将会为 O ( n 2 ) O(n^2) O(n2),仔细分析,该过程寻找最近距离的过程有下面的特点:
符合这样的特点我们可以用堆进行优化。即:
top()和pop()操作代替。同时
v
i
s
t
e
d
visted
visted数组变得不再需要,因为该结点被
p
o
p
(
)
pop()
pop(),下轮更新队列中将不会存在该结点。如果只需要记录一条最短路径,那么我们在进行松弛操作后用一个数组
p
r
e
[
n
]
pre[n]
pre[n]记住用于松弛该结点的
s
k
s_k
sk即可。即:
p
r
e
[
s
j
]
=
s
k
pre[s_j] = s_k
pre[sj]=sk
我们这里实现了多条最短路径的输出,一维数组不再能满足我们的要求,我们需要一个二维数组
p
r
e
[
n
]
[
m
]
pre[n][m]
pre[n][m]记录所有的前驱结点。对于一个结点编号为
i
i
i的结点,其所有的前驱为
p
r
e
[
i
]
pre[i]
pre[i]集合中的所有元素。当然,如果最短路径只有一条,那么
p
r
e
[
i
]
pre[i]
pre[i]中只有一个元素。
另外,需要存储多条最短路径后,在松弛时进行的操作和只输出一条路径时不相同:
若 d i s [ s k ] + w e i g h t ( s k , s j ) < d i s [ s j ] dis[s_k] + weight(s_k,s_j) < dis[s_j] dis[sk]+weight(sk,sj)<dis[sj]:
直接将 p r e [ s j ] pre[s_j] pre[sj]置为 s k s_k sk的编号,然后将队列中修改 s j s_j sj的权重为 d i s [ s k ] + w e i g h t ( s k , s j ) dis[s_k] + weight(s_k,s_j) dis[sk]+weight(sk,sj),更新 d i s [ s j ] = d i s [ s k ] + w e i g h t ( s k , s j ) dis[s_j] = dis[s_k] + weight(s_k,s_j) dis[sj]=dis[sk]+weight(sk,sj)
若 d i s [ s k ] + w e i g h t ( s k , s j ) = = d i s [ s j ] dis[s_k] + weight(s_k,s_j) == dis[s_j] dis[sk]+weight(sk,sj)==dis[sj]
向 p r e [ s j ] pre[s_j] pre[sj]中增加 s k s_k sk的编号
按照上述的方式进行记录后,我们最终得到的 p r e pre pre数组,为了便于说明,我们给出下面的一个例子:

以图中的结点4为例,我们给出他的 p r e pre pre数组树:

根据这棵 p r e pre pre数组树,我们能够通过深度优先遍历的方式得到从结点4开始到源点0的所有的最短路径。在深搜过程中我们每得到一条路径就应该将他存在一个二维的 p a t h path path数组,每一维存储一条路径。在得到所有最短路径后,遍历 p a t h path path数组的每个元素,每一个元素即是一条路径的路径数组,逐条打印出路径结果即可。