Dijkstra算法求解最短路径—— 从零开始的图论讲解(2)

news2025/4/16 10:32:34

前言

在本系列第一期:从零开始的图论讲解(1)——图的概念,图的存储,图的遍历与图的拓扑排序-CSDN博客

笔者给大家介绍了 图的概念,如何存图,如何简单遍历图,已经什么是图的拓扑排序

按照之前的学习规划,今天笔者将继续带大家深入了解图论中的一个核心问题:最短路径的求解

博客将聚焦介绍Dijkstra 算法,这是解决单源最短路径问题中最经典、应用最广泛的算法之一。

博客中出现的参考图都是笔者手画的,代码示例也是笔者手敲的!影响虽小,但请勿抄袭

什么是最短路径问题

在具体介绍算法之前,我先给刚学习的读者简单科普一下什么是最短路径问题,简单来说,

最短路径问题的核心就是:

在一个图中,找到从起点出发,到达终点的路径,使路径的总权值最小。

 这里的可以是有向图,也可以是无向图,这里的权值也代表很多意思,抽象地说,就是代表达到两点之间的代价,比如路程、时间、费用等。

  • 在地图导航中,寻找从出发地到目的地的最短行驶距离;

  • 在网络通信中,找到数据包传输延迟最小的路径;

  • 在项目管理中,计算最短的工期安排。

根据具体场景的不同,最短路径问题还可以细分为几种类型:
1. 单源最短路径:从一个指定节点出发,计算它到其他所有节点的最短路径。
2. 多源最短路径:计算任意两点之间的最短路径。
3. 单对最短路径:只关心从一个节点到另一个节点的最短路径。

在本篇博客中,我们将专注于介绍单源最短路径问题,并学习如何使用经典的Dijkstra 算法来高效解决这一问题。 

什么是Dijkstra 算法

Dijkstra 算法是由荷兰计算机科学家艾兹赫尔·迪克斯特拉(Edsger W. Dijkstra)在1959年提出的。它是一种贪心思想的算法,专门用来计算从一个起点到图中其他所有节点的最短路径,前提是:
 图的所有边权值必须是非负数,如果有权值是负数,那么有另外的算法去解决,我们以后再谈.

Dijkstra 算法的特点:

  • 适用于无向图有向图

  • 可以快速找到单源最短路径,效率优良,且思路简单。

  • 常与优先队列(堆)配合,进一步优化性能,我们先介绍基础算法,然后再介绍用小根堆优化过的算法.

Dijkstra 算法的核心思想 :

Dijkstra 算法的核心思想就是:

1.先选择好起点.

2.每次访问距离起点最近的,且之前没有被访问过的点

3.更新它的邻居的最短路径,并将其标记为已访问。

具体的步骤是:

1.解决最短路径问题时,我们通常要记录从起点到各个节点的当前最短距离。
因此,可以创建一个 dist[] 数组,长度为 节点数量 + 1

数组含义:

  • dist[i] 表示起点 start 到第 i 个节点的最短距离。

  • 起点 dist[start] = 0,表示从起点到自己,距离为 0。

  • 其余节点初始值设置为 (通常用 Integer.MAX_VALUE 或自定义的 INF),表示“暂时不可达”。

如果算法结束后,dist[i] 仍然是 ,则说明:起点无法到达节点 i

同时,还需要一个 vis[] 数组用于标记节点是否已经确定了最短路径:

  • vis[i] = false:表示节点 i 还未确定最短路径。

  • vis[i] = true:表示节点 i 的最短路径已确定,无需再次更新。

2.在执行算法之前,必须先完成图的存储。
可以使用两种方式存储图:

  • 邻接表:适合稀疏图,节省空间。

  • 邻接矩阵:适合稠密图,查询方便。

构建完成后,图的结构应该已经完整地反映每个节点的所有出边。

