1358 字
7 分钟
最短路径:Dijkstra、Bellman-Ford、Floyd 的 TypeScript 实现
概述
最短路径 研究图中从一个点到另一个点的最低代价路径。根据边权是否为负、是否需要所有点对结果,常用算法包括 Dijkstra、Bellman-Ford 和 Floyd-Warshall。
前置知识
- 图的邻接表 / 边列表:不同算法适合不同表示
- 松弛操作:用更短路径更新距离
- 优先队列:用于优化 Dijkstra 的最小距离节点选择
问题定义
给定带权图、起点和终点或全部节点,计算路径权重之和最小的路径长度。
| 要素 | 说明 |
|---|---|
| 输入 | 顶点、带权边、起点或所有点 |
| 输出 | 单源距离数组、某个目标距离或所有点对距离矩阵 |
| 核心操作 | 松弛:dist[v] = min(dist[v], dist[u] + w) |
| 算法选择 | 无负权用 Dijkstra,有负权用 Bellman-Ford,多源用 Floyd |
核心原理:分步图解
Dijkstra 每次确定当前未访问节点中距离最小的点:
graph LR
A((0)) -- 2 --> B((1))
A -- 5 --> C((2))
B -- 1 --> C
B -- 4 --> D((3))
C -- 1 --> D从 0 出发:
- 初始
dist[0] = 0,其他为无穷大。 - 选择距离最小的未确定节点。
- 用它的出边更新邻居距离。
- 重复直到所有可达节点确定。
Dijkstra 依赖“当前最短节点不会再被更新”的贪心性质,因此不能处理负权边。
算法精细步骤
算法:Dijkstra(graph, start)输入:非负权图 graph,起点 start输出:start 到每个节点的最短距离
1. dist 全部初始化为 Infinity,dist[start] = 02. 将 [start, 0] 加入优先队列3. while 队列不为空:4. 取出距离最小的节点 u5. 如果该距离已过期,跳过6. 遍历 u 的所有边 u -> v, 权重 w:7. 如果 dist[u] + w < dist[v]:8. 更新 dist[v] 并加入队列9. 返回 dist复杂度分析:
| 算法 | 适用场景 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| Dijkstra 朴素版 | 无负权、稠密图 | O(V² + E) | O(V) |
| Dijkstra 堆优化 | 无负权、稀疏图 | O(E log V) | O(V + E) |
| Bellman-Ford | 可含负权边 | O(VE) | O(V) |
| Floyd-Warshall | 所有点对 | O(V³) | O(V²) |
TypeScript 实现
1. 图类型与朴素 Dijkstra
type WeightedGraph = [number, number][][];
function dijkstra(graph: WeightedGraph, start: number): number[] { const n = graph.length; const dist = new Array(n).fill(Infinity); const visited = new Array(n).fill(false); dist[start] = 0;
for (let i = 0; i < n; i++) { let u = -1; for (let j = 0; j < n; j++) { if (!visited[j] && (u === -1 || dist[j] < dist[u])) u = j; }
if (u === -1 || dist[u] === Infinity) break; visited[u] = true;
for (const [v, weight] of graph[u]) { if (dist[u] + weight < dist[v]) { dist[v] = dist[u] + weight; } } }
return dist;}2. 堆优化 Dijkstra
class MinHeap { private heap: [number, number][] = [];
push(item: [number, number]): void { this.heap.push(item); this.bubbleUp(this.heap.length - 1); }
pop(): [number, number] | undefined { if (this.heap.length === 0) return undefined; if (this.heap.length === 1) return this.heap.pop();
const top = this.heap[0]; this.heap[0] = this.heap.pop()!; this.bubbleDown(0); return top; }
get size(): number { return this.heap.length; }
private bubbleUp(index: number): void { while (index > 0) { const parent = Math.floor((index - 1) / 2); if (this.heap[parent][1] <= this.heap[index][1]) break; [this.heap[parent], this.heap[index]] = [this.heap[index], this.heap[parent]]; index = parent; } }
private bubbleDown(index: number): void { while (true) { const left = index * 2 + 1; const right = index * 2 + 2; let smallest = index;
if (left < this.heap.length && this.heap[left][1] < this.heap[smallest][1]) smallest = left; if (right < this.heap.length && this.heap[right][1] < this.heap[smallest][1]) smallest = right; if (smallest === index) break;
[this.heap[smallest], this.heap[index]] = [this.heap[index], this.heap[smallest]]; index = smallest; } }}
function dijkstraHeap(graph: WeightedGraph, start: number): number[] { const dist = new Array(graph.length).fill(Infinity); const heap = new MinHeap(); dist[start] = 0; heap.push([start, 0]);
while (heap.size > 0) { const [node, distance] = heap.pop()!; if (distance > dist[node]) continue;
for (const [next, weight] of graph[node]) { const nextDistance = distance + weight; if (nextDistance < dist[next]) { dist[next] = nextDistance; heap.push([next, nextDistance]); } } }
return dist;}3. Bellman-Ford
function bellmanFord(edges: [number, number, number][], n: number, start: number): number[] { const dist = new Array(n).fill(Infinity); dist[start] = 0;
for (let i = 0; i < n - 1; i++) { let updated = false;
for (const [from, to, weight] of edges) { if (dist[from] !== Infinity && dist[from] + weight < dist[to]) { dist[to] = dist[from] + weight; updated = true; } }
if (!updated) break; }
for (const [from, to, weight] of edges) { if (dist[from] !== Infinity && dist[from] + weight < dist[to]) { throw new Error('存在负权环'); } }
return dist;}4. Floyd-Warshall
function floydWarshall(graph: number[][]): number[][] { const n = graph.length; const dist = graph.map(row => [...row]);
for (let k = 0; k < n; k++) { for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (dist[i][k] + dist[k][j] < dist[i][j]) { dist[i][j] = dist[i][k] + dist[k][j]; } } } }
return dist;}工程优化:按图类型选算法
| 图特征 | 推荐算法 | 原因 |
|---|---|---|
| 非负权单源 | Dijkstra + 优先队列 | 速度快,最常用 |
| 有负权边 | Bellman-Ford | 能检测负权环 |
| 所有点对 | Floyd-Warshall | 实现简单,适合节点较少 |
| 边权都为 1 | BFS | 不需要最短路径权重算法 |
| 0/1 边权 | 0-1 BFS | 可用双端队列优化 |
实际工程中还要注意距离溢出、不可达节点表示、图是否为有向图,以及输入节点编号是否从 0 开始。
应用与局限
典型应用
- 地图导航、路由规划
- 网络延迟时间、任务链路成本
- 游戏寻路、图上资源分配
- 多源距离预处理
局限性
| 局限 | 说明 |
|---|---|
| Dijkstra 不支持负权 | 负权会破坏贪心正确性 |
| Floyd 成本高 | O(V³) 只适合节点数较小的图 |
| 图建模影响结果 | 边方向、权重和节点编号必须定义清楚 |
总结
graph LR
A[判断边权] --> B{有负权?}
B -->|否| C[Dijkstra]
B -->|是| D[Bellman-Ford]
A --> E{需要所有点对?}
E -->|是| F[Floyd-Warshall]核心要点:
- 最短路径的核心操作是松弛边。
- 无负权单源最短路优先使用 Dijkstra。
- 有负权边时使用 Bellman-Ford,并检测负权环。
- 所有点对最短路可用 Floyd,但要接受 O(V³) 成本。
最短路径:Dijkstra、Bellman-Ford、Floyd 的 TypeScript 实现
https://www.hehonglei.cn/technology/shortest-path/