最短路径与拓扑排序
本节目标
- 理解 Dijkstra 的"贪心 + 优先队列"本质
- 掌握 Kahn 算法做拓扑排序
- 能区分"无权最短路(BFS)"与"带权最短路(Dijkstra)"
Dijkstra(边权非负):每次从优先队列取出"当前距离最小"的节点,松弛它的邻居。
// 运行环境:Node.js 14+
// 保存为 dsa-l16.js,执行:node dsa-l16.js
// 用简单数组当优先队列(演示用,生产用 MinHeap)
function dijkstra(graph, start) {
const dist = {} // 起点到各点的最短距离
for (const v in graph) dist[v] = Infinity
dist[start] = 0
const visited = new Set()
while (visited.size < Object.keys(graph).length) {
// 取未访问中距离最小的节点
let u = null, best = Infinity
for (const v in dist) if (!visited.has(v) && dist[v] < best) { best = dist[v]; u = v }
if (u === null) break
visited.add(u)
for (const [v, w] of graph[u]) { // 松弛:经 u 到 v 更近则更新
if (dist[u] + w < dist[v]) dist[v] = dist[u] + w
}
}
return dist
}
const g = { A: [['B', 4], ['C', 2]], B: [['D', 3]], C: [['B', 1], ['D', 5]], D: [] }
console.log(dijkstra(g, 'A')) // { A:0, B:3, C:2, D:6 }
// 拓扑排序(Kahn):不断删"入度为 0"的节点
function topoSort(n, edges) {
const indeg = new Array(n).fill(0)
const adj = Array.from({ length: n }, () => [])
for (const [u, v] of edges) { adj[u].push(v); indeg[v]++ }
const q = []; for (let i = 0; i < n; i++) if (indeg[i] === 0) q.push(i)
const res = []
while (q.length) {
const u = q.shift(); res.push(u)
for (const v of adj[u]) if (--indeg[v] === 0) q.push(v)
}
return res.length === n ? res : [] // 有环则返回空
}
console.log(topoSort(4, [[0,1],[1,2],[2,3]])) // [0,1,2,3]名词解释
- 最短路径(Shortest Path):图中从起点到终点代价最小的路线。无权图用 BFS(边数最少);带非负权用 Dijkstra;有负权用 Bellman-Ford。
- 松弛(Relaxation):
if (dist[u]+w < dist[v]) dist[v]=dist[u]+w——尝试用更短的路径更新距离,是 Dijkstra/最短路的核心操作。 - 拓扑排序(Topological Sort):把有向无环图(DAG)的顶点排成线性序列,使所有边 u→v 都满足 u 在 v 前。用于任务依赖排序(如课程选修、构建顺序)。
课后练习
- Dijkstra 为什么要求边权非负?
- 答案:它靠"一旦取出节点 u 就确定其最短路"的贪心,若有负权,后面可能经负边让已确定的距离更短,贪心失效。负权要用 Bellman-Ford。
- 拓扑排序返回空数组意味着什么?
- 答案:说明图中存在环(入度永远无法全部清零),无法排出合法顺序;拓扑排序只对 DAG 有效。
总结
最短路径与拓扑排序,是图论里最"实用"的两个分支。Dijkstra 的精髓在于"贪心 + 优先队列":它每次锁定一个"当前最近"的节点,因为边权非负,这个最近值不可能再被刷新——这一步贪心的正确性,是整个算法的根基。理解它,你就明白为什么负权会破坏 Dijkstra(贪心前提崩了),以及为什么生产环境一定要用堆而不是线性扫描。拓扑排序则解决另一类问题:当任务之间有"先后顺序"依赖(A 必须在 B 前),Kahn 算法用"删入度为 0 节点"的方式把依赖关系线性化,是构建系统、包管理、课程排课的核心算法。我对初学者的建议是:看到"最少步数"想 BFS,看到"带权距离"想 Dijkstra,看到"依赖顺序"想拓扑——把这三句话焊死,图论应用题就破了一大半。