Dijkstra’s Algorithm

Context: FIT2004_MOC · single-source shortest distance on a weighted graph — BFS with the queue swapped for a min-Heap Parent Framework: Graph

Quick Revision

  • 🎯 Objective: serve the closest non-finalised vertex, relax its out-edges ➔ every served vertex leaves with its final .
  • 📦 Core Components: min-Heap discovered ➔ serve/update | v.distance ➔ best-known | v.previous ➔ backtracking for the path | v.visited ➔ finalised.
  • ⚡ Key Constraint: non-negative weights only — the greedy finalisation is what a negative edge breaks, and it is also what the correctness proof rests on.

📝 How It Works

1. The Two Paradigms It Fuses

  • Dynamic programming ➔ optimal substructure: is built from plus — subproblem answers are reused, never recomputed.
  • Greedy ➔ the closest reachable vertex is finalised immediately: if is nearest to , no detour can beat it ➔ Greedy Algorithm.
  • Where greed dies ➔ a negative edge makes the detour cheaper after was finalised ➔ Dijkstra is wrong on negative edges. (It may still happen to be right when the negative edge sits outside any cycle — never rely on it.)
  • The escape hatches ➔ Bellman-Ford (single source, tolerates negative edges) and Floyd-Warshall (all pairs) — later in the unit.

2. The Diff From BFS

  • Queue ➔ priority queue ➔ BFS serves by arrival, Dijkstra by smallest distance; that single ADT swap (Priority Queue (ADT)) is the whole algorithm.
  • Increment ➔ relaxation ➔ BFS sets v.distance = u.distance + 1; Dijkstra sets v.distance = u.distance + w and, on an already discovered vertex, updates downward if .
  • Two flags, not one ➔ discovered (in the heap, estimate may still fall) vs visited/finalised (served, distance frozen). A finalised neighbour is skipped, never relaxed.
  • Unweighted case ➔ if all weights are equal, BFS already solves it in — do not pay for a heap you do not need.
  • Same loop, third variant ➔ change the relaxation to v.distance = w and the algorithm computes a Minimum Spanning Tree instead ➔ Prim’s Algorithm.

3. Reading the Answer Out

  • Distance ➔ v.distance at the moment is served; it never changes again.
  • Path ➔ set v.previous = u on every successful relaxation, then backtrack from the target and reverse.
  • Single target ➔ terminate the moment the target is moved to visited; the remaining heap is irrelevant.
  • Unreachable ➔ a vertex never discovered keeps ; the loop ends with an empty heap, not an error.
  • Threshold cutoff ➔ stop once the served key exceeds a stated bound ; everything still in the heap has a key , so nothing cheaper can appear.
  • Single target, undirected only ➔ iff is undirected, so the target may be used as the source; on a digraph that is wrong unless every edge is reversed first.
  • Many sources ➔ seeding the heap with every source at is the safe form; a weighted super source works only with weight- links to each , and is the easier one to get wrong ➔ Uninformed Search (BFS and DFS) §7.

4. Beating the When the Weights Are Restricted (applied P6, P9)

  • Where the lives ➔ entirely in the priority queue. Restrict the weights enough and a cheaper queue exists, dropping the whole algorithm to .
  • Zero-one shortest paths ➔ weights . A BFS queue always holds vertices of only two distinct distances and ; a -weight edge lands its target at the same distance, so push it to the front and a -weight edge to the back of a deque. That deque is a priority queue with at most two keys ⟹ per operation.
  • Still needs relaxation ➔ unlike BFS, the first discovery is no longer final (a vertex may be reached by a -edge before a -edge finds it), so keep the dist[u] + w < dist[v] test and skip stale deque entries with if d == dist[u].
  • Bounded integer weights ➔ for constant . Every distance is , and the minimum served never decreases, so store an array of buckets of linked lists: insert and update are , and one forward sweep over all buckets costs in total ⟹ .
  • The transferable move (LO3) ➔ read the constraint on the keys, then replace the ADT — the same lever as Counting Sort escaping the comparison floor.
  • Alternative for - ➔ contract every zero-weight component (found by a DFS) into one vertex, then run plain BFS on the contracted graph.

5. State-Graph Modelling — Change the Graph, Not the Algorithm (applied P3)

  • The paradigm ➔ when the answer depends on more than where you are, put the extra state into the vertex and run unmodified Dijkstra on the enlarged graph.
  • Worked shape (road trip with fuel) ➔ vertex town holding litres, giving vertices for fuel capacity .
  • Two edge families ➔ travel for every , weight (driving costs no money) · refuel for , weight (the town’s petrol price).
  • One litre at a time ➔ adding at weight is redundant and only makes the graph denser.
  • Answer ➔ run Dijkstra from and read ; the edge weights now model money, and the fuel constraint is enforced by which edges exist.
  • The general paradigm ➔ transforming the input so a known unmodified algorithm applies is the LO1 move, reused across the W5 and W6 applied sheets ➔ State-Space Graph Modelling holds the recipe, the unweighted BFS instance, and the sizing rule .

