← Exam 2 home

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 / questionUseTime
Unit lengths (fewest edges / hops)BFS\(O(|V|+|E|)\)
Non-negative lengthsDijkstra\(O((|V|+|E|)\log|V|)\) binary heap
Negative edges, cycles allowed, no negative cycleBellman–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\) edgesBellman–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

  1. One row per vertex, one column per iteration. Column 0: source 0, everyone else ∞.
  2. 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.
  3. Update only its out-neighbours: new value = min(old, dist(u) + edge). Copy every other value unchanged. When a value drops, also change prev.
  4. Removed vertices never change again. The last iteration usually changes nothing; still write the column.
  5. Shortest-path tree = the prev edges.

Worked: DPV 4.1 (Dijkstra from A)

1482661214511ABCDEFGH
DPV 4.1 (as drawn in lecture, directed).
Answer
removed →initABCDEGFH
A000000000
B∞11111111
C∞∞3333333
D∞∞∞444444
E∞44444444
F∞87777666
G∞∞7555555
H∞∞∞∞88666

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.

1482661214511ABCDEFGHd=0d=1d=3d=4d=4d=6d=5d=6
Shortest-path tree from A (thick), final distances above each vertex.

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)

Implementationdeletemininsert / decreasekeyDijkstra 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|)\)

3. Negative edges break Dijkstra

Quiz 6: 3-vertex graph, no negative cycle, Dijkstra fails

23−2SAB
Quiz 6: Dijkstra finalizes A at 2; the true distance is 1.

Edges S→A 2, S→B 3, B→A −2. No cycles at all.

Subtlety if asked to defend it. DPV's pseudocode has no "skip removed vertices" test, so taken literally it would overwrite dist(A) with 1 when it scans B. It still failed: it already declared A final and used dist(A) = 2 for A's out-edges. Add A→C 1 to see a wrong answer that is never repaired: Dijkstra removes S, A (sets C = 3), B, C and reports dist(C) = 3, but the true distance is 2 (S→B→A→C). For the quiz, the 3-vertex graph plus "Dijkstra finalizes A at 2, true distance 1" is the expected answer.

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\):

−222uvw0−20
Original lengths: shortest-path tree u→v→w (thick), distances 0, −2, 0.
155uvw015
After adding 3 to every edge: tree changes to u→v, u→w.

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)

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)

76564−2213−2−41−113−2ABSCDFGHEI
DPV 4.2.
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.

vertexround 01234
S00000
A∞7777
B∞∞111111
C∞6555
D∞∞777
E∞6666
F∞5444
G∞∞888
H∞∞777
I∞∞877

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.

76564−2213−2−41−113−2ABSCDFGHEId=7d=11d=0d=5d=7d=4d=8d=7d=6d=7
Bellman–Ford shortest-path tree from S (thick), final distances above each vertex.

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.

21436121SACBDE
A weighted DAG (example for the one-pass algorithm).

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\).

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
ABCDElevel 0level 1level 2level 3level 4
DPV 4.4 counterexample. Thick = DFS tree from A; dashed = 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)

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.

TermWhat to remember
Direct addressingUniverse \(U=\{0,\ldots,m-1\}\), store key \(k\) in \(T[k]\). All ops \(O(1)\). Wastes memory when \(m \gg n\).
Hash function\(h: U\to\{0,\ldots,m-1\}\); store \(k\) in slot \(h(k)\). Collision: two keys, same slot.
ChainingEach slot holds a linked list. Insert \(O(1)\). Search/delete depend on chain length.
Load factor\(\alpha = n/m\) = average chain length. Under simple uniform hashing, expected search time \(\Theta(1+\alpha)\).
Division method\(h(k) = k \bmod m\). Fast (one division). Avoid \(m\) a power of 2 (then \(h\) is just the low bits of \(k\)). Good \(m\): a prime not too close to a power of 2. E.g. \(m=25\): \(h(35) = 10\).
Multiplication method\(h(k) = \lfloor m\,(kA \bmod 1)\rfloor\), \(0
Universal hashingPick \(h\) at random from a family \(\mathcal{H}\) where, for every pair \(k\ne l\), at most \(|\mathcal{H}|/m\) functions collide them. Defeats an adversary choosing bad keys. With chaining: expected \(O(1)\) per search.
Open addressingAll keys in the table itself (\(\alpha\le 1\)). Probe slots until you find \(k\) or an empty (NIL) slot.
Linear probing\(h(k,i) = (h'(k)+i) \bmod m\). Primary clustering: long runs of full slots.
Quadratic probing\(h(k,i) = (h'(k)+c_1 i+c_2 i^2) \bmod m\). Better, but secondary clustering: same \(h'(k)\) ⟹ same probe sequence.
Double hashing\(h(k,i) = (h_1(k)+i\,h_2(k)) \bmod m\). \(h_2(k)\) must be relatively prime to \(m\) so the whole table gets probed.
Uniform hashingIdeal for open addressing: each of the \(m!\) probe permutations equally likely. Hard to implement; the three schemes above approximate it.

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.