3.构图完成后,开始进行 Dijkstra 算法:

    private static void dijkstra(int start)
    {
        Arrays.fill(dis,INF);
        dis[start] = 0;

        for(int i=1;i<=n;i++)
        {
            int pd = -1;
            int minDist = INF;

            // 找到当前未访问的点中,距离最小的点
            for(int j=1;j<=n;j++)
            {
                if(!vis[j] && dis[j] < minDist)
                {
                    pd = j;
                    minDist = dis[j];
                }
            }
            // 如果 pd == -1,说明所有点都被访问完了
            if(pd==-1) break;
            vis[pd] = true;
            // 遍历 pd 的所有邻接点
            for(Edge edge : graph.get(pd))
            {
                int v = edge.v;
                int w = edge.w;
                if(dis[pd] + w < dis[v])
                {
                    dis[v] = dis[pd] + w;
                }
            }
        }
    }
  • 每次循环,都会从未访问的节点中,选择 距离起点最近 的节点 pd

  • 如果 pd == -1,说明所有节点都已被访问,或者剩下的节点无法从起点到达,算法提前结束。

  • 选中 pd 后,遍历它的邻接点,尝试通过 pd 更新这些点的最短路径:

    dis[v] = min(dis[v], dis[pd] + w);

    这里 dis[pd] + w 代表:
    “从起点先走到 pd,再从 pd 走到 v” 的路径长度

    如果这条路径更短,就用它更新 dis[v],确保每次记录的都是当前已知的最短路径。

以上就是朴素版的Dijkstra 算法的具体步骤,接下来我们给一组例子,模拟一遍该算法,让您看的更明白

如图: 

假设我们令 1 为源点strat,求 1 到其他点的最短路径,现在我们用Dijkstra 算法模拟一遍

初始状态:

点编号dist[] 数组值vis[] 状态
10false
2false
3false
4false

第一轮:距离源点最近的点且i] = false 的节点 : 1

  • 标记 vis[1] = true

  • 遍历邻接点:

    • 2 的距离 0 + 2 = 2,更新 dist[2] = 2

    • 3 的距离 0 + 5 = 5,更新 dist[3] = 5

点编号dist[] 数组值vis[] 状态
10true
22false
35false
4false

第二轮: 距离源点最近的点且i] = false 的节点 : 2

  • 标记 vis[2] = true

  • 遍历邻接点:

    • 3 的距离 2 + 1 = 3,比原来的 5 小,更新 dist[3] = 3

    • 4 的距离 2 + 2 = 4,更新 dist[4] = 4

此时我们可以看到,借节点2到达3所需要的权值更小,体现出了Dijkstra算法的作用

点编号dist[] 数组值vis[] 状态
10true
22true
33false
44false

第三轮: 距离源点最近的点且i] = false 的节点 : 3

  • 标记 vis[3] = true

  • 遍历邻接点:

    • 4 的距离 3 + 3 = 6,但 dist[4] 已经是 4,所以不更新

请注意,虽然节点2也和节点3邻接,但是此时的dist[2] 已经是最优解了

为什么?

因为在第二轮时:

首先:节点2被选中,dist[2] = 2,说明在所有未访问的点中,从源点出发,能到2的路径已经是全局最短。这时候就把 vis[2] 设置成 true,表明节点2已经确定了最短路径。

其次:Dijkstra 的算法核心原则就是

一旦某个节点 i 被选择,并且 vis[i] = true,说明 dist[i] 的值已经是最终确定的最小值,不会再被改变。

在第一轮时,选择1也不会改变也是同理

点编号dist[] 数组值vis[] 状态
10true
22true
33true
44false

第四轮:选出未访问且距离最小的点:节点4

     

  • 标记 vis[4] = true

  • 由于前面的点都已经确定最短路径,因此没有可维护的节点

点编号dist[] 数组值vis[] 状态
10true
22true
33true
44true

 最终结果:

点编号最短距离 dist[]
10
22
33
44

 完整代码

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;

public class Dijkstra
{
    static int n,m;
    static int N = 505;
    final static int INF = Integer.MAX_VALUE-200000000;
    static List<List<Edge>> graph = new ArrayList<>();

    static boolean[] vis = new boolean[N];
    static int[] dis = new int[N];

    // 定义一个边的类 (u->v,权值w)
    static class Edge {
        int v, w;
        Edge(int v, int w) {
            this.v = v;
            this.w = w;
        }
    }
    // 添加一条 u -> v, 权值为 w 的边
    static void addEdge(int u, int v, int w)
    {
        graph.get(u).add(new Edge(v,w));
    }

