Java算法-一文搞懂Dijkstra算法

一、Dijkstra算法介绍

① 什么是Dijkstra算法

想象一下,你站在一个巨大的迷宫里,四周是错综复杂的道路,每条路通往不同的地方,并且每条路也拥有不同的长度,你的目标是找到从当前位置到出口的最短路径。这时,你手里拿着一张地图,能够看到所有位置的节点以及通向的位置和路径的长度。你应该如何选择呢?这就是Dijkstra算法的专场了,它能够像指南针一样帮你找到你想去的地方的最短路径

也就是说,Dijkstra算法用于解决"单源最短路径"

📕 初始化:创建一个以当前节点为起点以下标表示目的地的表示距离的数组

📕 搜索:从起点出发,搜索周围的路径。每走一条路,就会记录下从起点到这的距离(每次都会选取一条最短的路径继续前进)。

📕 更新:因为可能有多个路口通向同一个节点,当另一个路口通向该节点的距离比原来记录的距离更短时,我们会更新这个距离为新的最短距离(最重要的!!)


② 为什么用Dijkstra求最短路径

看到"最短路径"或许你会想,为什么不用bfs或者dfs呢?这是我们之前所学习过的知识,并且确实也用来解决"最短路径"问题。但既然这里没有用它们,肯定是因为在某些地方bfs和dfs做不到,而Dijkstra算法能做到那么我们需要先看一下bfs(dfs)的原理

📕 BFS的核心思想

BFS的搜索方式是"层层递进"的,它从起点开始,探索所有离起点最近的节点(默认距离为1),然后是距离为2的节点,以此类推。在无权图中,这样的搜索方法确实能够保证找到最短路径。

但在有权图中,边的权重各不相同,而BFS仅仅关注"经过的边数"忽略了"边的权重"

📕 DFS的核心思想

DFS是一种"一条路走到黑"的搜索方式,它会沿着一条路径尽可能的深入,因此可能会错过正确的最短路径。

而两者不能够解决"有权图最短路径"的根本原因,其实在于:

不论是DFS还是BFS在搜索的过程中都不会二次更新已经搜索过的结点而Dijkstra算法最核心的步骤,就是更新!!!


二、Dijkstra求最短路

① Dijkstra求最短路(基础版)

题目就图论没什么好说的了,就是我们的Dijkstra算法的基本应用

已知一张 n 个结点m 条边的有权有向图,让我们求"1号"到"n号"的最短路~

上面虽然大家已经对Dijkstra算法有了基本的了解,并且也知道了它的步骤,但直接来做题肯定还是比较抽象的(天赋哥别看...)

既然是,那我们画个图就好了~

📕 初始化:

创建一个 int[ ] 数组 dist 用于存储每一个" 1号 到 i号 的最短路径 "

其中,INF代表该位置还未被访问到,也就没有最短路径。而dist[1]代表起点到起点的距离,为0~

创建一个邻接表 List<int[ ]>[ ] list ,用于存储某个点对应存在的路径
int[] -> [目标数字,权值]

创建一个 boolean[ ] 数组,用于判断是否已经以某节点作为中心扫描过

📕 搜索:

这边已经比较详细的写出其中的步骤和细节啦~相信通过步骤图,大家也能切实的体会到"更新"是多么重要的一步!!

最后我们只需要输出 dist[n] 即可(如果dist[n] = INF则输出 -1)

📖 代码示例

import java.util.*;
// 1:无需package
// 2: 类名必须Main, 不可修改

public class Main {
    public static int INF = 0x3f3f3f3f;
    //用于存储某个点对应存在的路径
    //int[] -> [目标数字,权值]
    public static List<int[]>[] list;
    //判断是否已经以某节点作为中心扫描过
    public static boolean[] count;
    //存储[1 -> n]的最短距离
    public static int[] dist;
    public static int n;
    public static int m;
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        n = in.nextInt();
        m = in.nextInt();
        list = new List[m + 1];
        count = new boolean[n + 1];
        dist = new int[n + 1];
        for(int i = 0;i <= m;i++){
            list[i] = new ArrayList<>();
        }
        for(int i = 1;i <= m;i++){
            int a = in.nextInt();
            int b = in.nextInt();
            int c = in.nextInt();
            if(list[a] == null){
                list[a] = new ArrayList<>();
            }
            list[a].add(new int[]{b,c});
        }
        dijkstra();
        System.out.println(dist[n] == INF ? -1 : dist[n]);
    }
    public static void dijkstra(){
        Arrays.fill(dist,INF);
        //1 -> 1 自然距离为0
        dist[1] = 0;
        for(int i = 1;i <= n;i++){
            //用u来查找目前能探索的"路径最短"节点
            int u = -1;
            for(int j = 1;j <= n;j++){
                //如果已经以[j]位置进行了查找拓宽操作.则跳过
                if(count[j]){
                    continue;
                }
                if(u == -1 || dist[j] < dist[u]){
                    u = j;
                }
            }
            count[u] = true;
            //此时已经查找到可探索路径中最短的
            if(list[u] == null){
                continue;
            }
            List<int[]> path = list[u];
            for(int k = 0;k < path.size();k++){
                //路对面的结点
                int v = path.get(k)[0];
                //路的距离
                int w = path.get(k)[1];
                dist[v] = Math.min(dist[v], dist[u] + w);
            }
        }
    }
}

② Dijkstra求最短路(堆优化)

(这次的数据范围 n 最高达到了 10^5,反观上一道题,n 最高也才500)

