当前位置: 首页 > news >正文

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

前言

在本系列第一期:从零开始的图论讲解(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)

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

相关文章:

  • 第十章 go mod操作
  • 【Java SE】Collections类详解
  • 2.1 腾讯校招通关指南-算法与数据结构
  • trl的安装与单GPU多GPU测试
  • 一文读懂WPF系列之依赖属性与附加属性
  • C++进阶——C++11_智能指针
  • 架构思维:缓存层场景实战_读缓存(下)
  • 【已更新完毕】2025泰迪杯数据挖掘竞赛C题数学建模思路代码文章教学:竞赛智能客服机器人构建
  • 如何高效使用 Text to SQL 提升数据分析效率?四个关键应用场景解析
  • Token安全存储的几种方式
  • Docker 搭建 RabbitMQ
  • 我的机器学习之路(初稿)
  • Django之modelform使用
  • ORACLE数据库转国产阿里OceanBase数据库
  • 【版本控制】git命令使用大全
  • display: flex,导致子dom宽高失效
  • Upwork数据战争:用爬虫与AI预测垄断订单
  • uniapp小程序位置授权弹框与隐私协议耦合(合而为一)(只在真机上有用,模拟器会分开弹 )
  • 单例模式-3-双检锁/双重校验锁(DCL,即 double-checked locking)
  • Spark-SQL核心编程语言
  • 视频丨习近平同普京会谈:共同弘扬正确二战史观,维护联合国权威和地位
  • 酒店取消订单加价卖何以屡禁不绝?专家建议建立黑名单并在商家页面醒目标注
  • 法治日报:商品明细是隐私,外卖员快递员不应知晓
  • 视频丨习近平主席出席俄方在机场举行的迎宾仪式
  • 公元1057年:千年龙虎榜到底有多厉害?
  • “子宫内膜异位症”相关论文男性患者样本超六成?福建省人民医院发布情况说明