6. Two Heap Implementations — Update vs Duplicate

  • Update (the unit’s form) ➔ one entry per vertex, reached in through the index map ➔ heap size , every operation ; this is the version the assignment wants, since swapping in a Fibonacci heap later changes one line.
  • Duplicate / lazy ➔ a library heap (heapq) offers append and serve but no update, so an improved estimate is pushed as a second entry and the stale copies are discarded when served.
  • Where the duplicates come from ➔ one push per successful edge relaxation ⟹ heap size .
  • The bound does not move ➔ ⟹ both implementations are .
  • What moves is the constant ➔ the bigger heap is appended to and served from more often; say same bound, more operations, never “asymptotically slower”.
  • The discard test ➔ if u.visited: continue immediately after the serve — the same staleness guard as d != dist[u] in the - deque (§4).
  • Match the size to the code ➔ quote only for the update form and for the duplicate form; a heap size that contradicts your own implementation is the marked error.

⚙️ Core Implementation

🔹 Dijkstra with a min-heap

🔹 Zero-one BFS (applied P6)

⚖️ Complexity

Binary Heap, graph as an adjacency list, (connected).

CaseTimeAuxiliary spaceTrigger
Bestsingle target adjacent to the source, early exit — the init still runs
Average / Worstevery edge relaxed once, every vertex served once — Dijkstra has no bad-input case, only a bad-graph size
Weights (deque)the queue holds at most two distinct keys
Weights integer, (bucket queue)distances bounded by , minimum never decreases
  • Slide derivation ➔ outer loop serve edge scan update O(\log V)$$=O(V^{2}\log V)=O(E\log V), using for a dense graph.
  • Aggregate derivation (tighter, prefer it) ➔ serves at plus relaxations at ➔ once . The slide’s is the dense instantiation, not a separate bound.
  • Fibonacci heap ➔ amortised update ➔ , collapsing on a dense graph to .
  • Space ➔ auxiliary (heap distance/previous/flags) on top of the adjacency list, which is input, not auxiliary (Algorithmic Complexity §6).
  • Adjacency matrix, priced exactly ➔ the neighbour scan becomes per served vertex ⟹ cell reads, but a heap operation fires only where an edge actually exists ⟹ , not . An empty cell costs a read, never a .

Line-by-line derivation — re-quote it for any priority queue. Price the five steps against the ADT’s own costs: append , serve , update .

StepRunsCost eachTotal
reset every vertex
initialise the queue or
serve the minimum
append a first discovery
relax an edge downward
  • Binary heap ➔ ⟹ .
  • Fibonacci heap ➔ amortised ⟹ .
  • Deque or bucket queue ➔ ⟹ (§4).
  • Same template next door ➔ Prim fills in the identical table; Kruskal replaces rows – with one sort and find calls.

⚖️ Core Decision Matrix

SituationReach forWhyCost
Unweighted (or all-equal weights)BFShop count already is the distance
Weights - BFS with a dequethe queue never holds more than two keys
Integer weights Dijkstra bucket priority queueminimum never decreases ⟹ one forward sweep
Weighted, all Dijkstragreedy finalisation is sound
Some , single sourceBellman-Fordrelaxes rounds, detects negative cycles
All pairs, negatives allowedFloyd-Warshalltransitive closure over all intermediates
Dense and update-heavyDijkstra Fibonacci heapupdate drops to amortised
Cost depends on extra state (fuel, tolls, time of day)state graph unmodified Dijkstraput the state in the vertex, not in the code on the enlarged graph

When It Flips: the hinge is the sign of the weights, not their presence. One negative edge invalidates the finalisation step and no heap choice repairs it — the algorithm must change, not its ADT. Restricting the weights upward (, bounded integers) flips the other way: the algorithm stays, the ADT gets cheaper.

📊 Exam Execution Trace & Applied Exercises

Lecture’s directed weighted graph — , , , , , , , , . Source .

Manual Execution Trace

StepServed (finalised)Relaxations performedHeap after
0—init
,
: ✅ · ·
: ✅
finalised ➔ skip · : ✅
finalised ➔ skip

Final Extracted Output: · finalisation order is nondecreasing in distance · shortest path to backtracks , i.e. at cost .

  • Read the trap ➔ was written three times () and only the value it held when served is the answer. A vertex’s distance is an estimate until it is finalised.

Applied Exercise — break the “update only on discovery” bug (applied P1)

Problem: an implementation writes v.distance the first time is discovered and never relaxes it again. Exhibit a graph where it fails. Graph: , , , , .

Final Extracted Output: and instead of and . The recipe for breaking it: a vertex whose first discovery is via an expensive edge and whose true optimum arrives on a later, cheaper route.

Applied Exercise — state and discharge the correctness claim

Claim: for every vertex removed from the queue, is correct. Let be the finalised set.

Final Extracted Output: contradiction either way — if then had the smaller key and would have been served before ; if then . The inequality is exactly where non-negativity is spent.

⚠️ Common Mistakes

  • 💡 Relaxing a finalised vertex ➔ writing to v.distance after was served silently corrupts an already-correct answer and hides the negative-weight failure. Test v.visited first.
  • 💡 Relaxing only on first discovery ➔ the mirror error, and the one the applied sheet examines: an estimate must be allowed to fall while is still in the heap.
  • 💡 Quoting as the bound ➔ that is the dense case. The bound that survives marking is with named and the sparse/dense distinction stated (Graph §3).
  • 💡 “Dijkstra fails on negative edges because distances go negative” ➔ no — it fails because a vertex is finalised too early; state it as a broken greedy choice, with the vs counterexample.

🧠 Active Recall