Exercises — Dijkstra's algorithm — greedy, priority queue, O((V+E) log V) — no negative edges
Before we start, the shared objects/terms every problem uses.
Figure 1 below draws the graph used in problems L2.1–L2.3. Keep it open while working Level 2: each arrow is a directed edge, and the number beside it is that edge's weight.

Level 1 — Recognition
L1.1
Which of these graphs can Dijkstra run correctly on? (a) all edge weights (b) weights (c) weights (d) weights
Recall Solution
Dijkstra requires every edge weight to be (non-negative). A weight of is fine.
- (a) ✓ all equal .
- (b) ✓ includes , that is allowed.
- (c) ✗ has , a negative edge — the correctness proof's "" step dies.
- (d) ✓ zeros are allowed. Answer: a, b, d. For (c) you would use Bellman-Ford algorithm.
L1.2
When all edge weights are exactly , which simpler algorithm gives the same answer faster, and what is its running time?
Recall Solution
BFS. When every edge costs the same, "closest by weight" = "fewest edges", which is exactly what BFS explores layer by layer. BFS runs in — no heap, no factor. So don't pay for Dijkstra's priority queue when you don't need weights.
L1.3
True or false: a vertex is finalized the moment we push it into the priority queue.
Recall Solution
False. A vertex is finalized only when it is popped with a non-stale distance (recall: an entry is stale when , meaning a better route was found after this entry was pushed). Between a push and its pop, a cheaper route may still be found, lowering . Finalizing at push time is a classic bug.
Level 2 — Application (run it by hand)
Use the graph in Figure 1 above for L2.1–L2.3:
A→B(4), A→C(1), C→B(2), C→D(5), B→D(1), source A.
L2.1
Fill the full trace table and give final distances from A.
Recall Solution
Recall from the setup: means "no route yet", and a relaxation succeeds only when is strictly less than the current .
| Step | Pop | Relaxations | dist after |
|---|---|---|---|
| init | — | A0 B∞ C∞ D∞ | |
| 1 | A(0) | C: , B: | A0 B4 C1 D∞ |
| 2 | C(1) | B: ✓, D: | A0 B3 C1 D6 |
| 3 | B(3) | D: ✓ | A0 B3 C1 D4 |
| 4 | D(4) | none | done |
Line-by-line, why each relaxation succeeds or fails:
- Step 1, pop A(0). and still hold . Relaxing : — succeeds (any finite number beats ), so . Relaxing : — succeeds, .
- Step 2, pop C(1) (we pop before because — the heap always yields the minimum). Relaxing : , compared to the current ; since it succeeds, lowering from to . Relaxing : — succeeds, .
- Step 3, pop B(3). Relaxing : , compared to the current ; since it succeeds, lowering from to .
- Step 4, pop D(4). has no outgoing edges, so there is nothing to relax — the run ends.
Final: .
Key moment: after step 1 the heap holds B(4) and C(1); we pop the minimum C(1), and that is what makes the relaxation in step 2 available, dropping from 4 to 3.
L2.2
What is the shortest path (the actual route, not just the number) from A to D? How would you recover it?
Recall Solution
Recall from the setup: the vertex we arrived from on 's current best route. It starts as for every vertex, and each successful relaxation of sets (it is updated in lock-step with ). Reading off the successes from the L2.1 trace:
- 's last successful relaxation was in step 3 ⇒ .
- 's last successful relaxation was in step 2 ⇒ .
- 's successful relaxation was in step 1 ⇒ .
- (the source has no parent) — this is where the trace-back stops. Follow the parents backwards from the target: , then reverse. Path: , total . ✓ matches .
L2.3
Add the directed edge A→D(4). Do the final distances change?
Recall Solution
When we pop A(0) we now also relax : — succeeds, so becomes immediately (and ).
Later, the route also totals . When step 3 tests : , compared to the current ; since the relaxation fails — it's a tie, so stays and stays .
No distance changes. (The recovered path may differ depending on tie-breaking, but the cost is identical.)
Level 3 — Analysis (why it behaves this way)
L3.1
Give a tiny graph with a negative edge where Dijkstra returns a wrong answer, and pinpoint the exact wrong finalization.
Recall Solution
Take vertices with edges s→A(2), s→B(1), B→A(-3).
True shortest to : (cheaper than the direct ).
Dijkstra's run:
- Pop
s(0), relax , . Heap{B(1), A(2)}. - Pop the minimum
B(1), finalize it. Relax its edge : ⇒ and we pushA(-2). Heap{A(-2), A(2)}. - Pop
A(-2), finalize .
With this ordering Dijkstra actually recovers . To force the wrong answer we need the negative edge to point into an already-finalized vertex. The genuine failure is drawn in Figure 2 below: s→X(1), s→Y(2), Y→X(-5).
- Pop
s(0), relax , . Heap{X(1), Y(2)}. - Pop
X(1), finalize — done forever. - Pop
Y(2), relax : , but is already finalized; a correct Dijkstra never re-pushes/re-finalizes it. - Wrong finalization: , while the true distance is . The "" step of the proof is exactly what fails: the tail edge is negative, so a longer-hop path became cheaper after was declared done.
Figure 2 shows this failing graph — trace it against the bullets above: is finalized at (orange) before the cheaper route through (via the magenta edge) is ever examined.