    // Dijkstra 算法
    private static void dijkstra(int start)
    {
        Arrays.fill(dis,INF);
        dis[start] = 0;

        for(int i=1;i<=n;i++)
        {
            int pd = -1;
            int minDist = INF;

            // 找到当前未访问的点中,距离最小的点
            for(int j=1;j<=n;j++)
            {
                if(!vis[j] && dis[j] < minDist)
                {
                    pd = j;
                    minDist = dis[j];
                }
            }
            // 如果 pd == -1,说明所有点都被访问完了
            if(pd==-1) break;
            vis[pd] = true;
            // 遍历 pd 的所有邻接点
            for(Edge edge : graph.get(pd))
            {
                int v = edge.v;
                int w = edge.w;
                if(dis[pd] + w < dis[v])
                {
                    dis[v] = dis[pd] + w;
                }
            }
        }
    }
    public static void main(String[] args)
    {
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        m = scanner.nextInt();

        // 提前创建 n+1 个 ArrayList,避免越界
        for(int i=0;i<=n;i++) {
            graph.add(new ArrayList<>());
        }

        for(int i=0;i<m;i++)
        {
            int a = scanner.nextInt();
            int b = scanner.nextInt();
            int c = scanner.nextInt();
            addEdge(a,b,c);
        }

        dijkstra(1);

        if(dis[n]>=INF-100000000)
        {
            System.out.println(-1);
        }
        else
        {
            System.out.println(dis[n]);
        }
    }
}

如何优化Dijkstra 算法? 

我们可以看到

        for(int i=1;i<=n;i++)
        {
            int pd = -1;
            int minDist = INF;

            // 找到当前未访问的点中,距离最小的点
            for(int j=1;j<=n;j++)
            {
                if(!vis[j] && dis[j] < minDist)
                {
                    pd = j;
                    minDist = dis[j];
                }
            }
            // 如果 pd == -1,说明所有点都被访问完了
            if(pd==-1) break;
            vis[pd] = true;
            // 遍历 pd 的所有邻接点
            for(Edge edge : graph.get(pd))
            {
                int v = edge.v;
                int w = edge.w;
                if(dis[pd] + w < dis[v])
                {
                    dis[v] = dis[pd] + w;
                }
            }
        }

 内层寻找最小点的部分:   O(n^2)

遍历边的部分:O(m),但这个通常远小于 O(n^2),所以主导复杂度是  O(n^2)。

如何优化呢?

在朴素版 Dijkstra 中,我们每次都需要从未访问过的点中,找到距离源点最近的节点,然后进行标记和松弛操作。这个寻找过程使用 O(n) 的循环,显然效率不高。

为了优化这一过程,我们可以使用小根堆来维护当前所有未访问的候选节点。每当我们遍历邻接点并更新 dist[] 时,就将这些点加入小根堆。堆顶的元素总是当前到源点距离最小的节点,因此每次从堆里取出的节点,正好就是下一个要访问的目标。

需要注意的是,由于同一个节点在更新路径时可能会多次入堆,实际取出时,可能会遇到这个节点已经被访问过的情况。因此,在正式处理之前,我们要加一个判断:

如果当前节点已经被访问过,直接 continue,跳过它,继续从堆中取下一个节点。

代码如下:

import java.util.*;

public class BetterperformanceDijkstra {

   static   class  Node
     {
         public int v;
         public int w;

         public Node(int v, int w) {
             this.v = v;
             this.w = w;
         }
     }

     static  int n,m;
     static  List<List<Node>> list = new ArrayList<>();

   public static void  addEge(int u, int v, int w)
   {
       list.get(u).add(new Node(v,w));

   }
   static  boolean[] vis;
   static int[] dist;
   static  final  int INF = Integer.MAX_VALUE-20000000;
public  static  void dijkstra(int start)
{
    Arrays.fill(dist,INF);
    dist[start] = 0;
    PriorityQueue<Node> priorityQueue = new PriorityQueue<>(((o1, o2) -> o1.w-o2.w));
    Node cur = new Node(start,0);
    priorityQueue.offer(cur);
    while(!priorityQueue.isEmpty())
    {
        Node temp = priorityQueue.poll();
        int v = temp.v;
        int w = temp.w;
        if(vis[v])
        {
            continue;//已经访问过了
        }
        vis[v] = true;
        if(list.get(v)==null)
        {
            continue;//没有联通的点
        }
        for(Node tep : list.get(v))
        {
            int vi = tep.v;
            int wi = tep.w;
            if(dist[vi]>dist[v]+wi)//维护最短路径
            {
                dist[vi] = dist[v]+wi;
                priorityQueue.offer(new Node(vi,dist[vi]));
            }
        }
    }
}
    public static void main(String[] args) {
           Scanner scanner = new Scanner(System.in);
           n = scanner.nextInt();
           m = scanner.nextInt();
           for(int i=0;i<=n;i++)
           {
               list.add(new ArrayList<>());
           }
           vis = new boolean[n+1];
           dist = new int[n+1];
           for(int i=0;i<m;i++)
           {
               int a = scanner.nextInt();
               int b = scanner.nextInt();
               int c = scanner.nextInt();
               addEge(a,b,c);
           }
           dijkstra(1);
        System.out.println(dist[n]==INF?"-1":dist[n]);
    }
}

