图论- Dijkstra算法

news2025/2/22 9:09:04

Dijkstra算法

    • 前言
    • 概念
    • BFS基础模版
    • Dijkstra
      • Dijkstra函数签名
      • State类
      • distTo 记录最短路径
      • 伪代码模版
        • 第一个问题解答
        • 第二个问题解答
        • 第三个问题解答

前言

学习这个算法之间,必须要对BFS遍历比较熟悉,它的本质就是一个特殊改造过的BFS算法.

概念

Dijkstra算法是一种计算图中单源最短路径算法,本质上是一个经过特殊改造的BFS算法,改造点有两个:

  • 使用优先队列,而不是普通队列进行BFS算法.
  • 添加了一个备忘录,记录起点到每个可达节点的最短路径权重和.

BFS基础模版

// 图结构的 BFS 遍历,从节点 s 开始进行 BFS,且记录路径的权重和
// 每个节点自行维护 State 类,记录从 s 走来的权重和
class State {
    // 当前节点 ID
    int node;
    // 从起点 s 到当前节点的权重和
    int weight;

    public State(int node, int weight) {
        this.node = node;
        this.weight = weight;
    }
}

void bfs(Graph graph, int s) {
    boolean[] visited = new boolean[graph.size()];
    Queue<State> q = new LinkedList<>();

    q.offer(new State(s, 0));
    visited[s] = true;

    while (!q.isEmpty()) {
        State state = q.poll();
        int cur = state.node;
        int weight = state.weight;
        System.out.println("visit " + cur + " with path weight " + weight);
        for (Edge e : graph.neighbors(cur)) {
            if (!visited[e.to]) {
                q.offer(new State(e.to, weight + e.weight));
                visited[e.to] = true;
            }
        }
    }
}

Dijkstra

Dijkstra函数签名

输入是一幅图 graph 和一个起点 start,返回是一个记录最短路径权重的数组:

// 输入一幅图和一个起点 start,计算 start 到其他节点的最短距离
int[] dijkstra(int start, Graph graph);

State类

我们也需要一个 State 类来辅助 BFS 算法的运行,清晰起见,我们用 id 变量记录当前节点 ID,用 distFromStart 变量记录从起点到当前节点的距离。

class State {
    // 图节点的 id
    int id;
    // 从 start 节点到当前节点的距离
    int distFromStart;
    State(int id, int distFromStart) {
        this.id = id;
        this.distFromStart = distFromStart;
    }
}

distTo 记录最短路径

加权图中的 Dijkstra 算法和无权图中的普通 BFS 算法不同,在 Dijkstra 算法中,你第一次经过某个节点时的路径权重,不见得就是最小的,所以对于同一个节点,我们可能会经过多次,而且每次的 distFromStart 可能都不一样,比如下图:
在这里插入图片描述
当重复遍历到同一个节点时,我们可以比较一下当前的 distFromStart 和 distTo 中的值,如果当前的更小,就更新 distTo,反之,就不用再往后继续遍历了。

伪代码模版

