一、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算法 的文章到这里就结束啦,作者能力有限,如果有哪里说的不够清楚或者不够准确,还请各位在评论区多多指出,我也会虚心学习的,我们下次再见啦~