改进迪杰斯特拉算法(dijkstra):输出所有最短路径
对于权值非负的图求解单源最短路径,第一想法是使用dijkstra算法。最短路径问题是满足最优子结构的:父问题一定会使用子问题的最优解。问题在于子问题的计算次序。dijkstra算法思想建立在我们为无负权图定义的子问题计算顺序基础上:即离源点最近点不会变成其他问题的子问题,其他问题只能成为他的子问题。
本次实验在实现dijkstra算法的基础上:
- 构建基于邻接表的图类:
Graph.class
,便于以后实验复用。 - 此外加入了优先队列进行优化
- 不仅实现对最优解(最短路径)的记录而且对所有的最优解(所有的最短路径)进行输出。
本次实现额外实现部分:
- 实现基于邻接表的Graph数据结构
- 堆优化:将时间复杂度优化到 n l g ( n ) nlg(n) nlg(n)
- 输出【所有的最短路径】:需要花费额外的时间复杂度(一般的dijkstra不需要输出所有的最短路径,只需要输出一条,本实验输出了所有的最短路径)
程序如下:
//
// Created by BlancheSun on 2022/4/10.
//
#include<iostream>
#include<algorithm>
#include <vector>
#include <queue>
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;
}
以下图为例进行执行
结果如下:可见所有的最短路径都被输出
1. 基于邻接表的Graph
基础的邻接矩阵也能作为dijkstra算法的数据结构,但是对于稀疏图或者结点很多的情况下输入数据较为复杂且容易超出内存。本次实验使用邻接表实现图的数据结构,主要设置如下:
-
ArcNode.class
:邻接表的表结点。含有表结点的终点编号adjvec
和边权重weight
两个属性。 -
vector<vector<ArcNode>> ArcList
:-
ArcList[i]
表示结点i
为起点的所有表结点的集合
-
-
Graph.class
:含有三个属性:
-
vexnum
:结点数 -
arcnum
:边数 -
vector<vector<ArcNode>> ArcList
:邻接表
含有四个方法:
-
add_edge(int start, int end)
:向邻接表中增加表结点 -
dijkstra(int source, int target)
:计算最短路径和所有结点的前驱结点集合,以求得所有最短路径 -
recordShortestPath(vector<vector<int>> &pre, vector<vector<int>>& path
:根据dijksra中的前驱结点集合pre
,进行深度优先搜索,得到所有的最短路径,存入path数组中。 -
printAllPath(vector<vector<int>> &path)
:打印所有的最短路径
-
2 基本算法思路说明与堆优化
(1)基本思路
dikstra算法的核心过程很简单,主要特点是使用广度优先搜索。算法主要过程如下:
- 初始化:
- 集合初始化:设置已松弛集合 T = ∅ T = ∅ T=∅,和未松弛集合 U = { s 0 , s 1 . . . , s n } U = \{s_0,s_1...,s_n\} U={s0,s1...,sn}全部n个结点。
- 距离数组初始化:设置有大小为结点个数n的距离数组 v e c t o r < i n t > d i s ( n , + ∞ ) vector<int>\ dis(n, +\infty) vector<int> dis(n,+∞),另外将 d i s [ s 0 ] dis[s_0] dis[s0]初始化为0。
- 循环n轮:每次扩展扩展使用不在集合 T T T中(在集合 U U U中),且距离源点 s 0 s_0 s0最近的结点 s k s_k sk对所有与 s k s_k sk直接相联的结点 s j s_j sj进行松弛,第一轮时即使用 s 0 s_0 s0进行松弛。松弛条件为:
{ 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 ] \begin{cases}visited[s_j]==false:s_j为被用来松弛过\\ dis[s_k] + weight(s_k,s_j) < dis[s_j]\end{cases} {visited[sj]==false:sj为被用来松弛过dis[sk]+weight(sk,sj)<dis[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)。更新完后将 s k s_k sk加入集合T,从U中移出 s k s_k sk。
从动态规划的角度看,上式也可以理解成动态规划的递推式。
- 循环结束:每个结点都被加入 T T T,此时的 d i s [ n ] dis[n] dis[n]即为最短距离。
(2)堆优化
在(1)的步骤二循环n轮中,每次循环需要寻找距离源点 s 0 s_0 s0最近的结点。若是采取循环扫描一遍的过程,那么dijkstra算法的时间复杂度将会为 O ( n 2 ) O(n^2) O(n2),仔细分析,该过程寻找最近距离的过程有下面的特点:
- 集合大小是动态变化的结点
- 每次从集合中寻找的距离最小的元素,不需要所有的元素有序
符合这样的特点我们可以用堆进行优化。即:
- ① 将遍历
d
i
s
[
s
k
]
dis[s_k]
dis[sk]寻找最小值的过程使用一个优先队列
n
o
d
e
Q
u
e
u
e
nodeQueue
nodeQueue的
top()
和pop()
操作代替。同时 v i s t e d visted visted数组变得不再需要,因为该结点被 p o p ( ) pop() pop(),下轮更新队列中将不会存在该结点。 - ② 每次对松弛对 d i s [ v ] dis[v] dis[v]进行操作时,同时需要向优先队列中减小该结点的 w e i g h t weight weight。
3 实现多条最短路径记录
3.5.3.1 路径的记录
如果只需要记录一条最短路径,那么我们在进行松弛操作后用一个数组
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的编号
3.1 路径的输出
按照上述的方式进行记录后,我们最终得到的 p r e pre pre数组,为了便于说明,我们给出下面的一个例子:
以图中的结点4为例,我们给出他的 p r e pre pre数组树:
文章来源:https://www.toymoban.com/news/detail-403408.html
根据这棵 p r e pre pre数组树,我们能够通过深度优先遍历的方式得到从结点4开始到源点0的所有的最短路径。在深搜过程中我们每得到一条路径就应该将他存在一个二维的 p a t h path path数组,每一维存储一条路径。在得到所有最短路径后,遍历 p a t h path path数组的每个元素,每一个元素即是一条路径的路径数组,逐条打印出路径结果即可。文章来源地址https://www.toymoban.com/news/detail-403408.html
到了这里,关于dijkstra算法:堆优化 + 输出所有最短路径(得到所有最优解)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!