// 输入一幅图和一个起点 start,计算 start 到其他节点的最短距离
int[] dijkstra(int start, Graph graph) {
    // 图中节点的个数
    int V = graph.size();
    // 记录最短路径的权重,你可以理解为 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 : graph.neighbors(curNodeID)) {
            // 看看从 curNode 达到 nextNode 的距离是否会更短
            int distToNextNode = distTo[curNodeID] + graph.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 的最短路径,是否可以修改算法,提升一些效率?

第一个问题解答

循环结束的条件是队列为空,那么你就要注意看什么时候往队列里放元素(调用 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,也能得到正确答案,但是效率会低很多。

Dijkstra 算法使用优先级队列,主要是为了效率上的优化,类似一种贪心算法的思路。

为什么说是一种贪心思路呢,比如说下面这种情况,你想计算从起点 start 到终点 end 的最短路径权重:
在这里插入图片描述
假设你当前只遍历了图中的这几个节点,那么你下一步准备遍历那个节点?这三条路径都可能成为最短路径的一部分,但你觉得哪条路径更有「潜力」成为最短路径中的一部分?

从目前的情况来看,显然橙色路径的可能性更大嘛,所以我们希望节点 2 排在队列靠前的位置,优先被拿出来向后遍历。

所以我们使用 PriorityQueue 作为队列,让 distFromStart 的值较小的节点排在前面,这就类似我们之前讲
贪心算法 说到的贪心思路,可以很大程度上优化算法的效率。

第三个问题解答

肯定可以的,因为我们标准 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 了,所以效率有一定的提高。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2299311.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CAS单点登录(第7版)9.属性

如有疑问&#xff0c;请看视频&#xff1a;CAS单点登录&#xff08;第7版&#xff09; 属性 属性定义 概述 属性定义 从身份验证或属性存储库源获取和解析 CAS 中属性的定义时&#xff0c;往往使用其名称进行定义和引用&#xff0c;而无需任何其他元数据或修饰。例如&#…

【电路笔记】-双向计数器

双向计数器 文章目录 双向计数器1、概述2、双向计数器双向计数器能够通过任何给定的计数序列向上或向下方向计数。 1、概述 双向计数器是同步向上/向下二进制计数器,能够在两个方向上向或从某个预设值以及零进行计数。 除了从零开始“向上”计数并增加或递增到某个预设值之外…

Python PyCharm DeepSeek接入

Python PyCharm DeepSeek接入 创建API key 首先进入DeepSeek官网&#xff0c;https://www.deepseek.com/ 点击左侧“API Keys”&#xff0c;创建API key&#xff0c;输出名称为“AI” 点击“创建"&#xff0c;将API key保存&#xff0c;复制在其它地方。 在PyCharm中下…

从ARM官方获取自己想要的gcc交叉编译工具链接(Arm GNU Toolchain),并在Ubuntu系统中进行配置

前言 本文是博文 https://blog.csdn.net/wenhao_ir/article/details/145547974 的分支博文。 在本博文中我们完成gcc交叉编译工具gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf.tar.xz的下载、配置、测试。 下载自己想要的gcc交叉编译工具的源码 目标文件的名字及说…

【系统架构设计师】虚拟机体系结构风格

目录 1. 说明2. 解释器体系结构风格3. 规则系统体系结构风格4. 例题4.1 例题1 1. 说明 1.p263。2.虚拟机体系结构风格的基本思想是人为构建一个运行环境&#xff0c;在这个环境之上&#xff0c;可以解析与运行自定义的一些语言&#xff0c;这样来增加架构的灵活性。3.虚拟机体…

Python 字典思维导图

在本章中&#xff0c;你将学习能够将相关信息关联起来的Python字典。你将学习如何访问和修改字典中的信息。鉴于字典可存储的信息量几乎不受限制&#xff0c;因此我们会演示如何遍 历字典中的数据。另外&#xff0c;你还将学习存储字典的列表、存储列表的字典和存储字典的字典。…

前台、后台、守护进程对比,进程组的相关函数

前台进程&#xff0c;后台进程&#xff0c;守护进程的对比 在前面我们已经了解了前台进程&#xff0c;后台进程&#xff0c;守护进程。 直接在终端中输入命令&#xff1a; 这是最常见的启动前台进程的方式。例如&#xff0c;在终端中输入 ./myprogram 就可以启动 myprogram 程…

openAI最新o1模型 推理能力上表现出色 准确性方面提升 API如何接入?

OpenAI o1模型在回答问题前会进行深入思考&#xff0c;并生成一条内部推理链&#xff0c;使其在尝试解决问题时可以识别并纠正错误&#xff0c;将复杂的步骤分解为更简单的部分&#xff0c;并在当前方法无效时尝试不同的途径。据悉&#xff0c;o1不仅数学水平与美国奥林匹克竞赛…

跨平台键鼠共享免费方案--Deskflow!流畅体验用MacBook高效控制Windows设备

在混合办公场景中&#xff0c;多设备协同已成为提升效率的关键需求。对于同时使用Mac与Windows设备的用户&#xff0c;如何通过一套键盘和触控板实现无缝切换&#xff0c;避免桌面空间浪费与操作冗余&#xff1f;本文将基于开源工具Deskflow&#xff0c;提供一套专业级解决方案…

CAS单点登录(第7版)27.开发人员

如有疑问&#xff0c;请看视频&#xff1a;CAS单点登录&#xff08;第7版&#xff09; 开发人员 Javadocs文档 group org.apereo.cas has published 42 artifact(s) with total 8210 version(s) org.apereo.cas org apereo.cas 小组已出版 42 件作品&#xff0c;共 8210 个版…

算法与数据结构(多数元素)

题目 思路 方法一&#xff1a;哈希表 因为要求出现次数最多的元素&#xff0c;所以我们可以使用哈希映射存储每个元素及其出现的次数。每次记录出现的次数若比最大次数大&#xff0c;则替换。 方法二&#xff1a;摩尔算法 摩尔的核心算法就是对抗&#xff0c;因为存在次数多…

【2.10-2.16学习周报】

文章目录 摘要Abstract一、理论方法介绍1.模糊类增量学习2.Rainbow Memory(RM)2.1多样性感知内存更新2.2通过数据增强增强样本多样性(DA) 二、实验1.实验概况2.RM核心代码3.实验结果 总结 摘要 本博客概述了文章《Rainbow Memory: Continual Learning with a Memory of Divers…

python包的管理

管理python包 python能跻身最欢迎编程语言前列的一个主要原因是python有着活跃的社区提供丰富的包&#xff0c;诸如numpy&#xff0c;pandas&#xff0c;scikit-learn等等。 python的包都存放PyPI中&#xff0c;PyPI即Python Package Index&#xff0c;是python的软件仓库。所…

我用 Cursor 开发了一款个人小记系统

https://note.iiter.cn 项目背景 在日常工作和学习中,我们经常需要快速记录一些想法、收藏一些有用的链接或者保存一些重要的文本、图片内容。虽然市面上已经有很多笔记软件,但我想要一个更轻量、更简单的工具,专注于快速记录和智能检索。于是我开发了这款个人小记系统。 系统…

安全测试中的身份认证与访问控制深度解析

第一部分:基本概念与核心问题 1. 身份认证与访问控制基础 1.1 身份认证三要素 知识因素(密码、PIN码)持有因素(硬件令牌、手机)生物因素(指纹、面部识别)1.2 访问控制模型 DAC(自主访问控制)MAC(强制访问控制)RBAC(基于角色的访问控制)2. 关键安全机制 2.1 会话…

代码随想录-训练营-day30

今天我们要进入动态规划的背包问题&#xff0c;背包问题也是一类经典问题了。总的来说可以分为&#xff1a; 今天让我们先来复习0-1背包的题目&#xff0c;这也是所有背包问题的基础。所谓的0-1背包问题一般来说就是给一个背包带有最大容量&#xff0c;然后给一个物体对应的需要…

全平台搭载旭日5!科沃斯GOAT智能割草机器人全新系列正式开售

要闻 近日&#xff0c;科沃斯全新发布的GOAT A Series 和 GOAT O Series割草机器人&#xff0c;将在多国市场正式上市发售。作为业界最强的割草机器人产品之一&#xff0c;GOAT致力为割草机带来基于机器人视觉的专业定位解决方案。科沃斯GOAT全新系列产品全平台搭载地瓜机器人…

自定义组件数据监听器案例,纯数据字段,自定义组件生命周期,页面的生命周期,插槽

1.自定义组件数据监听器案例 1.1基础案例模板 1.2定义button事件的处理函数 1.3监听对象中属性的变化&#xff0c;并且为fullColor赋值 使用通配符监听所有属性变化 2.自定义组件的纯数据字段 、 3.自定义组件的生命周期 4.组件所在页面的生命周期 5.自定义组件插槽 5.1单个插…

mybatis-lombok工具包介绍

Lombok是一个实用的]ava类库&#xff0c;能通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法&#xff0c;并可以自动化生成日志变量&#xff0c;简化java开发、提高效率。 使用前要加入Lombok依赖

LDO技术:线性调整率与负载调整率全解析

LDO(Low Dropout Regulator)低压差线性稳压器&#xff0c;其结构比较简单、纹波和噪声比DCDC小、成本也优于DCDC&#xff0c;缺点是在输入电压和输出电压的压差比较大时&#xff0c;效率低些&#xff0c;但在小电流电源电路上被广泛使用。现在输入电压和输出电压的压差可做到10…