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 的最短路径权重:

img

假设你当前只遍历了图中的这几个节点,那么你下一步准备遍历那个节点?这三条路径都可能成为最短路径的一部分,但你觉得哪条路径更有「潜力」成为最短路径中的一部分

从目前的情况来看,显然橙色路径的可能性更大嘛,所以我们希望节点 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 对应的值就是从 startend 的最短距离。

这个算法较之前的实现提前 return 了,所以效率有一定的提高。

posted @ 2022-04-08 10:03  BailanZ  阅读(133)  评论(0编辑  收藏  举报