Dijkstra算法
Dijkstra算法
之前写了一篇例题的思考,半夜总觉得写的是个P,我自己都看不懂,所以大早上起来优化一下:-);
先给出一个我觉得好理解的模板:尽我所能的给出了注释来帮助理解(初始化一个啥样的邻接矩阵让人痛苦:(
#include<iostream>
#include<algorithm>
using namespace std;
#define N 2021
//Dijkstra模板
//邻接矩阵 起点到目标距离 是否走过
int group[N][N];int startToDist[N];int visited[N];
//Dijkstra算法
int dijkstra(int start) {
//初始化起点到Dist距离 因为要求最短路径 所以初值全部设为最大值
memset(startToDist, 0x3f, sizeof(startToDist));
//起点到自己的距离为0
startToDist[start] = 0;
//依次选择节点
for (int i = 1;i < N;i++) {
int t = -1;
//挑选出一个没有访问过 同时到start距离最短的点 第一个应该是start自身 因为距离为0
for (int j = 1;j < N;j++) {
if (!visited[j] && (t == -1 || startToDist[j] < startToDist[t])) {
t = j;
}
}
visited[t] = 1;//记录为走过
//更新距离 第一个遍历应该是求start到所有的邻接点距离 简单来说就是更新出所有直接连着start的点
for (int j = 1;j < N;j++) {
startToDist[j] = min(startToDist[j], startToDist[t] + group[t][j]);
}
}
return startToDist[2020];//返回起点到2020的最短路径 根据需要调整就好 因为startToDist数组中是start到每个点的最短路径
}
//初始化表格需要 用的时候按照需求
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
int lcm(int a, int b) {
return a * b / gcd(a, b);
}
int main() {
//初始化表格 按照要求初始化 这里随便设计一下
for (int i = 1;i < N;i++) {
for (int j = 1;j < N;j++) {
if (i != j) {
if (fabs(i - j) <= 21) {//fabs()绝对值
group[i][j] = lcm(i, j);
group[j][i] = lcm(i, j);
}
else {
group[i][j] = 0x3f3f3f3f;
group[j][i] = 0x3f3f3f3f;
}
}
}
}
int s;
cin >> s;
cout << dijkstra(s);
system("pause");
return 0;
}
大部分Dijkstra算法都是带着dp table也就是备忘录数组的,但是在学习《labuladong的算法小抄》的过程中,学习到了另一种不带visited数组的写法。
伪代码:
// 返回节点 from 到节点 to 之间的边的权重
int weight(int from, int to);
// 输入节点 s 返回 s 的相邻节点
List<Integer> adj(int s);
// 输入一幅图和一个起点 start,计算 start 到其他节点的最短距离
int[] dijkstra(int start, List<Integer>[] graph) {
// 图中节点的个数
int V = graph.length;
// 记录最短路径的权重,你可以理解为 dp table
// 定义:distTo[i] 的值就是节点 start 到达节点 i 的最短路径权重
int[] distTo = new int[V];
// 求最小值,所以 dp table 初始化为正无穷
Arrays.fill(distTo, Integer.MAX_VALUE);
// base case,start 到 start 的最短距离就是 0
distTo[start] = 0;
// 优先级队列,distFromStart 较小的排在前面
Queue<State> pq = new PriorityQueue<>((a, b) -> {
return a.distFromStart - b.distFromStart;
});
// 从起点 start 开始进行 BFS
pq.offer(new State(start, 0));
while (!pq.isEmpty()) {
State curState = pq.poll();
int curNodeID = curState.id;
int curDistFromStart = curState.distFromStart;
if (curDistFromStart > distTo[curNodeID]) {
// 已经有一条更短的路径到达 curNode 节点了
continue;
}
// 将 curNode 的相邻节点装入队列
for (int nextNodeID : adj(curNodeID)) {
// 看看从 curNode 达到 nextNode 的距离是否会更短
int distToNextNode = distTo[curNodeID] + weight(curNodeID, nextNodeID);
if (distTo[nextNodeID] > distToNextNode) {
// 更新 dp table
distTo[nextNodeID] = distToNextNode;
// 将这个节点以及距离放入队列
pq.offer(new State(nextNodeID, distToNextNode));
}
}
}
return distTo;
}
对比普通的 BFS 算法,你可能会有以下疑问:
1、没有 visited
集合记录已访问的节点,所以一个节点会被访问多次,会被多次加入队列,那会不会导致队列永远不为空,造成死循环?
2、为什么用优先级队列 PriorityQueue
而不是 LinkedList
实现的普通队列?为什么要按照 distFromStart
的值来排序?
3、如果我只想计算起点 start
到某一个终点 end
的最短路径,是否可以修改算法,提升一些效率?
我们先回答第一个问题,为什么这个算法不用 visited
集合也不会死循环。
对于这类问题,我教你一个思考方法:
循环结束的条件是队列为空,那么你就要注意看什么时候往队列里放元素(调用 offer
)方法,再注意看什么时候从队列往外拿元素(调用 poll
方法)。
while
循环每执行一次,都会往外拿一个元素,但想往队列里放元素,可就有很多限制了,必须满足下面这个条件:
// 看看从 curNode 达到 nextNode 的距离是否会更短
if (distTo[nextNodeID] > distToNextNode) {
// 更新 dp table
distTo[nextNodeID] = distToNextNode;
pq.offer(new State(nextNodeID, distToNextNode));
}
这也是为什么我说 distTo
数组可以理解成我们熟悉的 dp table,因为这个算法逻辑就是在不断的最小化 distTo
数组中的元素:
如果你能让到达 nextNodeID
的距离更短,那就更新 distTo[nextNodeID]
的值,让你入队,否则的话对不起,不让入队。
因为两个节点之间的最短距离(路径权重)肯定是一个确定的值,不可能无限减小下去,所以队列一定会空,队列空了之后,distTo
数组中记录的就是从 start
到其他节点的最短距离。
接下来解答第二个问题,为什么要用 PriorityQueue
而不是 LinkedList
实现的普通队列?
如果你非要用普通队列,其实也没问题的,你可以直接把 PriorityQueue
改成 LinkedList
,也能得到正确答案,但是效率会低很多。
Dijkstra 算法使用优先级队列,主要是为了效率上的优化,类似一种贪心算法的思路。
为什么说是一种贪心思路呢,比如说下面这种情况,你想计算从起点 start
到终点 end
的最短路径权重:
假设你当前只遍历了图中的这几个节点,那么你下一步准备遍历那个节点?这三条路径都可能成为最短路径的一部分,但你觉得哪条路径更有「潜力」成为最短路径中的一部分?
从目前的情况来看,显然橙色路径的可能性更大嘛,所以我们希望节点 2
排在队列靠前的位置,优先被拿出来向后遍历。
所以我们使用 PriorityQueue
作为队列,让 distFromStart
的值较小的节点排在前面,这就类似我们之前讲 贪心算法 说到的贪心思路,可以很大程度上优化算法的效率。
大家应该听过 Bellman-Ford 算法,这个算法是一种更通用的最短路径算法,因为它可以处理带有负权重边的图,Bellman-Ford 算法逻辑和 Dijkstra 算法非常类似,用到的就是普通队列,本文就提一句,后面有空再具体写。
接下来说第三个问题,如果只关心起点 start
到某一个终点 end
的最短路径,是否可以修改代码提升算法效率。
肯定可以的,因为我们标准 Dijkstra 算法会算出 start
到所有其他节点的最短路径,你只想计算到 end
的最短路径,相当于减少计算量,当然可以提升效率。
需要在代码中做的修改也非常少,只要改改函数签名,再加个 if 判断就行了:
// 输入起点 start 和终点 end,计算起点到终点的最短距离
int dijkstra(int start, int end, List<Integer>[] graph) {
// ...
while (!pq.isEmpty()) {
State curState = pq.poll();
int curNodeID = curState.id;
int curDistFromStart = curState.distFromStart;
// 在这里加一个判断就行了,其他代码不用改
if (curNodeID == end) {
return curDistFromStart;
}
if (curDistFromStart > distTo[curNodeID]) {
continue;
}
// ...
}
// 如果运行到这里,说明从 start 无法走到 end
return Integer.MAX_VALUE;
}
因为优先级队列自动排序的性质,每次从队列里面拿出来的都是 distFromStart
值最小的,所以当你第一次从队列中拿出终点 end
时,此时的 distFromStart
对应的值就是从 start
到 end
的最短距离。
这个算法较之前的实现提前 return 了,所以效率有一定的提高。
本文来自博客园,作者:{BailanZ},转载请注明原文链接:https://www.cnblogs.com/BailanZ/p/16115517.html