最近在看关于求最短路径的一些算法,因此专门整理一下,以方便后续的复习。
求最短路径的情形主要分为以下两种:(1)单源最短路径;(2)多元汇最短路径。而单源最短路经中包括所有边数的权都是正数的,和边数有负数的这两种。针对不同的问题,可以分别用不同的算法进行求解。
下图表示不同问题所应用的不同算法,以及算法的复杂度:
这篇文章主要介绍一些Dijstra算法。
朴素Dijstra主要用于在一个稠密图中,它的边数远远大于它的点数,因此我们可以用邻接矩阵来表示这个图。他的具体算法的设计思想如下:
输入:赋权的有向图 G = ( V , E , W ) G=(V,E,W) G=(V,E,W), V = { v 1 , v 2 , . . v n } V=\left\{v_1,v_2,..v_n\right\} V={v1,v2,..vn}, s : = v 1 s:=v_1 s:=v1。
输出:从源点s到所有的 v i ∈ V \ { s } v_i\in V\backslash\left\{s\right\} vi∈V\{s} 的最短路径。
1.初始 S = { v 1 } S=\left\{v_1\right\} S={v1};
2.对于 v i ∈ V − S v_i\in V-S vi∈V−S,计算 d i s t [ s , v i ] dist\left[s,v_i\right] dist[s,vi];
3.对于选择 m i n v j ∈ V d i s t [ s , v i ] \mathop{min}\limits_{v_j\in V}dist\left[s,v_i\right] vj∈Vmindist[s,vi],并将这个 v j v_j vj 放进集合 S S S 中,更新 V − S V-S V−S 中的顶点的 d i s t dist dist 值;
4.重复1,直到 S = V S=V S=V;
我们举一个具体例子来解释一下这个算法:
首先利用一个邻接矩阵来存储这个有向图:
接下来利用一个一维数组存储1号顶点到其余各个顶点的距离:
既然是求 1 号顶点到其余各个顶点的最短路程,那就先找一个离 1 号顶点最近的顶点。其中顶点2离顶点1距离最小,因此顶点2被确定下来。
接下来以顶点2为确定点,选择顶点2的两条出边:2->3,2->4这两条边。先讨论通过 2->3 这条边能否让 1 号顶点到 3 号顶点的路程变短。也就是说现在来比较 dis[3]和 dis[2]+e[2][3]的大小。其中 dis[3]表示 1 号顶点到 3 号顶点的路程,dis[2]+e[2][3]中 dis[2]表示 1 号顶点到 2 号顶点的路程,e[2][3]表示 2->3 这条边。所以 dis[2]+e[2][3]就表示从 1 号顶点先到 2 号顶点,再通过 2->3 这条边,到达 3 号顶点的路程。
我们发现:dis[3]=12, dis[2]+e[2][3]=10,因此dis[3]要更新为10。
同理通过2->4,可以将dis[4]更新为4。dis[4]>dis[3],因此选定顶点4作为确定点继续进行更新。顶点4的三条出边:3,5,6。此时dis[3]最小,因此选择dis[3]为确定点,进行下一轮迭代,以此类推,直到dis数组中全部为确定值,最终得到的dis矩阵:
输出dis[6],即最短路径为17.
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
数据范围
1 ≤ n ≤ 500 1\leq n \leq 500 1≤n≤500
1 ≤ m ≤ 1 0 5 1\leq m \leq 10^5 1≤m≤105,
图中涉及边长均不超过10000。
最终的实现代码如下:
import java.io.*;
import java.util.*;
class Main {
private static int N = 510;
private static int[][] g = new int[N][N]; //为稠密阵所以用邻接矩阵存储
private static boolean[] st = new boolean[N]; 用于记录该点的最短距离是否已经确定
private static int[] dist = new int[N]; //用于记录每一个点距离第一个点的距离
private static int n;
private static int max = 5000000;
public static void main(String[] args) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String[] str1 = bufferedReader.readLine().split(" ");
n = Integer.parseInt(str1[0]);
int m = Integer.parseInt(str1[1]);
for (int i = 1; i <= n; i ++) {
Arrays.fill(g[i], max); //初始化图 因为是求最短路径 所以每个点初始为无限大
}
for (int i = 0; i < m; i ++) {
String[] str2 = bufferedReader.readLine().split(" ");
int a = Integer.parseInt(str2[0]);
int b = Integer.parseInt(str2[1]);
int c = Integer.parseInt(str2[2]);
g[a][b] = Math.min(g[a][b], c); //如果发生重边的情况则保留最短的一条边
}
System.out.println(dijkstra());
bufferedReader.close();
}
public static int dijkstra() {
Arrays.fill(dist, max); //初始化距离 0x3f代表无限大
dist[1] = 0; //第一个点到自身的距离为0
for (int i = 0; i < n; i ++) {
int t = -1; //t存储当前访问的点
for (int j = 1; j <= n; j ++) {
if (!st[j] && (t == -1 || dist[t] > dist[j])) {
t = j;
}
}
st[t] = true;
for (int j = 1; j <= n; j ++) {
dist[j] = Math.min(dist[j], dist[t] + g[t][j]); //依次更新每个点所到相邻的点路径值
}
}
if (dist[n] == max) {
return -1;
} else {
return dist[n];
}
}
}
堆优化的Dijstra算法相比于朴素Dijstra算法,复杂度降低了,它主要适用于稀疏图的情况,即有向图的顶点和边数数量相似。因此需要利用邻接表来实现,而不能用之前的邻接矩阵。
堆优化的Dijstra算法主要利用了优先队列的一些方法,算法流程与朴素Dijstra算法类似,下面对应一个具体问题来了解。
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
数据范围:
1 ≤ n , m ≤ 1.5 ∗ 1 0 5 1\leq n,m \leq 1.5 *10^5 1≤n,m≤1.5∗105,
代码如下:
import java.io.*;
import java.util.*;
class Main {
private static int N = 150010;
private static int[] h = new int[N], e = new int[N], ne = new int[N], w = new int[N]; // 创建邻接表
private static int max = 50000000;
private static boolean[] st = new boolean[N];
private static int[] dist = new int[N]; // // 存储1号点到每个点的最短距离
private static int idx = 0;
private static int n;
public static void main(String[] args) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String[] str1 = bufferedReader.readLine().split(" ");
n = Integer.parseInt(str1[0]);
int m = Integer.parseInt(str1[1]);
Arrays.fill(h, -1);
for (int i = 0; i < m; i ++) {
String[] str2 = bufferedReader.readLine().split(" ");
int a = Integer.parseInt(str2[0]);
int b = Integer.parseInt(str2[1]);
int c = Integer.parseInt(str2[2]);
add(a, b, c);
}
int t = dijkstra();
System.out.println(t);
bufferedReader.close();
}
public static void add(int a, int b, int c) {
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
public static int dijkstra() {
//维护当前未在st中标记过且离源点最近的点
Arrays.fill(dist, max);
Queue<int[]> queue = new PriorityQueue<>(n, new Comparator<int[]> () {
public int compare(int[] a, int[] b) {
return a[1] - b[1];
}
});
queue.offer(new int[] {1, 0});
dist[1] = 0;
while (!queue.isEmpty()) {
//1、找到当前未在s中出现过且离源点最近的点
int[] p = queue.poll();
int ver = p[0];
int distance = p[1];
if (st[ver]) {
continue;
}
/2、将该点进行标记
st[ver] = true;
//3、用t更新其他点的距离
for (int i = h[ver]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > distance + w[i]) {
dist[j] = distance + w[i];
queue.offer(new int[] {j, dist[j]});
}
}
}
if (dist[n] == max) {
return -1;
} else {
return dist[n];
}
}
}
Dijstra算法主要用于一个边数权值均为正值的有向图,对于稀疏图和稠密图也应该选择不同的算法进行使用。