3.5.10 · D3Graphs

Worked examples — Dijkstra's algorithm — greedy, priority queue, O((V+E) log V) — no negative edges

3,833 words17 min readBack to topic

This page runs Dijkstra through every kind of graph it can meet. Before touching a single number, we lay out a scenario matrix: a grid of the different situations the algorithm must survive. Then each worked example is tagged with the exact matrix cell it covers, so by the end no scenario is left unshown.

Two words we lean on the whole way — let us pin them to pictures first.

Before any example, let us fix the priority-queue vocabulary so that "pop", "push" and "skip" mean something concrete rather than assumed knowledge.


The scenario matrix

# Case class What makes it tricky Covered by
C1 Greedy re-relaxation closest vertex later improves a farther one Ex 1
C2 Tie in distances two vertices pop with equal Ex 2
C3 Unreachable vertex no path exists → stays Ex 3
C4 Zero-weight edge non-negative but weight Ex 4
C5 Stale heap entry a vertex sits twice in the PQ Ex 5
C6 Self-loop / parallel edges degenerate edges that must not corrupt Ex 6
C7 Negative edge (failure) limiting case where Dijkstra is wrong Ex 7
C8 Real-world word problem translate a story into a weighted graph Ex 8
C9 Exam twist: recover the path not just distances, but the route itself Ex 9
C10 Complexity / degenerate size dense vs sparse limiting behaviour Ex 10

Every cell C1–C10 gets its own example below.


Ex 1 — Greedy re-relaxation (cell C1)

Figure — Dijkstra's algorithm — greedy, priority queue, O((V+E) log V) — no negative edges

Figure s01 (alt text): four nodes A, B, C, D on a blueprint grid. The amber arrow is labelled ; the cyan arrows , , , form the alternative route. Walk-through: trace the two ways into — the single amber arrow (cost ) versus the two cyan arrows through (cost ). Then trace into — the winning path threads , hopping along cyan arrows only. Keep this picture in view for every step below.