结尾

结尾笔者留几个题目给大家练手

3112. 访问消失节点的最少时间 - 力扣(LeetCode)

743. 网络延迟时间 - 力扣(LeetCode)

希望本博客对大家来说有收获,也不枉我分享出来!!!谢谢大家

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

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

相关文章

[连载]Transformer架构详解

Transformer: Attention Is All You Need Paper 地址&#xff1a;https://arxiv.org/abs/1706.03762 Paper 代码&#xff1a;https://github.com/tensorflow/tensor2tensor Paper 作者&#xff1a;Ashish Vaswani,Noam Shazeer,Niki Parmar,Jakob Uszkoreit,Llion Jones,Aidan…

LVGL Video控件和Radiobtn控件详解

LVGL Video控件和Radiobtn控件详解 一、 Video控件详解1. 概述2. 创建和初始化3. 基本属性设置4. 视频控制5. 回调函数6. 高级功能7. 注意事项 二、Radiobtn控件详解1. 概述2. 创建和初始化3. 属性设置4. 状态控制5. 组管理6. 事件处理7. 样式设置8. 注意事项 三、效果展示四、…

组合数哭唧唧

前言&#xff1a;手写一个简单的组合数&#xff0c;但是由于长期没写&#xff0c;导致一些细节没处理好 题目链接 #include<bits/stdc.h> using namespace std; #define endl "\n"#define int long longconst int N (int)2e510; const int Mod (int)1e97;int…

NLP高频面试题(四十二)——RAG系统评估:方法、指标与实践指南

1. 引言:RAG系统概述与评估挑战 检索增强生成(Retrieval-Augmented Generation,简称 RAG)是近年来自然语言处理领域的一个重要进展。RAG系统在大型语言模型生成文本的过程中引入了外部检索模块,从外部知识库获取相关信息,以缓解纯生成模型可能出现的幻觉和知识盲点。通过…

Linux路漫漫

目录 Vim模式 基本操作 文本编辑 更多功能 1. 直接启动 Vim 2. 打开一个已存在的文件 3. 打开多个文件 4. 以只读模式打开文件 5. 从指定行号开始编辑 6. 快速打开并执行命令 7. 检查是否安装了 Vim 8. 退出 Vim 前提条件 SCP 命令格式 具体操作 1. Windows 命…

游戏引擎学习第227天

今天的计划 今天的工作重点是进行吸引模式&#xff08;attract mode&#xff09;的开发&#xff0c;主要是处理游戏的进出和其他一些小的细节问题&#xff0c;这些是之前想要整理和清理的部分。我做了一些工作&#xff0c;将游戏代码中的不同部分分离到逻辑上独立的区域&#…

一键直达:用n8n打造谷歌邮箱到Telegram的实时通知流

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 &#x1f38f;&#xff1a;你只管努力&#xff0c;剩下的交给时间 &#x1f3e0; &#xff1a;小破站 一键直达&#xff1a;用n8n打造谷歌邮箱到Telegram的实时通知流 前言n8n的强大之处实现简便性实战…

【QT】 QT定时器的使用

QT定时器的使用 1. QTimer介绍&#xff08;1&#xff09;QTimer的使用方法步骤示例代码1&#xff1a;定时器的启动和关闭现象&#xff1a;示例代码2&#xff1a;定时器每隔1s在标签上切换图片现象&#xff1a; (2)实际开发的作用 2.日期 QDate(1)主要方法 3.时间 QTime(1)主要方…

【自动化测试】如何获取cookie,跳过登录的简单操作

前言 &#x1f31f;&#x1f31f;本期讲解关于自动化测试函数相关知识介绍~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &#x1f386;那么废话…

每天五分钟深度学习PyTorch:RNN CELL模型原理以及搭建

