单源最短路径算法 Dijsktra算法 核心思想是“贪心+松弛(relax)”:
每一步选出当前已知距离最小的未定点,把它“固定”(finalize),然后用它去松弛(更新)与其相连的邻居距离。
一旦一个点被取出并固定,其最短距离就是最终最短值(在非负权条件下成立)。
伪代码 1 2 3 4 5 6 7 8 9 10 11 12 13 初始化:对于所有 v,dist[v] = +∞;dist[s] = 0 ;prev[v] = -1 把 (dist[s] , s) 放入优先队列 PQ while PQ 不空: (d, u) = PQ.pop () if d != dist[u] : continue for each 边 u -> v (权重 w): if dist[u] + w < dist[v] : dist[v] = dist[u] + w prev[v] = u PQ.push ( (dist[v] , v) )
Java实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 import java.util.*;public class Dijkstra { static final long INF = Long.MAX_VALUE / 4 ; static class Edge { int to; long w; Edge(int to, long w) { this .to = to; this .w = w; } } static class Result { long [] dist; int [] prev; Result(long [] dist, int [] prev) { this .dist = dist; this .prev = prev; } } public static Result dijkstra (List<List<Edge>> graph, int source) { int n = graph.size(); long [] dist = new long [n]; int [] prev = new int [n]; Arrays.fill(dist, INF); Arrays.fill(prev, -1 ); dist[source] = 0 ; PriorityQueue<long []> pq = new PriorityQueue <>(Comparator.comparingLong(a -> a[0 ])); pq.add(new long []{0 , source}); while (!pq.isEmpty()) { long [] cur = pq.poll(); long d = cur[0 ]; int u = (int ) cur[1 ]; if (d != dist[u]) continue ; for (Edge e : graph.get(u)) { int v = e.to; long nd = d + e.w; if (nd < dist[v]) { dist[v] = nd; prev[v] = u; pq.add(new long []{nd, v}); } } } return new Result (dist, prev); } public static List<Integer> reconstructPath (int target, int [] prev) { List<Integer> path = new ArrayList <>(); for (int at = target; at != -1 ; at = prev[at]) path.add(at); Collections.reverse(path); if (path.size() == 0 || prev[path.get(0 )] != -1 && path.get(0 ) != 0 ) { } return path; } public static void main (String[] args) { int n = 5 ; List<List<Edge>> g = new ArrayList <>(); for (int i = 0 ; i < n; i++) g.add(new ArrayList <>()); g.get(0 ).add(new Edge (1 ,2 )); g.get(0 ).add(new Edge (2 ,5 )); g.get(1 ).add(new Edge (2 ,1 )); g.get(1 ).add(new Edge (3 ,2 )); g.get(2 ).add(new Edge (3 ,3 )); g.get(2 ).add(new Edge (4 ,1 )); g.get(3 ).add(new Edge (4 ,2 )); Result res = dijkstra(g, 0 ); System.out.println("dist: " + Arrays.toString(res.dist)); System.out.println("path A->E: " + reconstructPath(4 , res.prev)); } }
其中if (d != dist[u]) continue; // 过期条目处理了过期条目,由于队列中会加入未更新的边值,例如A->B的初始值为10,而A经过C到B的值为2,那么在第一次入队时,[10,B]的数据会进入队列,而当C固定后,经过C到B的数据[2,B]会入队,此时[10,B]为未更新的数据,应该被略过。
复杂度 最小堆中每次更新是log(V)每条边最多遍历一次,共计E次,共计Elog(V)
最小堆中每次取最小值并删除是log(V),总共V个节点,共计Vlog(V)
时间复杂度为O((E+V)log(V)),对于稀疏图V=E为Vlog(V),对于稠密图$E=V^2$ 为Elog(V)
空间复杂度为O(E+V)邻接表所占空间
特点 用于计算带非负权 的有向图/无向图的单源最短路径。由于Dijsktra需要用到贪心算法,一个顶点的路径在确定后一定为从源点到该点的最小路径,如果存在负权,可能源点可以从之后的点通过负权获得一个更小的路径,将破坏贪心性质。
Bellman-Ford 算法
用一个 dist[] 数组维护从源 s 到各点的当前最短估计。
对 所有边 做“松弛(relax)”操作,重复 V-1 次(V = 节点数)。
因为任意不重复的最短路径最多包含 V-1 条边,经过 V-1 次全边松弛,所有简单路径都会被考虑到。
做完 V-1 次后,再做一次全边扫描:
若还能松弛说明存在 源可达的负权环 (最短路不存在或可任意减小)。
松弛操作:若 dist[u] + w < dist[v],则 dist[v] = dist[u] + w,并记录 prev[v] = u。
伪代码 1 2 3 4 5 6 7 8 9 10 11 12 initialize dist[v] = +INF for all v dist[s] = 0 for i = 1 to V-1 : for each edge (u, v, w): if dist[u] != INF and dist[u] + w < dist[v] : dist[v] = dist[u] + w prev[v] = ufor each edge (u, v, w): if dist[u] != INF and dist[u] + w < dist[v] : report "存在负权回路"
Java实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 import java.util.*;public class BellmanFord { static final long INF = Long.MAX_VALUE / 4 ; static class Edge { int u, v; long w; Edge(int u, int v, long w) { this .u = u; this .v = v; this .w = w; } } static class Result { long [] dist; int [] prev; boolean hasNegativeCycle; List<Integer> negativeCycle; Result(long [] dist, int [] prev, boolean hasNegativeCycle, List<Integer> negativeCycle) { this .dist = dist; this .prev = prev; this .hasNegativeCycle = hasNegativeCycle; this .negativeCycle = negativeCycle; } } public static Result bellmanFord (int n, List<Edge> edges, int source) { long [] dist = new long [n]; int [] prev = new int [n]; Arrays.fill(dist, INF); Arrays.fill(prev, -1 ); dist[source] = 0 ; boolean updated; for (int i = 0 ; i < n - 1 ; i++) { updated = false ; for (Edge e : edges) { if (dist[e.u] != INF && dist[e.u] + e.w < dist[e.v]) { dist[e.v] = dist[e.u] + e.w; prev[e.v] = e.u; updated = true ; } } if (!updated) break ; } int x = -1 ; for (Edge e : edges) { if (dist[e.u] != INF && dist[e.u] + e.w < dist[e.v]) { x = e.v; break ; } } if (x == -1 ) { return new Result (dist, prev, false , null ); } else { int y = x; for (int i = 0 ; i < n; i++) { y = prev[y]; } List<Integer> cycle = new ArrayList <>(); int cur = y; do { cycle.add(cur); cur = prev[cur]; } while (cur != y && cur != -1 ); Collections.reverse(cycle); return new Result (dist, prev, true , cycle); } } public static List<Integer> reconstructPath (int target, int [] prev) { List<Integer> path = new ArrayList <>(); if (target < 0 ) return path; for (int at = target; at != -1 ; at = prev[at]) path.add(at); Collections.reverse(path); return path; } public static void main (String[] args) { int n = 3 ; List<Edge> edges = new ArrayList <>(); edges.add(new Edge (0 , 1 , 4 )); edges.add(new Edge (0 , 2 , 2 )); edges.add(new Edge (2 , 1 , -3 )); Result r = bellmanFord(n, edges, 0 ); System.out.println("hasNegativeCycle = " + r.hasNegativeCycle); System.out.println("dist = " + Arrays.toString(r.dist)); if (r.hasNegativeCycle) { System.out.println("negative cycle nodes = " + r.negativeCycle); } else { System.out.println("path A->B = " + reconstructPath(1 , r.prev)); } } }
特点 用于计算含负权 的有向图/无向图的单源最短路径,但不允许负权回路。
复杂度
时间复杂度:O(V·E)(V = 顶点数,E = 边数)
空间复杂度:O(V + E)(边表 + dist / prev)
SPFA 算法 SPFA是Ford的优化版本,主要只在 某个顶点的距离被更新 时,才把它放进队列里,之后只松弛这个顶点的出边。这样避免了大量不必要的操作。
任意两点最短路径 Floyd算法 设有一个图 $G=(V,E)$,顶点数为 $n$。
用一个二维数组 dist[i][j] 表示从顶点i到顶点j的当前最短路径长度。
递推公式为 $$ dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j]) $$ 主要实现为三重循环
1 2 3 4 5 for k = 1. .n for i = 1. .n for j = 1. .n if dist[i][k] + dist[k][j] < dist[i][j]: dist[i][j] = dist[i][k] + dist[k][j]
复杂度 时间复杂度 :O(n³) (三重循环)。
空间复杂度 :O(n²)。
适合 顶点数较小(几百级别) 的稠密图。
特点 简单易实现,适用于任意权图(可含负边,但不能有负环)。
最小生成树 Kruskal Prim 并查集