最短路径
一、定义与背景
- 加权有向图:每条边具有权值(时间、距离等)的有向图。
- 最短路径:从起点 ( s ) 到终点 ( t ) 的路径中,总权值最小的路径。
- 单点最短路径问题:给定起点 ( s ),找到从 ( s ) 到所有可达顶点的最短路径。
- 应用:
- 地图:交叉路口(顶点)、公路(边)、距离/时间(权值)。
- 网络:路由器(顶点)、连接(边)。
- 任务调度:任务(顶点)、优先级限制(边)。
- 套汇:货币(顶点)、汇率(边)。
二、最短路径的性质
- 有向性:路径必须遵循边的方向。
- 权值灵活性:不一定代表距离,可为时间、成本等,不要求与距离成正比。
- 可达性:不可达顶点无最短路径,假设示例图强连通。
- 负权值:增加问题复杂性,需特殊处理。
- 简单性:最短路径通常不含环(忽略零权值环)。
- 唯一性:可能存在多条权值相等的最短路径,算法只需返回一条。
- 平行边与自环:选择权值最小的平行边,忽略自环。
最短路径树(SPT)
- 定义:以 ( s ) 为根,包含从 ( s ) 到所有可达顶点的最短路径的子图。
- 表示:父链接数组 ( edgeTo[] ),每条路径对应从 ( s ) 到顶点的唯一路径。
三、数据结构
1. 加权有向边(DirectedEdge
)
public class DirectedEdge {
private final int v, w; // 起点和终点
private final double weight; // 权值
public DirectedEdge(int v, int w, double weight) { this.v = v; this.w = w; this.weight = weight; }
public double weight() { return weight; }
public int from() { return v; }
public int to() { return w; }
}
2. 加权有向图(EdgeWeightedDigraph
)
public class EdgeWeightedDigraph {
private final int V; // 顶点数
private int E; // 边数
private Bag<DirectedEdge>[] adj; // 邻接表
public EdgeWeightedDigraph(int V) {
this.V = V; this.E = 0;
adj = (Bag<DirectedEdge>[]) new Bag[V];
for (int v = 0; v < V; v++) adj[v] = new Bag<>();
}
public void addEdge(DirectedEdge e) { adj[e.from()].add(e); E++; }
public Iterable<DirectedEdge> adj(int v) { return adj[v]; }
public Iterable<DirectedEdge> edges() {
Bag<DirectedEdge> b = new Bag<>();
for (int v = 0; v < V; v++) for (DirectedEdge e : adj[v]) b.add(e);
return b;
}
}
3. 最短路径 API
public class SP {
public SP(EdgeWeightedDigraph G, int s) {} // 构造最短路径树
public double distTo(int v) {} // 从 s 到 v 的距离
public boolean hasPathTo(int v) {} // 是否存在路径
public Iterable<DirectedEdge> pathTo(int v) {} // 返回路径
}
4. 数据结构支持
- ( edgeTo[v] ):从 ( s ) 到 ( v ) 的路径上的最后一条边。
- ( distTo[v] ):从 ( s ) 到 ( v ) 的最短路径长度,初始为无穷大,不可达时保持无穷大。
5. 松弛操作(Relaxation)
- 边的松弛:
- 检查 ( s \to v \to w ) 是否比当前 ( s \to w ) 更短。
- 若 ( distTo[w] > distTo[v] + e.weight() ),更新 ( distTo[w] ) 和 ( edgeTo[w] )。
private void relax(DirectedEdge e) {
int v = e.from(), w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
}
}
四、理论基础
1. 最优性条件(命题 P)
- ( distTo[w] \leq distTo[v] + e.weight() ) 对所有边成立时,( distTo[] ) 为最短路径长度。
- 证明:若存在有效边(违反条件),可构造更短路径,与最短性矛盾。
2. 通用算法(命题 Q)
- 初始化 ( distTo[s] = 0 ),其他为无穷大,反复放松边直到无有效边。
- 时间依赖放松顺序,正确性由最优性条件保证。
五、算法实现
1. Dijkstra 算法(非负权值)
- 思想:贪心策略,每次选择 ( distTo[] ) 最小的非树顶点放松。
- 实现:
public class DijkstraSP {
private double[] distTo;
private DirectedEdge[] edgeTo;
private IndexMinPQ<Double> pq;
public DijkstraSP(EdgeWeightedDigraph G, int s) {
distTo = new double[G.V()];
edgeTo = new DirectedEdge[G.V()];
pq = new IndexMinPQ<>(G.V());
for (int v = 0; v < G.V(); v++) distTo[v] = Double.POSITIVE_INFINITY;
distTo[s] = 0.0; pq.insert(s, 0.0);
while (!pq.isEmpty()) relax(G, pq.delMin());
}
private void relax(EdgeWeightedDigraph G, int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
if (pq.contains(w)) pq.changeKey(w, distTo[w]);
else pq.insert(w, distTo[w]);
}
}
}
}
- 性能:时间 ( O(E \log V) ),空间 ( O(V) )。
- 证明(命题 R):非负权值保证 ( distTo[] ) 单调递减,优先队列确保最优性。
2. 无环图算法(AcyclicSP)
public class AcyclicSP {
private double[] distTo;
private DirectedEdge[] edgeTo;
public AcyclicSP(EdgeWeightedDigraph G, int s) {
distTo = new double[G.V()];
edgeTo = new DirectedEdge[G.V()];
for (int v = 0; v < G.V(); v++) distTo[v] = Double.POSITIVE_INFINITY;
distTo[s] = 0.0;
Topological top = new Topological(G);
for (int v : top.order()) relax(G, v);
}
}
- 性能:时间 ( O(E + V) ),空间 ( O(V) )。
- 证明(命题 S):拓扑顺序确保每条边放松一次即达最优。
3. Bellman-Ford 算法(支持负权值)
- 思想:基于队列,反复放松边,检测负权重环。
- 实现:
public class BellmanFordSP {
private double[] distTo;
private DirectedEdge[] edgeTo;
private boolean[] onQ;
private Queue<Integer> queue;
private Iterable<DirectedEdge> cycle;
public BellmanFordSP(EdgeWeightedDigraph G, int s) {
distTo = new double[G.V()];
edgeTo = new DirectedEdge[G.V()];
onQ = new boolean[G.V()];
queue = new Queue<>();
for (int v = 0; v < G.V(); v++) distTo[v] = Double.POSITIVE_INFINITY;
distTo[s] = 0.0; queue.enqueue(s); onQ[s] = true;
while (!queue.isEmpty() && !hasNegativeCycle()) {
int v = queue.dequeue();
onQ[v] = false;
relax(G, v);
}
}
private void relax(EdgeWeightedDigraph G, int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
if (!onQ[w]) { queue.enqueue(w); onQ[w] = true; }
}
}
}
private void findNegativeCycle() { /* 检测 edgeTo[] 中的环 */ }
}
- 性能:时间 ( O(EV) )(最坏情况),空间 ( O(V) )。
- 证明(命题 Y):( V-1 ) 轮后无有效边则最优,否则存在负权重环。
六、应用
1. 无环图最长路径
- 问题:找总权值最大的路径。
- 方法:将权值取反,求最短路径。
- 性能:( O(E + V) )(命题 T)。
2. 并行任务调度
- 问题:在优先级限制下,最短时间内完成任务。
- 方法:构造无环图,求从起点到终点的最长路径(关键路径)。
- 性能:线性时间(命题 U)。
3. 套汇
- 问题:检测汇率中的盈利环。
- 方法:权值取 ( -\ln(w) ),用 Bellman-Ford 检测负权重环。
- 性能:( O(EV) )(命题 Z)。
七、性能比较
算法 |
限制条件 |
时间复杂度 |
空间复杂度 |
优势 |
Dijkstra |
权值非负 |
( E \log V ) |
( V ) |
最坏情况性能优 |
AcyclicSP |
无环 |
( E + V ) |
( V ) |
无环图最优 |
Bellman-Ford |
无负权重环 |
( E + V \sim EV ) |
( V ) |
适用性广,支持负权值 |
八、展望
- 历史:Dijkstra (1959)、Bellman-Ford (20世纪50年代)。
- 优化:斐波那契堆改进 Dijkstra 至 ( E + V \log V )。
- 开放问题:负权值图的最坏情况线性时间算法尚未解决。
九、总结
- 核心:松弛操作与最优性条件。
- 算法:
- Dijkstra:贪心,适用于非负权值。
- AcyclicSP:拓扑排序,适用于无环图。
- Bellman-Ford:队列迭代,处理负权值。
- 适用性:根据图的性质选择算法,满足多样化需求。