L3.2
In the lazy-heap implementation, bound (i) the number of heap operations over the whole run and (ii) the peak size of the heap at any instant. Use them to justify .
Recall Solution
Keep the two quantities separate — they are different things.
- (i) Cumulative pushes/pops over the run. Each successful relaxation pushes exactly one new pair, and there are at most successful relaxations total (each directed edge can relax only when is finalized), plus the one initial push of . So the run performs pushes and hence pops. Each push or pop costs .
- (ii) Peak (instantaneous) heap size. At any moment the heap can contain at most every entry ever pushed and not yet popped, so it is bounded by the total pushes: . Therefore .
Combining: operations each costing gives .
- The -term. Separately, we must create and set up the array (and ) with one entry per vertex — that is work, independent of the heap. This array initialization is the only source of the standalone in the bound.
Total: .
L3.3
On a graph with , , all weights , someone runs Dijkstra and someone else runs BFS. Do they return identical ? Which is asymptotically cheaper, and by what factor?
Recall Solution
Same : with unit weights, minimum-weight path = fewest-edges path, which is exactly BFS's layers. Cost: BFS is ; Dijkstra is . Dijkstra is a factor of slower. Use BFS here.
Level 4 — Synthesis (combine with other ideas)
L4.1
Relate Dijkstra to Prim's algorithm. Both use a min-heap and finalize the closest frontier vertex — what single line of code differs, and what does each compute?
Recall Solution
Skeleton is identical: min-heap, pop closest unfinalized vertex, relax neighbors.
- Dijkstra relaxes with
dist[u] + w(u,v)— the accumulated distance from the source. It computes shortest paths (SSSP). - Prim relaxes with just
w(u,v)— the single edge cost to attach to the tree. It computes a Minimum Spanning Tree (MST). One term (dist[u] +) is the entire difference.
L4.2
[[A search]] is "Dijkstra + a heuristic ". If for all , what does A reduce to? What property must have for A* to stay correct?**
Recall Solution
With , A* orders the queue purely by — it reduces exactly to Dijkstra. For correctness, must be admissible: (true remaining distance from to the goal), i.e. it never overestimates. An admissible only reorders exploration toward the goal without ever finalizing a vertex too early.
L4.3
Johnson's algorithm wants to run Dijkstra on a graph that has negative edges. How does it make Dijkstra legal, and what does it run first?
Recall Solution
Johnson reweights every edge to be non-negative while preserving which path is shortest. It:
- Adds a virtual source connected to all vertices with weight-0 edges.
- Runs Bellman-Ford algorithm once to get a potential (shortest dist from the virtual source).
- Reweights each edge: .
- Runs Dijkstra from every vertex on the reweighted (now non-negative) graph, then subtracts the potentials back out. This gives all-pairs shortest paths while still using fast Dijkstra.
Level 5 — Mastery
L5.1
A graph has a zero-weight cycle (each edge weight ) plus s→A(5). Does Dijkstra terminate, and does it loop forever around the cycle?
Recall Solution
It terminates. After is finalized at , relaxing gives , then , then the edge tries , which is not . Relaxation fails, no push, no loop. Zero-weight cycles are safe because relaxation only fires on strict improvement. (Only negative cycles cause infinite improvement — and Dijkstra forbids negative edges anyway.)
L5.2
You must find the shortest path from to a single target , and you may stop early. When exactly is it safe to stop, and why can't you stop as soon as is merely pushed?
Recall Solution
Stop the instant is popped (with a non-stale distance) — at that moment is provably final by the greedy invariant. You cannot stop at push time: after is pushed with some value, a later-popped vertex closer to might relax to something smaller. Only the pop guarantees no smaller route survives, because every unfinalized vertex still in the heap has and all edges are .
L5.3
Modify Dijkstra to also count how many distinct shortest paths exist from to each vertex. What extra array and update rule do you add, and what does it report for vertex on the L2.3 graph (the one with A→D(4) added)?
Recall Solution
Add a counter array initialized with and for all other . During relaxation of edge , compare the candidate distance against the current :
- Strictly better (): we found a new cheaper best, so the old count is obsolete ⇒ set and reset .
- Equal — a tie (): another shortest route arrived, so ⇒ add (do not change ).
- Worse (): change nothing.
The count is only trustworthy once is finalized (popped), since more ties may still arrive from vertices with equal . On the L2.3 graph, the shortest routes to (both cost ) are and the direct , so .
L5.4
Worst-case entries pushed for a dense graph. With a lazy heap on a graph where and (complete), give the asymptotic number of heap pushes and the running time.
Recall Solution
Pushes . Each heap operation is . Running time . On a dense graph a Fibonacci heap wins: , dropping the .
Recall One-line self-test
Pop the min ::: finalize it — its distance is now true and never changes.
The symbol means ::: "no route found yet / unreachable so far" — any finite path length beats it.
A relaxation "succeeds" when ::: is strictly less than the current (then we lower and set ).
Weight of exactly 0 ::: allowed (proof needs , and ).
A heap entry is stale when ::: its stored distance exceeds the current (a better route was found after it was pushed).
Lazy vs eager heap ::: lazy pushes a fresh pair (no decrease-key); eager lowers the existing key in place.
if d > dist[u]: continue ::: skips stale lazy-heap entries.
Count shortest paths: on a tie ::: add into instead of resetting.
Connections
- Bellman-Ford algorithm — the fix when edges go negative (L3.1, L4.3)
- BFS — the cheaper choice for unit weights (L1.2, L3.3)
- A* search — Dijkstra plus an admissible heuristic (L4.2)
- Priority Queue (Binary Heap) — the heap driving every pop/push
- Prim's algorithm — same skeleton, different relaxation key (L4.1)
- Johnson's algorithm — reweight, then Dijkstra everywhere (L4.3)
- Greedy algorithms — the invariant that licenses "pop = finalize"