There are two ways to reach : the direct amber edge costing , and the two-hop cyan path costing . The greedy order is what discovers the cheaper one.

  1. Init. , all else . Push . Why this step? We know we are at the source for free; everything else is unknown.
  2. Pop A(0). Relax : . Relax : . Why this step? From a finalized vertex we push improvements to neighbours.
  3. Pop C(1) — the heap holds ; the minimum is C. Relax : . Relax : . Why this step? Popping the closest vertex first is the greedy rule; it lets C fix B before B is ever finalized (the red-hot moment the figure's cyan path beats the amber one).
  4. Pop B(3). Relax : .
  5. Pop D(4). No outgoing edges. Done.

Answer: .


Ex 2 — A genuine tie (cell C2)

  1. Pop S(0). , . Why this step? The source is finalized first; relaxing its two edges seeds A and B with equal guesses — this is what manufactures the tie we want to study.
  2. Pop the min of {A(3),B(3)}. Ties are broken arbitrarily (say A). Relax : . Why this step? Correctness never depends on which equal-key vertex we pop; both give the same true distance.
  3. Pop B(3). Relax : , not , so no update. Why this step? The second symmetric route offers no improvement (the < guard rejects an equal value), proving the tie did not corrupt .
  4. Pop T(5). Done. Why this step? T has no outgoing edges; popping it just confirms its finalized distance and drains the queue.

Answer: , unaffected by tie order.


Ex 3 — Unreachable vertex (cell C3)

  1. Init. , . Why this step? Everyone except the source starts "unknown" (); Z's sticky note begins exactly like every other far city — we cannot yet tell it is an island.
  2. Pops: A→ relax B (); B→ relax C (); C→ nothing. Why this step? The core loop only ever pushes vertices we can reach; each pop finalizes a reachable city and relaxes its outgoing edges, following the chain A→B→C to its natural end.
  3. Z is never pushed — nothing ever relaxes an edge into it. Why this step? Z has in-degree , so no relaxation ever names it; the queue empties without touching Z, leaving its untouched. That is the correct "no path" report.

Answer: . The is the correct report: "no path exists."


Ex 4 — Zero-weight edge (cell C4)

  1. Pop A(0). Relax : . Relax : . Why this step? A edge is still valid; is genuinely reachable at cost .
  2. Pop B(0) (min of {B(0),C(5)}). Relax : . Why this step? Non-negativity (including exactly ) keeps the greedy invariant intact — no infinite loops, since edges never reduce a finalized value.
  3. Pop C(1). Done. Why this step? C has no outgoing edges; its pop simply finalizes the improved distance and empties the queue, confirming the free hop through B paid off.

Answer: .


Ex 5 — Stale heap entry (cell C5)

This is the lazy heap in action (defined up top): we never decrease a key — we push a fresh better pair and let the outdated one rot in the heap. A stale entry is a pair whose is larger than the current because a better pair overtook it. The guard that detects staleness is exactly the pseudocode line from the top of this page:

(d, u) = PQ.pop()
if d > dist[u]: continue     # d is stale -> skip
  1. Pop A(0). Push B with key and C with key . Heap: . Why this step? Standard relaxation from the source seeds both neighbours' first guesses.
  2. Pop C(1). Relax : , push . Heap now: — B is in twice. Why this step? C is closer than B(9), so it pops first and discovers a cheaper route into B; lazy deletion means we push rather than editing the old .
  3. Pop . Run the guard: d > dist[B]? ? No → process B, finalize at . Why this step? This is B's first non-stale pop, so its is now provably final.
  4. Pop . Run the guard: d > dist[B]? ? Yes → continue, skip. Why this step? The entry is a fossil from step 1, superseded by . Skipping it avoids re-relaxing B's neighbours from a wrong, inflated distance.

Answer: ; the stale pop does nothing.


Ex 6 — Self-loop and parallel edges (cell C6)

  1. Pop A(0). Relax self-loop : ? No → no update. Why this step? A self-loop with non-negative weight can never beat ; the relaxation guard < rejects it. (A negative self-loop would loop forever — another reason we forbid negatives.)
  2. Relax parallel edges : first ; then . Why this step? Dijkstra treats parallel edges independently; the cheaper one simply wins via the same guard.
  3. Pop B(2). Done. Why this step? B has no outgoing edges; its pop finalizes the cheaper parallel-edge value and drains the queue, confirming neither degenerate edge corrupted the result.

Answer: ; degenerate edges harmless.


Ex 7 — Negative edge, the failure case (cell C7)

We want ONE clean graph that makes Dijkstra return the wrong answer, and a crisp reason. To appreciate why the counter-example must be built carefully, note two facts first:

Figure — Dijkstra's algorithm — greedy, priority queue, O((V+E) log V) — no negative edges

Figure s02 (alt text): four nodes S, A, B, C on a blueprint grid. Cyan arrows , , ; the amber arrow is the dangerous negative edge. Walk-through: the amber arrow points back into A from B. But A sits at distance from S and gets popped almost immediately — before B (distance ) is ever processed. By the time the amber edge is relaxed, A is already stamped "done", so its improvement never propagates to C. The caption at the bottom states the wrong vs true value.

  1. Pop S(0). Relax : . Relax : . Why this step? Standard seeding of the source's neighbours; nothing negative touched yet.
  2. Pop A(0) — it is the heap minimum, so A is finalized at . Relax : . Why this step? Greedy pops the closest vertex; A(0) beats B(3), so A is declared done now — this premature finalization is the trap.
  3. Pop B(3). Relax : → this wants to set . But standard Dijkstra never re-opens a finalized vertex, so the improvement to A is ignored, and C is never re-relaxed. Why this step? This is the exact line where the correctness proof's "" assumption dies: a later negative edge tried to improve an already finalized vertex, and the algorithm has no mechanism to accept it.
  4. Heap empties. Dijkstra returns . Why this step? Termination is normal; the bug is silent — no crash, just a wrong number.

Dijkstra's output: . True answer: .


Ex 8 — Real-world word problem (cell C8)

Translate: vertices = {Depot,P1,P2,P3}, weights = minutes (all , so Dijkstra is valid).

  1. Pop Depot(0). , . Why this step? Depot is the source; relaxing its two outgoing flights seeds P1 and P2 with first guesses.
  2. Pop P2(2). Relax P2→P1: . Relax P2→P3: . Why this step? P2 is nearest; going through it lets us reconsider P1 (5 beats the direct 6).
  3. Pop P1(5). Relax P1→P3: , not → no change. Why this step? We must check whether the cheaper route into P1 opens a cheaper route onward to P3; here it ties, so the < guard rejects it.
  4. Pop P3(9). Done. Why this step? P3 has no outgoing edges; its pop finalizes the fastest time and drains the queue.

Answer: fastest time to P3 is minutes (route Depot→P2→P3, tying Depot→P2→P1→P3 = ).


Ex 9 — Exam twist: recover the actual path (cell C9)

Add one array, initialized to a sentinel: prev[v] = NULL for every vertex (this is the prev line already in the pseudocode at the top). NULL means "no known predecessor yet", so any vertex still holding NULL at the end is either the source or unreachable.

  1. Relax . Relax . Why this step? Each successful relaxation stamps who provided the current best route into the neighbour.
  2. improves B → overwrite . . Why this step? When a cheaper route into B is found (via C), the predecessor must be rewritten to match the new .
  3. improves D → overwrite . Why this step? D's best route now threads through B, so its predecessor updates in lock-step with .
  4. Reconstruct by walking prev from D backwards until we hit a vertex whose prev is NULL (that is the source ): . Reverse → . Why this step? Following predecessors reverses the discovery; stopping at NULL guarantees we halt at the source and never chase a dangling pointer.

Answer: shortest path is , total weight .


Ex 10 — Complexity in limiting cases (cell C10)

  1. Sparse, : . Why this step? With few edges the vertex term matters; heap operations scale with .
  2. Dense, : . Why this step? Edges dominate; every edge may push once, each push .
  3. Degenerate limit — Fibonacci heap gives : dense becomes , beating by a factor via amortized decrease-key from the Priority Queue (Binary Heap) upgrade. Why this step? In the densest limit the factor is the only thing left to shave; swapping the heap removes it, which is exactly when the upgrade pays off.

Answer: (a) , (b) with a binary heap.


Every cell, checked

Recall Did we hit all ten cells?

C1 Ex1 (re-relax) · C2 Ex2 (tie) · C3 Ex3 (unreachable) · C4 Ex4 (zero edge) · C5 Ex5 (stale) · C6 Ex6 (self-loop) · C7 Ex7 (negative fails) · C8 Ex8 (word problem) · C9 Ex9 (path recovery) · C10 Ex10 (complexity). Plus the read-once disclaimer covering directed/undirected and overflow. No gaps.


Connections

  • Dijkstra (Hinglish) — the parent note
  • Bellman-Ford algorithm — the fix for Ex 7's negative edges
  • BFS — the zero/unit-weight limit of these scenarios
  • Priority Queue (Binary Heap) — the stale-entry mechanism of Ex 5
  • Greedy algorithms — why the re-relax order (Ex 1) is provably safe
  • A* search — add a heuristic to the same relaxation loop
  • Johnson's algorithm — reweight so Dijkstra survives negatives