本文重点 RNN Cell(循环神经网络单元)是循环神经网络(RNN)的核心组成部分,用于处理序列数据中的每个时间步,并维护隐藏状态以捕获序列中的时间依赖关系。 RNN CELL的结构 RNN是一个循环结构,它可以看作是RNN CELL的循环,RNN CELL的结构如下图所示,RNN CELL不断进行…

【基于开源insightface的人脸检测,人脸识别初步测试】

简介 InsightFace是一个基于深度学习的开源人脸识别项目,由蚂蚁金服的深度学习团队开发。该项目提供了人脸检测、人脸特征提取、人脸识别等功能,支持多种操作系统和深度学习框架。本文将详细介绍如何在Ubuntu系统上安装和实战InsightFace项目。 目前github有非常多的人脸识…

进程(完)

今天我们就补充一个小的知识点,查看进程树命令,来结束我们对linux进程的学习,那么话不多说,来看. 查看进程树 pstree 基本语法&#xff1a; pstree [选项] 优点&#xff1a;可以更加直观的来查看进程信息 常用选项&#xff1a; -p&#xff1a;显示进程的pid -u&#xff…

【控制学】控制学分类

【控制学】控制学分类 文章目录 [TOC](文章目录) 前言一、工程控制论1. 经典控制理论2. 现代控制理论 二、生物控制论三、经济控制论总结 前言 控制学是物理、数学与工程的桥梁 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、工程控制论 1. 经典…

软考中级-软件设计师 2022年上半年下午题真题解析:通关秘籍+避坑指南

&#x1f4da; 目录&#xff08;快速跳转&#xff09; 大题&#xff08;下午题&#xff09;&#xff08;每题15分&#xff0c;共75分&#xff09;一、结构化分析与设计&#x1f354; 试题一&#xff1a;外卖订餐系统 二、数据库应用分析与设计&#x1f697; 试题二&#xff1a;…

波束形成(BF)从算法仿真到工程源码实现-第十节-非线性波束形成

一、概述 本节我们基于webrtc的非线性波束形成进行代码仿真&#xff0c;并对仿真结果进行展示和分析总结。更多资料和代码可以进入https://t.zsxq.com/qgmoN &#xff0c;同时欢迎大家提出宝贵的建议&#xff0c;以共同探讨学习。 二、仿真代码 2.1 常量参数 % *author : a…

《忘尘谷》音阶与调性解析

一、音高与音名的对应关系 根据搜索结果及音乐理论&#xff0c;结合《忘尘谷》的曲谱信息&#xff0c;其音阶与调性分析如下&#xff1a; 调性判定 原曲调性为 D调&#xff08;原曲标注为D调&#xff09;&#xff0c;但曲谱编配时采用 C调指法&#xff0c;通过变调夹夹2品&…

App测试小工具

前言 最近app测试比较多&#xff0c;每次都得手动输入日志tag&#xff0c;手动安装&#xff0c;测完又去卸载&#xff0c;太麻烦。就搞了小工具使用。 效果预览 每次测试完成&#xff0c;点击退出本次测试&#xff0c;就直接卸载了&#xff0c;usb插下一个手机又可以继续测了…

数智读书笔记系列029 《代数大脑:揭秘智能背后的逻辑》

《代数大脑:揭秘智能背后的逻辑》书籍简介 作者简介 加里F. 马库斯(Gary F. Marcus)是纽约大学心理学荣休教授、人工智能企业家,曾创立Geometric Intelligence(后被Uber收购)和Robust.AI公司。他在神经科学、语言学和人工智能领域发表了大量论文,并著有《重启AI》等多部…

Apache Kafka UI :一款功能丰富且美观的 Kafka 开源管理平台!!

Apache Kafka UI 是一个免费的开源 Web UI&#xff0c;用于监控和管理 Apache Kafka 集群&#xff0c;可方便地查看 Kafka Brokers、Topics、消息、Consumer 等情况&#xff0c;支持多集群管理、性能监控、访问控制等功能。 1 特征 多集群管理&#xff1a; 在一个地方监控和管理…

临床协调简历模板

模板信息 简历范文名称&#xff1a;临床协调简历模板&#xff0c;所属行业&#xff1a;其他 | 职位&#xff0c;模板编号&#xff1a;C1S3WO 专业的个人简历模板&#xff0c;逻辑清晰&#xff0c;排版简洁美观&#xff0c;让你的个人简历显得更专业&#xff0c;找到好工作。希…