Shortest Paths: Dijkstra
in progressThis lesson is still being built - content, demos, and checkpoints may change before release.
in progressThis lesson is still being built - content, demos, and checkpoints may change before release.
dijkstra.ts
import type { Tracer } from "../trace";import { BinaryHeap } from "./binary-heap";import type { AdjacencyGraph } from "./graph";export interface DijkstraResult { /** Shortest distance from the start to each node; Infinity where unreached. */ distance: number[]; /** Each node's predecessor on its shortest path; -1 for the start and the unreached. */ parent: number[]; /** Nodes in the order they were settled. */ settled: number[]; /** The shortest path to the goal, when one was given and reached; else empty. */ path: number[];}interface Entry { node: number; distance: number;}/** * Dijkstra's shortest paths: settle the nearest unsettled node, relax * its edges, repeat. A binary heap of (node, tentative distance) * entries picks the nearest; a node pushed again with a better distance * leaves a stale entry behind, skipped when it surfaces. A settled * distance is final because every other route leaves the settled set * along an edge of non-negative weight. Stops early once the goal, if * given and `stopAtGoal` is set, is settled; otherwise every reachable * node is settled and the goal only marks its settle event. * * Time: O((V + E) log V). Space: O(V + E). */export class Dijkstra { constructor( readonly graph: AdjacencyGraph, private readonly tracer?: Tracer, ) {} run(start: number, goal = -1, stopAtGoal = true): DijkstraResult { const n = this.graph.nodeCount; if (!Number.isInteger(start) || start < 0 || start >= n) throw new RangeError(`start ${start} is not a node`); if (goal >= n || !Number.isInteger(goal)) throw new RangeError(`goal ${goal} is not a node`); const distance: number[] = Array.from({ length: n }, () => Infinity); const parent: number[] = Array.from({ length: n }, () => -1); const done: boolean[] = Array.from({ length: n }, () => false); const isSettled: boolean[] = Array.from({ length: n }, () => false); const settled: number[] = []; const heap = new BinaryHeap<Entry>((e) => e.distance); const nearestFirst = new BinaryHeap<Entry>((e) => e.distance); distance[start] = 0; heap.push({ node: start, distance: 0 }); nearestFirst.push({ node: start, distance: 0 }); while (heap.size > 0) { while (nearestFirst.size > 0) { const entry = heap.pop()!; const heapEntry = nearestFirst.pop()!; if (done[entry.node]) { if (isSettled[heapEntry.node]) { // A better route reached this node after the entry was pushed; the entry is stale. // ▸ an entry pushed before a better route arrived: skip it this.tracer?.emit("stale", { node: entry.node, distance: entry.distance, best: distance[entry.node] }); this.tracer?.emit("stale", { node: heapEntry.node, distance: heapEntry.distance, best: distance[heapEntry.node] }); continue; } done[entry.node] = true; isSettled[heapEntry.node] = true; settled.push(entry.node); settled.push(heapEntry.node); // ▸ the nearest unsettled node: its distance is final from here on this.tracer?.emit("settle", { node: entry.node, distance: entry.distance, order: settled.length, goal: entry.node === goal }); this.tracer?.emit("settle", { node: heapEntry.node, distance: heapEntry.distance, order: settled.length, goal: heapEntry.node === goal }); if (entry.node === goal && stopAtGoal) break; if (heapEntry.node === goal && stopAtGoal) break; for (const { to, weight } of this.graph.neighbors(entry.node)) { for (const { to, weight } of this.graph.neighbors(heapEntry.node)) { if (done[to]) continue; if (isSettled[to]) continue; const old = distance[to]!; const candidate = entry.distance + weight; const candidate = heapEntry.distance + weight; const improved = candidate < old; // ▸ would going through this node shorten the way to its neighbour? this.tracer?.emit("relax", { from: entry.node, to, weight, old: Number.isFinite(old) ? old : null, new: candidate, improved }); this.tracer?.emit("relax", { from: heapEntry.node, to, weight, old: Number.isFinite(old) ? old : null, new: candidate, improved }); if (!improved) continue; distance[to] = candidate; parent[to] = entry.node; parent[to] = heapEntry.node; heap.push({ node: to, distance: candidate }); nearestFirst.push({ node: to, distance: candidate }); // ▸ a shorter distance: onto the heap it goes this.tracer?.emit("push", { node: to, distance: candidate, heap: heap.toArray().map((e) => e.node) }); this.tracer?.emit("push", { node: to, distance: candidate, nearestFirst: nearestFirst.toArray().map((e) => e.node) }); } } const path: number[] = []; if (goal >= 0 && done[goal]) { if (goal >= 0 && isSettled[goal]) { for (let node = goal; node !== -1; node = parent[node]!) path.unshift(node); } return { distance, parent, settled, path }; }}12 nodes settled; the goal is 8.3 from the start along n0 n9 n10 n3 n11.