2 · Shortest paths + priority queues + hash tables
DPV Ch 42 exam problemsMCQs Most likely problems: a Dijkstra table (DPV 4.1 style), a Bellman–Ford table (DPV 4.2 style), "why does Dijkstra fail with negative edges" (Quiz 6, Ex 4.8, 4.9), or a lecture-solved design exercise (4.3, 4.15, 4.18, 4.21). Heaps and hash tables are MCQ material.
0. Which algorithm? (router)
| The graph / question | Use | Time |
|---|---|---|
| Unit lengths (fewest edges / hops) | BFS | \(O(|V|+|E|)\) |
| Non-negative lengths | Dijkstra | \(O((|V|+|E|)\log|V|)\) binary heap |
| Negative edges, cycles allowed, no negative cycle | Bellman–Ford | \(O(|V||E|)\) |
| Detect a negative cycle (e.g. currency arbitrage) | Bellman–Ford + one extra round | \(O(|V||E|)\) |
| DAG, any lengths (shortest or longest path) | One pass in topological order | \(O(|V|+|E|)\) |
| Shortest path with at most \(k\) edges | Bellman–Ford for \(k\) rounds | \(O(k|E|)\) |
| Feasible with tank \(L\) / bottleneck (DPV 4.13) | (a) BFS on edges \(\le L\); (b) Dijkstra with max instead of + | linear; \(O((|V|+|E|)\log|V|)\) |
All of them use the same update(u, v): \(\text{dist}(v) = \min(\text{dist}(v), \text{dist}(u) + \ell(u,v))\). They differ only in which edges get updated, in what order, and how many times.
1. BFS distances
procedure bfs(G, s)
for all u in V: dist(u) = ∞
dist(s) = 0
Q = [s] (FIFO queue)
while Q is not empty:
u = eject(Q)
for all edges (u, v) in E:
if dist(v) = ∞:
inject(Q, v)
dist(v) = dist(u) + 1
prev(v) = u
The queue holds vertices at distance \(d\) followed by vertices at distance \(d+1\), never more than two layers, because of FIFO order. \(O(|V|+|E|)\). Dijkstra is BFS with the FIFO queue replaced by a priority queue keyed by tentative distance.
2. Dijkstra
procedure dijkstra(G, ℓ, s) (requires ℓ(e) ≥ 0)
for all u in V:
dist(u) = ∞
prev(u) = nil
dist(s) = 0
H = makequeue(V) (keys = dist)
while H is not empty:
u = deletemin(H) (dist(u) is now final)
for all edges (u, v) in E:
if dist(v) > dist(u) + ℓ(u, v):
dist(v) = dist(u) + ℓ(u, v)
prev(v) = u
decreasekey(H, v)
Operation count: 1 makequeue, \(|V|\) deletemin, up to \(|V|+|E|\) insert/decreasekey.
How to fill the DPV-style table
- One row per vertex, one column per iteration. Column 0: source 0, everyone else ∞.
- Each iteration: deletemin the vertex with the smallest tentative dist among those not yet removed. Ties: alphabetically first. Write which vertex was removed at the top of the column.
- Update only its out-neighbours: new value = min(old, dist(u) + edge). Copy every other value unchanged. When a value drops, also change prev.
- Removed vertices never change again. The last iteration usually changes nothing; still write the column.
- Shortest-path tree = the prev edges.
Worked: DPV 4.1 (Dijkstra from A)
Answer
| removed → | init | A | B | C | D | E | G | F | H |
|---|---|---|---|---|---|---|---|---|---|
| A | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| B | ∞ | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| C | ∞ | ∞ | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
| D | ∞ | ∞ | ∞ | 4 | 4 | 4 | 4 | 4 | 4 |
| E | ∞ | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 |
| F | ∞ | 8 | 7 | 7 | 7 | 7 | 6 | 6 | 6 |
| G | ∞ | ∞ | 7 | 5 | 5 | 5 | 5 | 5 | 5 |
| H | ∞ | ∞ | ∞ | ∞ | 8 | 8 | 6 | 6 | 6 |
Removal order A, B, C, D, E, G, F, H. Tie at iteration 4: D and E both 4, D is removed first (alphabetical), then E. Same table whether you read the graph as directed (as in lecture) or undirected (as printed in DPV).
(b) Tree: A→B, B→C, C→D, A→E, C→G, G→F, G→H.
Why Dijkstra is correct
Let \(R\) be the set of removed vertices. When \(u\) is removed with tentative distance \(d\) (the smallest outside \(R\)), any other path from \(s\) to \(u\) must leave \(R\) at some first vertex \(v \notin R\). That prefix already costs at least \(\text{dist}(v) \ge d\), and the rest of the path has non-negative length, so the path costs \(\ge d\). Hence \(d\) is final. DPV's invariant: after each iteration, all of \(R\) is at distance \(\le\) some \(d\) and everything outside at \(\ge d\), and \(\text{dist}(u)\) is the shortest path using only intermediate vertices in \(R\).
The step that breaks with negative edges: "the rest of the path has non-negative length."
Running time by priority queue (DPV §4.5)
| Implementation | deletemin | insert / decreasekey | Dijkstra total |
|---|---|---|---|
| Unsorted array | \(O(|V|)\) | \(O(1)\) | \(O(|V|^2)\) |
| Binary heap | \(O(\log|V|)\) | \(O(\log|V|)\) | \(O((|V|+|E|)\log|V|)\) |
| \(d\)-ary heap | \(O\!\left(\frac{d\log|V|}{\log d}\right)\) | \(O\!\left(\frac{\log|V|}{\log d}\right)\) | \(O\!\left((|V|d+|E|)\frac{\log|V|}{\log d}\right)\) |
| Fibonacci heap | \(O(\log|V|)\) amortized | \(O(1)\) amortized | \(O(|V|\log|V|+|E|)\) |
- Dense graph (\(|E| = \Omega(|V|^2)\)): array wins. Binary heap wins once \(|E| < |V|^2/\log|V|\).
- Best \(d \approx |E|/|V|\) (the average degree): good on both sparse and dense graphs.
- Dijkstra's shortest-path tree is not in general a minimum spanning tree.
3. Negative edges break Dijkstra
Quiz 6: 3-vertex graph, no negative cycle, Dijkstra fails
Edges S→A 2, S→B 3, B→A −2. No cycles at all.
- Remove S: A = 2, B = 3.
- Remove A (2 < 3). Dijkstra declares dist(A) = 2 final.
- Remove B: but S→B→A costs \(3 - 2 = 1 < 2\). True dist(A) = 1. Dijkstra's answer 2 is wrong.
Ex 4.8: "add a large constant to every edge, then run Dijkstra"
Not valid. Adding \(c\) to every edge adds \(c\cdot k\) to a path with \(k\) edges, so paths with more edges are penalized more. The shortest path can change. Lecture counterexample, source \(u\):
Original: \(u\to v\to w\) costs \(-2+2 = 0\), beats \(u\to w = 2\). Distances 0, −2, 0. After +3: \(u\to v\to w\) costs \(1+5 = 6\), direct \(u\to w\) costs 5, so Dijkstra returns \(u\to w\), which is the wrong path in the original graph.
Ex 4.9: only the edges leaving \(s\) are negative. Can Dijkstra fail?
No, it works (assuming no negative cycle, i.e. the negative edges out of \(s\) do not combine with a path back into \(s\) to form one).
Proof. A shortest path never needs to revisit \(s\), so every shortest path to \(u\ne s\) has the form \(s\to v\leadsto u\), where only the first edge can be negative and the rest are positive. Dijkstra removes \(s\) first and sets \(\text{dist}(v)=\ell(s,v)\) for every out-neighbour. From then on, run the usual argument: when \(u\) is removed with value \(d\), any other path leaves \(R\) at some \(v\notin R\) with \(\text{dist}(v)\ge d\), and every edge after \(v\) is not an edge out of \(s\), so it is positive. The path costs \(\ge d\). The one place the proof used non-negativity is still true.
4. Bellman–Ford
procedure update((u, v) in E)
dist(v) = min{dist(v), dist(u) + ℓ(u, v)}
procedure bellman-ford(G, ℓ, s) (no negative cycles)
for all u in V:
dist(u) = ∞
prev(u) = nil
dist(s) = 0
repeat |V| − 1 times:
for all e in E:
update(e)
- Safe: every finite dist(v) is the length of some real path, so it never drops below the true distance.
- Enough: a shortest path has at most \(|V|-1\) edges. Round \(i\) fixes at least the \(i\)th edge of it, so \(|V|-1\) rounds suffice. \(O(|V||E|)\).
- Early stop: if a whole round changes nothing, stop.
- Negative cycle: run round \(|V|\). If any dist still decreases, a negative cycle is reachable from \(s\) (follow prev from the changed vertex to find it).
Table format
Rows = vertices, columns = rounds (0 = initial). Process edges in one fixed order every round and say what order you used. Within a round, an update uses values already changed earlier in the same round. The intermediate columns depend on the edge order; the final distances do not.
Worked: DPV 4.2 (Bellman–Ford from S)
Answer
Edge order used (alphabetical by tail, then head): A→B, A→C, B→G, B→H, C→D, C→F, E→F, E→H, F→D, G→I, H→G, I→H, S→A, S→C, S→E, S→F.
| vertex | round 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| S | 0 | 0 | 0 | 0 | 0 |
| A | ∞ | 7 | 7 | 7 | 7 |
| B | ∞ | ∞ | 11 | 11 | 11 |
| C | ∞ | 6 | 5 | 5 | 5 |
| D | ∞ | ∞ | 7 | 7 | 7 |
| E | ∞ | 6 | 6 | 6 | 6 |
| F | ∞ | 5 | 4 | 4 | 4 |
| G | ∞ | ∞ | 8 | 8 | 8 |
| H | ∞ | ∞ | 7 | 7 | 7 |
| I | ∞ | ∞ | 8 | 7 | 7 |
Round 1 only sets S's neighbours, because S's edges come last in this order. Round 4 changes nothing, so stop (no need to run all \(|V|-1 = 9\) rounds).
Final: S 0, A 7, B 11, C 5, D 7, E 6, F 4, G 8, H 7, I 7. Tree: S→A, A→B, A→C, C→D, S→E, E→F, B→H, H→G, G→I.
Check a few: C = 7 − 2 = 5 (beats S→C 6). F = 6 − 2 = 4 (beats S→F 5). H = 11 − 4 = 7. G = 7 + 1 = 8 (beats B→G: 11 − 2 = 9). I = 8 − 1 = 7.
5. Shortest (and longest) paths in a DAG
procedure dag-shortest-paths(G, ℓ, s)
linearize G (DFS, decreasing post)
for all u in V: dist(u) = ∞
dist(s) = 0
for each u in V, in linearized order:
for all edges (u, v) in E:
update(u, v)
Every path in a DAG visits vertices in topological order, so every shortest path's edges get updated in sequence. Negative edges are fine. \(O(|V|+|E|)\). For longest paths, use max instead of min (or negate all lengths). That only works because a DAG has no cycles; longest path in a general graph is NP-hard.
Example: topological order S, C, A, B, D, E. Shortest from S: S 0, C 2, A 1, B 7, D 5, E 6. Longest from S: S 0, C 2, A 6 (via C), B 12, D 13, E 14.
6. Lecture-solved and HW exercises
Ex 4.3 Squares: is there a simple 4-cycle? \(O(|V|^3)\) (solved in lecture)
A 4-cycle \(v, u, w, x\) exists iff two distinct vertices \(v \ne w\) have at least 2 common neighbours. Use the adjacency matrix. For each of the \(O(|V|^2)\) pairs \(v\ne w\), count \(|\text{Adj}(v)\cap\text{Adj}(w)|\) by scanning all \(|V|\) vertices: \(O(|V|)\) per pair, \(O(|V|^3)\) total. Answer yes iff some count \(\ge 2\).
The lecture version loops over \(u\) and pairs of its neighbours \(v, w\), checking \(|\text{Adj}(v)\cap\text{Adj}(w)|\ge 2\) (the intersection always contains \(u\)). The notes point out that with the outer loop this is \(O(|V|^4)\); drop the loop over \(u\) to meet \(O(|V|^3)\).
Ex 4.15 Unique shortest paths, usp[·] (solved in lecture)
for all u in V: d(u) = ∞; usp(u) = true
d(s) = 0
H = makequeue(V)
while H is not empty:
u = deletemin(H)
for all edges (u, v) in E:
if d(v) > d(u) + ℓ(u, v):
d(v) = d(u) + ℓ(u, v)
usp(v) = usp(u) (a strictly better path: unique iff u's is)
decreasekey(H, v)
else if d(v) = d(u) + ℓ(u, v):
usp(v) = false (a second shortest path)
Edge lengths are positive, so by the time \(u\) is removed, usp(u) is final. Same time as Dijkstra, \(O((|V|+|E|)\log|V|)\).
Ex 4.18 Fewest edges among shortest paths, best[·] (solved in lecture)
for all u in V: d(u) = ∞; best(u) = ∞
d(s) = 0; best(s) = 0
H = makequeue(V)
while H is not empty:
u = deletemin(H)
for all edges (u, v) in E:
if d(v) > d(u) + ℓ(u, v):
d(v) = d(u) + ℓ(u, v)
best(v) = best(u) + 1
decreasekey(H, v)
else if d(v) = d(u) + ℓ(u, v):
if best(v) > best(u) + 1: best(v) = best(u) + 1
Ex 4.5 Count shortest paths, unit lengths, linear time
BFS from \(u\) with a counter. count(u) = 1. When scanning edge \((x, y)\): if dist(y) = ∞, set dist(y) = dist(x) + 1 and count(y) = count(x) and enqueue; else if dist(y) = dist(x) + 1, count(y) += count(x). Output count(v). By the time \(x\) is dequeued, all vertices one layer up have been scanned, so count(x) is final. \(O(|V|+|E|)\).
Ex 4.13 Gas tank of L miles
(a) Delete every edge with \(\ell_e > L\), then BFS (or explore) from \(s\). Feasible iff \(t\) is reached. Linear.
(b) Minimum tank = the smallest possible largest edge on an \(s\)–\(t\) path. Run Dijkstra where a path's "length" is its maximum edge: dist(s) = 0, and the update is: if \(\max(\text{dist}(u), \ell(u,v)) < \text{dist}(v)\), set \(\text{dist}(v) = \max(\text{dist}(u), \ell(u,v))\). The max never decreases along a path (like non-negative edges), so Dijkstra's argument still works. Output dist(t). \(O((|V|+|E|)\log|V|)\) with a binary heap.
Ex 4.21 Currency trading (HW3 P7, also in HW3 solutions)
Vertices = currencies. Edge \(i\to j\) with length \(\ell(i,j) = -\log r_{i,j}\). A sequence of trades multiplies rates; logs turn the product into a sum: maximizing \(\prod r\) = minimizing \(\sum -\log r\).
- (a) Best way to convert \(s\) to \(t\): shortest path from \(s\) to \(t\) with these lengths. Lengths can be negative (any \(r>1\)), so use Bellman–Ford, \(O(|V||E|) = O(n^3)\) on the complete graph.
- (b) Anomaly: \(\prod r > 1 \iff \sum -\log r < 0\), a negative cycle. Run Bellman–Ford for round \(|V|\) (from every vertex, or add a super-source with 0-length edges to all). If anything still decreases, there is an anomaly.
- Dijkstra is wrong here: negative edges are the normal case.
HW3 P6 Time-dependent travel times (also in HW3 solutions)
Dijkstra where dist(v) = earliest arrival time at \(v\). dist(home) = \(t_0\). Update along edge \((i,j)\): arrival \(= \text{dist}(i) + f_{i,j}(\text{dist}(i))\); keep the minimum. The FIFO condition (entering later never gets you out earlier) means arriving earlier at \(i\) is never worse, so the greedy choice is still safe. Answer dist(work) − \(t_0\). \(O((|V|+|E|)\log|V|)\) with a binary heap, assuming each \(f\) evaluates in \(O(1)\).
Ex 4.4 Counterexample: shortest cycle from DFS back edges
Edges A–B, B–C, C–D, D–E, A–D, A–E. DFS from A (alphabetical) goes A→B→C→D→E (levels 0–4). Back edges D–A (cycle length \(3-0+1 = 4\)) and E–A (length 5). The strategy reports 4, but A–D–E–A has length 3. That cycle uses two non-tree edges, so no single back edge plus tree path finds it.
Ex 4.10 Shortest paths with at most k edges, \(O(k|E|)\)
Run Bellman–Ford from \(u\) for only \(k\) rounds. After round \(i\), dist(x) is at most the length of the shortest path to \(x\) using \(\le i\) edges (and never below the true distance). Since some shortest path to \(v\) has \(\le k\) edges, dist(v) is correct after \(k\) rounds. Each round is \(O(|E|)\).
Ex 4.11 / 4.12 Shortest cycle
4.11 (directed, positive lengths, \(O(|V|^3)\)): run Dijkstra from every vertex with the array implementation, \(O(|V|^2)\) each, to get all \(\text{dist}(u,v)\). Shortest cycle \(= \min_{(u,v)\in E} \big(\ell(u,v) + \text{dist}(v,u)\big)\). If every value is ∞, the graph is acyclic.
4.12 (undirected, shortest cycle through edge \(e=\{u,v\}\), \(O(|V|^2)\)): remove \(e\), run array-Dijkstra from \(u\) in \(O(|V|^2)\), answer \(\text{dist}(u,v) + \ell_e\) (∞ means no cycle contains \(e\)).
7. Priority queues (binary heap)
- Complete binary tree, filled level by level, left to right. Heap order: each key \(\le\) its children's keys, so the minimum is at the root.
- Array layout, positions \(1..n\): node \(j\) has parent \(\lfloor j/2\rfloor\), children \(2j\) and \(2j+1\). Height \(\lfloor\log_2 n\rfloor\).
- insert: put the new key in the first free position, bubble up (swap with parent while smaller). \(O(\log n)\).
- decreasekey: lower the key, bubble up from its current position. \(O(\log n)\).
- deletemin: return the root, move the last element to the root, sift down (swap with the smaller child while bigger). \(O(\log n)\).
- makeheap: put all \(n\) items in the array, then sift down positions \(n, n-1, \ldots, 1\). \(O(n)\), not \(O(n\log n)\). Heapsort = makeheap + \(n\) deletemins = \(O(n\log n)\).
- \(d\)-ary heap: parent of \(j\) is \(\lceil (j-1)/d\rceil\), children \((j-1)d+2, \ldots, jd+1\). Height \(\Theta(\log n/\log d)\): faster insert, slower deletemin.
Example: heap [2, 5, 3, 9, 6, 8]. Insert 1 at position 7; parent 3 holds 3 → swap; parent 1 holds 2 → swap. Result [1, 5, 2, 9, 6, 8, 3]. Deletemin: return 1, move 3 to the root, children 5 and 2 → swap with 2. Result [2, 5, 3, 9, 6, 8].
8. Hash tables (week 7 notes)
Dictionary ADT: search, insert, delete. Expected \(O(1)\) search under reasonable assumptions, worst case \(\Theta(n)\). Table size is proportional to the number of keys stored, not the number of possible keys.
9. Self-check
1. Dijkstra from S: S→A 4, S→B 1, B→A 2, A→C 1, B→C 5, C→D 3, A→D 7. Removal order and final distances?
Order S, B, A, C, D. A: 4 → 3 (via B). C: 6 (via B) → 4 (via A). D: 10 (via A) → 7 (via C). Final A 3, B 1, C 4, D 7. Tree S→B, B→A, A→C, C→D.
2. Dense graph, \(|E|\approx |V|^2/2\). Array or binary heap for Dijkstra?
Array: \(O(|V|^2)\) vs heap \(O(|V|^2\log|V|)\).
3. Bellman–Ford finished \(|V|-1\) rounds; in round \(|V|\) dist(C) still dropped. Conclusion?
A negative cycle is reachable from \(s\); following prev from C leads into it. Distances are not well defined (−∞ for vertices reachable from the cycle).
4. In a binary heap stored in positions 1..n, what are the parent and children of position 13?
Parent \(\lfloor 13/2\rfloor = 6\). Children 26 and 27.
5. Chaining, 200 keys, 50 slots. Load factor and expected search cost?
\(\alpha = 200/50 = 4\); expected search \(\Theta(1+\alpha)\): compute the hash, then scan a chain of about 4 elements on average. Worst case \(\Theta(n)\) if everything lands in one chain.
6. Why can longest paths be found in linear time in a DAG but not (efficiently) in general graphs?
In a DAG, negating lengths cannot create a negative cycle, and one pass in topological order updates every path's edges in order. In a general graph, negated lengths can form negative cycles; longest simple path is NP-hard.