一、什么是 Dijkstra 算法?
特点
- 解决有向图或者无向图的单源最短路径问题。
- 图中不能有负权边。
- 复杂度:
算法思路
从顶点 2
的三条临边中选择最短的边,显然是边 0->2
,它的花费是 2.
接下来,从顶点 2 开始继续选择边 2->1
,总花费是 2 + 1 = 3,这样我们就找到一条从 0->1
比直接从边 0->1
花费 5 要短的路径。
以此类推,整个过程可总结为:
- 使用贪心的思想进行 n-1 次查找。
- 从一个起点结点 v 开始,找出距离点 v 最近的点,假设为 v1,标记为 visited;
- 下一次查找时,从上述结点 v1 开始,又查找未访问结点中找出距离点 v1 最近的点 v2,判断
dist[v->v1] + W[v1->v2]
是否小于dist[v->v2]
,是则更新dist[v2]
。 - 重复上述过程。
二、题解
方法一:朴素版 Dijkstra
算法中的变量介绍
- 因为每次都要找到花费最小的边对应的顶点,所以使用最小堆较适合。
int[] dist[]
: 表示从 走到 的当前最短路径长度。String[] path
: 表示 的前一个顶点,即当前可确认最短路上的倒数第 2 结点。boolean vis[Vi]
:记录访问过的最短路经中顶点 。int[][] w
: 表示从结点 i 到结点 j 的边的权值。- 如果结点 i 到结点 j 不直通,则表示为 。
算法大致流程
- 迭代
N
次,每次可确定一个顶点 的到其点的最短距离:- 每一次找出没有访问过,并且距离源点距离比当前点要小的点的编号 mini。
- 遍历每一个点
1~V
,每次更新从0->j
的最短路,具体为;dist[j] = min(dist[j], dist[mini] + edges[mini][j])
- 遍历
N
次后,即可得到0~N
的最短路径。
存在的疑惑:
- Q1:为什么一开始
vis[1]
不用标记为true
?
A1:因为,我们就是需要从第 1 个点开始找与第 1 个点的最短距离边。
import java.util.*;
import java.math.*;
import java.io.*;
public class Main{
static int V, E;
static int[][] edges;
static int[] dist;
static boolean[] vis;
final static int MAX_VALUE = 10000000;
private static int dijkstra() {
dist[1] = 0;
for (int i = 0; i < V; i++) { //跌打n次,表示从每一个结点开始一次查找
int mini = -1;
for (int j = 1; j <= V; j++) { //枚举每一个点
if (vis[j] == false && (mini == -1 || dist[j] < dist[mini])) //从所有未访问的点钟找到dist最小的点
mini = j;
}
vis[mini] = true;
for (int j = 1; j <= V; j++) {
dist[j] = Math.min(dist[j], dist[mini] + edges[mini][j]);
}
}
return dist[V] == MAX_VALUE ? -1 : dist[V];
}
public static void main(String[] args) throws IOException {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(System.out));
V = sc.nextInt();
E = sc.nextInt();
edges = new int[V+1][V+1];
vis = new boolean[V+1];
dist = new int[V+1];
Arrays.fill(dist, MAX_VALUE);
for (int i = 1; i <= V; i++)
for (int j = 1; j <= V; j++) {
edges[i][j] = MAX_VALUE;
}
for (int i = 0; i < E; i++) {
int x = sc.nextInt();
int y = sc.nextInt();
int z = sc.nextInt();
edges[x][y] = z;
}
System.out.println(dijkstra());
}
}
复杂度分析
- 时间复杂度: ,
- 空间复杂度: ,
方法二:堆优化
- 朴素版的 dijkstra 每次要用 的时间去寻找距离源点的最短边,使用 pq 将本次遍历的距离最短的边的结点入队,下次取的时候直接用 时间取。
- 为了防止重复点入队,假如
boolean[] vis
先剪枝。
数据结构说明
Edge{...}
:包含该边的末尾节点的编号以及边的权重。
import java.util.*;
import java.math.*;
import java.io.*;
public class Main{
static int V, E;
static int[] dist;
static boolean[] vis;
final static int MAX_VALUE = 0x7fffffff;
static ArrayList<Edge>[] edges;
private static void dijkstra(int s) {
dist[s] = 0;
PriorityQueue<Edge> pq = new PriorityQueue<>((e1, e2) -> e1.w - e2.w);
pq.add(new Edge(s, dist[s]));
while (!pq.isEmpty()) {
Edge edge = pq.poll();
int v = edge.end;
if (vis[v])
continue;
vis[v] = true;
for (int i = 0; i < edges[v].size(); i++) {
Edge t = edges[v].get(i);
if (dist[t.end] > dist[v] + t.w) {
dist[t.end] = dist[v] + t.w;
pq.add(new Edge(t.end, dist[t.end]));
}
}
}
}
public static void main(String[] args) throws IOException {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(System.out));
V = sc.nextInt();
E = sc.nextInt();
int S = sc.nextInt();
vis = new boolean[V+1];
dist = new int[V+1];
edges = new ArrayList[V+1];
for (int i = 0; i <= V; i++) {
edges[i] = new ArrayList<Edge>();
}
Arrays.fill(dist, MAX_VALUE);
for (int i = 0; i < E; i++) {
int s = sc.nextInt();
int e = sc.nextInt();
int cost = sc.nextInt();
edges[s].add(new Edge(e, cost));
}
dijkstra(S);
for (int i = 1; i <= V; i++)
System.out.print(dist[i] + " ");
}
static class Edge {
int end, w;
Edge(int _end, int _w) {
end = _end;
w = _w;
}
}
}
复杂度分析
- 时间复杂度: ,没次取出堆顶的点都会遍历下其连接的边,比如第一次要遍历的边数为 E1,向堆里插入更新距离了的点的复杂度为 ,所以总的时间复杂度为 。由于所有边数总和为 E,所以总的时间复杂度为 。
- 空间复杂度: ,
方法三:链式前向星
qswl,算法错误:在Dijkstra 算法中,遍历某一个结点的孩子结点时,加入到队列后,不应该把它标记为访问过 vis[to] = true
,不要和 SPFA 算法搞混。
洛谷的题对 Java 很不友好的,总是 TLE/MLE
import java.util.*;
import java.math.*;
import java.io.*;
public class Main{
static int V, E;
static int[] dist;
static int INF = 0x3f3f3f3f;
static boolean[] vis;
static int[] head;
static Edge[] edges;
static int tot;
static int MAXN = 1000000;
private static void addEdge(int u, int v, int w) {
edges[++tot] = new Edge();
edges[tot].to = v;
edges[tot].w = w;
edges[tot].next = head[u];
head[u] = tot;
}
private static void dijkstra(int S) {
dist[S] = 0;
Queue<Node> q = new PriorityQueue<>((e1, e2) -> e1.cost - e2.cost);
q.add(new Node(S, dist[S]));
while (!q.isEmpty()) {
Node now = q.poll();
int v = now.to;
if (vis[v])
continue;
vis[v] = true;
for (int i = head[v]; i != 0; i = edges[i].next) {
int to = edges[i].to, w = edges[i].w;
if (dist[to] > dist[v] + w) {
dist[to] = dist[v] + w;
if (!vis[to]) {
q.add(new Node(to, dist[to]));
// vis[to] = true; 注
}
}
}
}
}
public static void main(String[] args) throws IOException {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(System.out));
V = sc.nextInt();
E = sc.nextInt();
int s = sc.nextInt();
vis = new boolean[MAXN];
dist = new int[MAXN];
head = new int[MAXN];
edges = new Edge[MAXN];
Arrays.fill(dist, INF);
for (int i = 0; i < E; i++) {
int from = sc.nextInt();
int to = sc.nextInt();
int cost = sc.nextInt();
addEdge(from, to, cost);
}
dijkstra(s);
for (int i = 1; i <= V; i++) {
System.out.print(dist[i] + " ");
}
}
static class Node {
int to, cost;
Node (int to, int cost) {
this.to = to;
this.cost = cost;
}
}
static class Edge {
int to, w, next;
public Edge() { }
public Edge(int to, int w, int next) {
this.to = to;
this.w = w;
this.next = next;
}
}
}
三、Dijkstra 算法总结
- 一种使用贪心的策略解决单源最短路算法。
- 最坏时间复杂度为 ,可用 pq 优化为
- 不能解决负权图。