Single-Source Shortest Paths
By the end of this lesson you will be able to explain what a shortest path even means once edges can be negative, trace initializeSingleSource, relax, bellmanFord, dagShortestPaths and dijkstra line by line on a picture of the graph (with the priority queue, topological order and pass numbers all visible), implement every one of them in idiomatic Dart, know exactly when each is safe to use (and see, concretely, where Dijkstra breaks with a negative edge), watch each of six shortest-path properties (triangle inequality, upper bound, no path, convergence, path relaxation, predecessor subgraph) happen on real data, find the critical path of a project (a PERT chart) with dagShortestPaths, and turn a system of difference constraints into a shortest-paths problem.
Weighted graphs, paths, and what δ(u,v) means
A graph is dots joined by arrows. The dots are vertices, the arrows are edges, and in a directed graph every arrow points one way (you may drive from u to v but not back unless a second arrow says so). If each arrow also carries a number, the graph is weighted and the number is the edge's weight w(u,v). This lesson asks one question: starting from one fixed vertex, the source s, what is the cheapest way to reach every other vertex? (If you have not met graphs yet, the graph basics lesson (BFS, DFS, topological sort, SCC) introduces the picture and the adjacency list we reuse here.)
Formally: G = (V, E) is a weighted directed graph with a weight function w: E → ℝ. For a path p = ⟨v₀, v₁, …, v_k⟩ its weight is w(p) = Σᵢ₌₁ᵏ w(v_{i-1}, v_i). The shortest-path weight from u to v is δ(u,v) = min{w(p) : p is a path from u to v} if any path exists, and ∞ otherwise. A shortest path from u to v is any path p with w(p) = δ(u,v).
Dart: paths, their weights and δ as code. bruteDelta takes the minimum over every simple path (tiny graphs only), reproduces the small example above (the cheapest route has more edges), shows δ = ∞ for “no path”, and shows bellmanFord returning false where δ would be −∞. The helpers negEdgeGraph(), dagGraph() and roadGraph() build the three example graphs used below (vertices are the ints 0, 1, 2, … in the order the page names them).
Two structural facts drive every algorithm in this lesson:
Fact B — the triangle inequality: for every edge (u,v) ∈ E, δ(s,v) ≤ δ(s,u) + w(u,v). The cheapest way to reach v can never be worse than "reach u the cheapest way, then take this one edge" — otherwise that would BE a cheaper way to reach v. This single inequality is the exact condition every relax call is trying to enforce.
Dart: Fact A checked on every subpath of every shortest path of the route-cost graph. The subpath ⟨vi, …, vj⟩ is the 0-indexed slice p.sublist(i, j + 1).
Every algorithm below maintains, for each vertex v, two labels. d[v] is the shortest-path estimate: our current best guess of the cost to reach v, always ≥ δ(s,v) and shrinking toward it. π[v] (read "pi") is the predecessor: the vertex just before v on the best path found so far, or null ("nothing yet"). Once an algorithm finishes and d[v] = δ(s,v) for every v, the edges {(π[v], v) : π[v] ≠ null} form a shortest-paths tree — following π pointers backward from any v traces out an actual shortest path from s.
Starting point: initializeSingleSource
Every shortest-path algorithm starts from the same blank slate: an honest, pessimistic guess for each vertex. The procedure gives every vertex the estimate ∞ and the predecessor null, then fixes the one thing we know for certain — the source costs 0 to reach from itself.
Representation. Each vertex is a plain integer id (0, 1, 2, …); d and π are parallel Lists indexed by that id. "Nothing yet" is Dart's null, and ∞ is double.infinity.
Reading the three runs: the loop on lines 1–3 stamps the same pair of values on every vertex, and only line 4 treats the source differently. The custom player has no edges at all, which is the point: the procedure is pure per-vertex setup, Θ(V).
Dart implementation:
Input size → what is feasible. V ≤ 106 → the Θ(V) loop is 106 assignments, instant. The procedure is never the bottleneck: the algorithm that follows costs far more.
The one move: relax
To relax an edge (u, v) means to test whether the edge gives a shortcut to v, and to take the shortcut if it does. It is the only operation that ever changes an estimate in this lesson; every algorithm below is just a different schedule of relax calls.
The three players show the only two outcomes: the edge improves v (lines 2–4 run) or it does not (the function falls through to line 5). The third player lets you try a zero-weight edge, where a tie must NOT count as an improvement because the test is a strict ">".
d[v] (or leaves it unchanged) — it can never make an estimate worse. This is exactly why running relax extra times, or in a different order, never breaks correctness: by the upper-bound property (see the last section) d[v] never drops below δ(s,v), so the worst extra relax calls can do is nothing. What they cannot do is skip the edges that matter — that is what each algorithm's schedule guarantees.Dart implementation:
Input size → what is feasible. one relax is a comparison and an addition, O(1). Bellman-Ford on V = 1000, E = 5000 calls it about 5·106 times; Dijkstra and dagShortestPaths call it exactly E times.
Bellman-Ford: sweep every edge, again and again
Bellman-Ford is the "no assumptions" algorithm: it accepts negative edge weights, and it tells you when a negative cycle makes the question meaningless. It returns true if the estimates are the true shortest distances and false if a negative cycle is reachable from s.
n − 1 times (n = |V|). Why n − 1? Some shortest path has at most n − 1 edges (a simple path can't repeat a vertex), so after that many full sweeps, every shortest path's edges have been relaxed in order at least once — guaranteeing every d[v] has settled to δ(s,v) by the path-relaxation property (last section), as long as no negative-weight cycle is reachable from s. A final sweep checks: did anything STILL want to improve? If so, a negative cycle exists (with no such cycle every d would already equal δ and satisfy the triangle inequality, so no edge could improve).true, a = 3 via s→b→a.In the first player (the route-cost graph) the edge order is fixed and several vertices improve more than once (c first drops to 15, then to −1; d improves three times: 5, 4, then 2), which is why a single sweep is not enough. In the second player the cycle s→t→x→s has weight 1 + 1 − 3 = −1: every lap lowers the estimates, the n − 1 passes cannot stop that, and the check sweep catches it. The third player has an unreachable vertex c, which keeps d = ∞ and does not disturb the answer.
false, the d values are meaningless. (2) "Detecting a negative cycle" only concerns cycles reachable from s: a negative cycle in a part of the graph the source cannot reach never triggers false, and it is correct that it does not (it cannot affect any δ(s,·)).Analysis visual: what pass i guarantees
The correctness argument is one sentence: "after pass i, every d is at most the cost of the best path that uses at most i edges." The tables below print both quantities side by side — the real d[v] after each pass (top table) and the exact "best path with ≤ i edges" optimum (bottom table) — so you can watch the first stay at or below the second and land on δ by pass n−1. Cells turn green when they equal δ.
The second player is the adversarial case: the chain's edges are listed in reverse order, so each pass can only push the information one hop, and the last vertex needs all n−1 passes. With a friendlier edge order the d row runs ahead of the "≤ i edges" row and converges earlier — that is why stopping early after a pass with no change (see the early-stopping question in the bank) is a safe optimisation.
Dart implementation:
Dart: the exact relax count of the double loop, (V − 1)·E, measured on graphs with V = 10, 100, 1000 and E = 3V; and the correctness idea “after pass i, d[v] is at most the best weight of a path with at most i edges”, checked pass by pass on the route-cost graph.
Input size → what is feasible. V ≤ 1000, E ≤ 5000 → (V−1)·E ≈ 5·106 relaxations, instant. V = 104, E = 5·104 → 5·108, several seconds. V = 105, E = 2·105 → 2·1010, hopeless: then you need non-negative weights (Dijkstra) or a DAG.
false exactly when a negative cycle is reachable from s. Handles negative edges. Pass i settles every vertex whose shortest path has ≤ i edges.Shortest paths in a directed acyclic graph
A DAG (directed acyclic graph) has no cycles at all. That single fact removes the hard part of the problem: with no cycles there are no negative cycles, so shortest paths are always well defined, and there is an order in which a single sweep is enough.
The second player shows unreachable vertices (dimmed): the sweep still visits them, but ∞ + w = ∞ never improves anything. The third player rejects any edge list containing a cycle with a clear message, because the topological order (and the whole one-pass argument) would not exist.
Operation counts for one run on the example DAG (weights irrelevant for counting), gathered by the same JavaScript implementation that drives the players:
Dart implementation (uses a DFS-based topological sort, the same technique as in the graph basics lesson):
Dart: dagShortestPaths visits each vertex once and relaxes each edge once. The code counts both on random acyclic graphs with V = 100, 1000, 5000 and E = 3V and checks the distances on the example DAG (negative weights, one pass).
Input size → what is feasible. V = 105, E = 2·105 → V + E = 3·105 steps, instant (the recursive DFS here goes as deep as the longest path, so for V in the hundreds of thousands use an explicit stack or Kahn’s algorithm). The same instance would cost Bellman-Ford 2·1010.
Application: PERT charts and the critical path
A PERT chart (Program Evaluation and Review Technique) models a project as a DAG: edges are jobs and edge weights are how long each job takes; vertices are milestones ("events") that can be reached only after every job leading in has finished. A critical path is a longest path through the DAG: it is the chain of jobs that decides the earliest possible finish, because no other chain takes longer.
The trick: negate every edge weight and run dagShortestPaths — the shortest path under negated weights is the longest path under the real ones. Negative weights are harmless here, exactly because a DAG has no cycles. In the players below, the number under each event is its earliest start time (the negated d), and edge labels are job durations.
Dart implementation (reuses dagShortestPaths on negated durations):
Input size → what is feasible. V ≤ 105 activities and E ≤ 2·105 dependencies → Θ(V + E) ≈ 3·105 steps. Checking every path instead would be exponential in the worst case.
Dijkstra's algorithm: settle the nearest vertex first
Dijkstra's algorithm is the fast choice for graphs whose weights are all non-negative. It settles vertices one at a time in order of increasing distance, so each edge needs relaxing only once.
The first player runs on the road graph: watch the queue shrink and the settled set grow, always taking the smallest key. The second player is the counterexample: a negative edge lets a vertex that is already settled get a better distance later, and Dijkstra never looks back (red = wrong final values). The third player rejects negative weights up front.
Analysis visual 1: the loop invariant
Dijkstra is correct because of this invariant: "whenever a vertex is taken, its d already equals δ". The players below check that sentence against the true δ (from Bellman-Ford) at every extraction: green rows keep the invariant, red rows break it. The proof idea, visible in the captions: any competing route to the extracted u must leave the settled set through some vertex y still in the queue; since u had the minimum key, d[y] ≥ d[u], and the rest of the route only adds non-negative weight — so it cannot beat d[u]. A negative edge is exactly what makes "only adds" false.
Analysis visual 2: cost table
The running time is not magic; it is two counters. The tables count how many array slots the minimum search scans (V per iteration, hence V²) and how many relax calls occur (one per edge, hence E), then price the same run with a binary heap and a Fibonacci heap.
Dart implementation (array-based priority queue, the simplest version):
Dart: Dijkstra’s cost counted (array version: V² slot scans and E relaxations; heap version: an ordered set with decrease-key = remove + add), the three bounds as functions with the sparse-versus-dense comparison, and the loop invariant as a test: d[u] = δ(s, u) at every extraction, true on the road graph and false with a negative edge.
Input size → what is feasible. V ≤ 1500 dense (E ≈ 106) → array Dijkstra V² ≈ 2.25·106 steps. V = 105, E = 3·105 → heap Dijkstra (V + E) lg V ≈ 6.6·106, while the array version would need V² = 1010, hopeless. Any negative weight → switch to Bellman-Ford.
Difference constraints as a shortest-path problem
A system of difference constraints is a list of inequalities in which every line compares just two unknowns: x_j − x_i ≤ b. The question is whether numbers exist that satisfy all lines at once, and if so to find them. It is a special case of linear programming that needs no LP machinery at all. Variables are named x₀, x₁, … (the same numbering the Dart code uses).
Build a constraint graph: one vertex per variable, plus one extra start vertex z with a zero-weight edge to every variable vertex (so every variable is reachable). For every constraint x_j − x_i ≤ b, add an edge (i, j) with weight b. Then run Bellman-Ford from z: if it detects a negative-weight cycle, the system is infeasible (a negative cycle among the constraint edges means the corresponding inequalities sum to something impossible, like 0 ≤ −1). Otherwise, setting each x_i = δ(z, i) satisfies every constraint (the triangle inequality on the constraint graph's edges IS exactly "x_j − x_i ≤ b" once x_i := d[i]).
The three players above build the constraint graph edge by edge from your constraints (the last one accepts your own system, in the exact format xj-xi<=b, for example x1-x0<=3). The next four run the algorithm and check the outcome.
Running Bellman-Ford on a constraint graph you type in
Checking the solution — or proving there is none
Dart implementation (builds the constraint graph, then reuses bellmanFord above):
And a checker for a candidate assignment (used by the tests to confirm that Bellman-Ford's output satisfies every constraint):
Dart: the facts about the constraint graph as executable checks: the Bellman-Ford distances solve the system, every δ(z, i) ≤ 0, adding a constant to every x[i] keeps every constraint true, the graph has n + 1 vertices and m + n edges so the main loop does n·(m + n) relaxations, and a negative cycle means no solution.
Input size → what is feasible. n = 1000 variables and m = 5000 constraints → n·(m + n) = 6·106 relaxations, instant. n = 104, m = 105 → 1.1·109, too slow for one second; use an early-stopping Bellman-Ford, a queue-based variant, or an LP solver.
Six properties that explain why relaxing works
This is where every "why does this work?" gets answered. Instead of one long proof per algorithm, the algorithms rest on six short properties — facts that hold no matter which order you relax edges in. Each player below runs a real relaxation sequence on a graph (default: s→a 5, s→b 2, b→a 1, a→c 3, c→t −1, plus x→s 4 where x is unreachable; the edge order is deliberately unfriendly so several passes are needed) and shows the property happening. All six accept your own source | u->v:w, … edge list (no negative cycle reachable).
1 · Triangle inequality
For every edge (u,v): δ(s,v) ≤ δ(s,u) + w(u,v). Why it is true: the route "cheapest to u, then this edge" is one candidate path to v, and δ(s,v) is the minimum over all candidates. Tight edges (equality) are the ones a shortest-paths tree may use.
Dart: the triangle inequality as a function over every edge. randomGraph is the seeded test-graph generator reused by the property panels below.
2 · Upper-bound property
d[v] ≥ δ(s,v) for all v after initializeSingleSource and after any sequence of relaxations; and once d[v] = δ(s,v) it never changes again. Why: each relax sets d[v] to d[u] + w, and d[u] + w ≥ δ(s,u) + w ≥ δ(s,v) by the triangle inequality; a value at the floor cannot go lower.
Dart: 200 random graphs, 60 arbitrary relax calls each, with the upper bound, “right after relax, d[v] ≤ d[u] + w” and “a value at its floor never changes” tested after every call.
3 · No-path property
If there is no path from s to v, then d[v] = δ(s,v) = ∞ always. Why: by the upper bound d[v] ≥ ∞, so it can never leave ∞. This is why a final d = ∞ is a trustworthy "unreachable" verdict.
Dart: after arbitrary relaxations every unreachable vertex still has d = ∞ and π = null.
4 · Convergence property
If s ⇝ u → v is a shortest path and d[u] = δ(s,u) at some moment before relax(u,v,w), then d[v] = δ(s,v) at every moment after. Why: the relaxation makes d[v] ≤ δ(s,u) + w(u,v) = δ(s,v), and the upper bound forbids going lower.
Dart: whenever the premise holds (the tail is exact and the edge lies on a shortest path), the head is exact right after relax and stays exact.
5 · Path-relaxation property
If p = ⟨v₀,…,v_k⟩ is a shortest path from s = v₀ and its edges are relaxed in the order (v₀,v₁), …, (v_{k−1},v_k) — with any other relaxations mixed in — then d[v_k] = δ(s,v_k). Why: apply the convergence property once per edge, left to right. This is the property that proves Bellman-Ford (n−1 full sweeps contain every ordered subsequence) and dagShortestPaths (topological order relaxes path edges in order).
Dart: relax the edges of a shortest path in path order, mixing in unrelated relaxations between them; the last vertex ends exact.
6 · Predecessor-subgraph property
The predecessor subgraph Gπ has an edge (π[v], v) for every vertex with a predecessor. If no negative cycle is reachable from s, Gπ is always a tree rooted at s; once every d[v] = δ(s,v), it is a shortest-paths tree. Why: each relax replaces a vertex's single parent, so it stays one parent per vertex; a cycle of parent pointers would mean a negative cycle. The second player shows the failure when such a cycle exists.
Dart: predecessorSubgraphIsTree is true after every relaxation when no negative cycle is reachable, isShortestPathTree is true at the end, and with a reachable negative cycle the parents loop (so the guarantee fails).
Input size → what is feasible. these six properties are proof tools, not algorithms: the checks above run on graphs of 6 to 7 vertices (a few thousand relaxations), which is all a property test needs. Each property holds for every graph size.
Quiz
Interview questions
Cheat sheet
| Algorithm / idea | Time (best / avg / worst) | Space | Negative edges? | Negative cycle? | When to use |
|---|---|---|---|---|---|
| initializeSingleSource | Θ(V) always | O(V) | n/a | n/a | Setup step every single-source algorithm calls first |
| relax | Θ(1) always | O(1) | n/a | n/a | The one primitive every algorithm below is built from |
| Bellman-Ford | Θ(V+E) with early stop once d has converged; O(VE) worst; the plain version is Θ(VE) always | O(V) | Yes | Detects it (returns false) | General graphs, negative edges allowed, need cycle detection; also difference constraints, arbitrage |
| dagShortestPaths | Θ(V + E) always | O(V) | Yes (no cycles exist to go negative around) | Impossible in a DAG | Graph is known acyclic (task scheduling, PERT charts, longest path via negation) |
| PERT critical path | Θ(V + E) always | O(V) | Durations ≥ 0 (negated internally) | Needs an acyclic graph | Earliest project finish, zero-slack jobs |
| Dijkstra (array queue) | Θ(V²) always (E ≤ V²) | O(V) | No — gives silently wrong answers | Not detected: terminates but returns wrong values | Non-negative weights, dense graphs |
| Dijkstra (binary heap) | O((V+E) lg V) | O(V) | No | — | Non-negative weights, sparse graphs |
| Dijkstra (Fibonacci heap, see Fibonacci heaps) | O(V lg V + E) amortised | O(V) | No | — | Non-negative weights, very large sparse graphs |
| Difference constraints | O((n+1)(n+m)) via Bellman-Ford | O(n + m) | Yes | = infeasible system | Scheduling / deadline systems modelled as x_j − x_i ≤ b |
| The six shortest-path properties | — (proof tools) | — | — | Tree guarantee needs none reachable | Why relax schedules work: triangle, upper bound, no path, convergence, path relaxation, predecessor subgraph |