上面的Dijkstra步骤图中,我们了解到"每次查找下一个路口,需要距离由近及远的遍历"。而在上面的代码中,我们采用了 for() 循环来进行查找这个最近路口显然这样做的效率是不高的

那么我们只需要一个,能够快速帮我们按照距离排好路口顺序的数据结构就好了呀,所以我们就能够想到我们的 "PriorityQueue"(优先级队列) !!!

我们可以通过优先队列存储一个二元组表示下一步可以访问的点
第一个元素为下一个点的编号第二个元素为两点之间的距离按照距离升序排序~

这样我们就可以在 log 的时间复杂度内查找点,而比之前使用for循环快了不少~

📖 代码示例

import java.util.*;
// 1:无需package
// 2: 类名必须Main, 不可修改

public class Main {
    public static int n;
    public static int m;
    public static int[] dis;
    public static boolean[] count;
    public static List<int[]>[] list;
    public static int INF = 0x3f3f3f3f;
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        n = in.nextInt();
        m = in.nextInt();
        //存储从1到x间的最短路径
        dis = new int[n + 1];
        count = new boolean[n + 1];
        list = new ArrayList[n + 1];
        for(int i = 1;i <= n;i++){
            list[i] = new ArrayList<>();
        }
        for(int i = 1;i <= m;i++){
            int a = in.nextInt();
            int b = in.nextInt();
            int c = in.nextInt();
            list[a].add(new int[]{b,c});
        }
        dijkstra();
        System.out.println(dis[n] == INF ? -1 : dis[n]);
    }
    public static void dijkstra(){
        //代表能到达的节点,以及两点间距离
        Arrays.fill(dis,INF);
        dis[1] = 0;
        PriorityQueue<int[]> p = new PriorityQueue<>(Comparator.comparing(k -> k[1]));
        p.add(new int[]{1,0});
        while(!p.isEmpty()){
            int[] t = p.poll();
            if(count[t[0]]) continue;
            count[t[0]] = true;
            for(int[] arr : list[t[0]]){
                int v = arr[0];
                int w = arr[1];
                if(dis[v] > dis[t[0]] + w){
                    dis[v] = dis[t[0]] + w;
                    p.add(new int[]{v,dis[v]});
                }
            }
        }
    }
}

③ 习题:电动车

不要因为题目复杂而害怕,这其实就是一个dijkstra的一个变种题~

那么这题是什么意思呢?

小蓝在 N 座城市之间来回穿梭(代表有 N 个结点)

把 N 座城市 编号 1 至 N,城市之间一共有 M 条双向高速公路(无向图)

其中第 i 条链接 ui 号城市和 vi 号城市 耗费 wi 个单位的电量(结点ui 与 结点 vi 间的距离为 wi)

途中经过城市可以为电瓶车充满电,如果从任意城市开电动车到任意另一个城市,都可以找到一条不用充电的公路,问电动车至少需要多少电量(其实就是问你所有城市路径中最大边的最小值)

所以其实我们只需要对Dijkstra求最短路的解法稍微修改一下即可~

📕 由有向图变成了无向图,也就是说每次输入"结点1","结点2","距离"时,需要向邻接表中添加两次,为"结点2"中加入{"结点1","距离"},为"结点1"中加入{"结点2","距离"}。

📕 查找的不再是最短路径,而是所有路径的最大边里,挑选一个最小值,所以dist[i]存储的不是最短路径,而是该条路径中的最大边中最小值。

📖 代码示例

import java.util.*;
// 1:无需package
// 2: 类名必须Main, 不可修改

public class Main {
    public static int n;
    public static int m;
    public static int[] dis;
    public static boolean[] count;
    //代表从 i 到 x 路途中的最大路距
    public static List<int[]>[] list;
    public static int INF = 0x3f3f3f3f;
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        n = in.nextInt();
        m = in.nextInt();
        dis = new int[n + 1];
        count = new boolean[n + 1];
        list = new List[n + 1];
        for(int i = 1;i <= n;i++){
            list[i] = new ArrayList<>();
        }
        for(int j = 1;j <= m;j++){
            int a = in.nextInt();
            int b = in.nextInt();
            int c = in.nextInt();
            list[a].add(new int[]{b,c});
            list[b].add(new int[]{a,c});
        }
        dijkstra(1);
        int max = 0;
        for(int i = 1;i <= n;i++){
            if(dis[i] == INF){
                System.out.println(-1);
                return;
            }else {
                max = Math.max(max,dis[i]);
            }
        }
        System.out.println(max);
    }
    public static void dijkstra(int n){
        PriorityQueue<int[]> queue = new PriorityQueue<>(Comparator.comparing(q -> q[1]));
        Arrays.fill(dis,INF);
        queue.add(new int[]{1,0});
        dis[1] = 0;
        while(!queue.isEmpty()){
            int[] t = queue.poll();
            int index = t[0];
            if(count[index]) continue;
            count[index] = true;
            List<int[]> path = list[index];
            for (int[] a: path) {
                int v = a[0];
                int w = a[1];
                int max = Math.max(dis[index],w);
                if(dis[v] > max){
                    dis[v] = max;
                }
                queue.add(new int[]{v,dis[v]});
            }
        }
    }
}

那么这篇关于 Dijkstra算法 的文章到这里就结束啦,作者能力有限,如果有哪里说的不够清楚或者不够准确,还请各位在评论区多多指出,我也会虚心学习的,我们下次再见啦~

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值