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

发布于:2025-04-16 ⋅ 阅读:(22) ⋅ 点赞:(0)

前言

在本系列第一期:从零开始的图论讲解(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[] 状态
1 0 false
2 false
3 false
4 false

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

  • 标记 vis[1] = true

  • 遍历邻接点:

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

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

点编号 dist[] 数组值 vis[] 状态
1 0 true
2 2 false
3 5 false
4 false

第二轮: 距离源点最近的点且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[] 状态
1 0 true
2 2 true
3 3 false
4 4 false

第三轮: 距离源点最近的点且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[] 状态
1 0 true
2 2 true
3 3 true
4 4 false

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

     

  • 标记 vis[4] = true

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

点编号 dist[] 数组值 vis[] 状态
1 0 true
2 2 true
3 3 true
4 4 true

 最终结果:

点编号 最短距离 dist[]
1 0
2 2
3 3
4 4

 完整代码

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)

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


网站公告

今日签到

点亮在社区的每一天
去签到