§1Knowledge map
graph = vertices + edges (directed or not, weighted or not); sum of degrees = 2 × edges │ ├── stored as: edge list (E) · adjacency list (V + E, the usual choice) · adjacency matrix (V², is there an edge? in 1 step) │ ▼ search: a frontier of discovered-but-unexplored vertices, cost O(V + E) ├── queue → BFS: rings, fewest EDGES from the start (levels, parents) └── stack / recursion → DFS: dives, discovery/finish times nested like brackets, finds cycles │ ▼ weights ≥ 0, cheapest route Dijkstra: settle the closest vertex, relax its edges; priority queue + lazy deletion; O((V + E) log V); a negative edge breaks it │ ▼ connect everything cheaply minimum spanning tree: cut property ⇒ greedy is safe ├── Kruskal: sort the edges, union-find skips cycles; O(E log V) └── Prim: grow one tree with a priority queue of edges leaving it; O(E log V)
One line each: a graph is dots and lines · the adjacency list is the usual choice for sparse graphs · BFS (queue) finds the fewest edges, DFS (stack) dives and finds cycles, both cost V + E · mark vertices when you discover them · Dijkstra settles the closest vertex and relaxes its edges, needs non-negative weights, uses a heap with lazy deletion · the cheapest edge across any cut belongs to the MST · Kruskal uses union-find (small trees, hang the smaller under the bigger, compress paths) · the MST is not the tree of cheapest routes.
§3Exercises
Ten exercises in four layers: three proofs, two designs, three implementations with milestones, and two read-the-code. Proofs and designs are the same in both languages; the implementation exercises come as templates in PythonJava with tests that fail on purpose, and your job is to make every test pass. Try each exercise before you open its solution.
| # | Layer | Topic | Video |
|---|
| 1 | Proof | BFS finds a route with the fewest edges | 2 |
| 2 | Proof | Dijkstra: why the popped distance is final, and what it costs | 3 |
| 3 | Proof | The cut property, and why Kruskal is correct | 4 |
| 4 | Design | Shortest route when the vertices carry the weights | 3 |
| 5 | Design | Finding a cycle in a directed graph | 2 |
| 6 | Implement | BFS with levels and parents | 2 |
| 7 | Implement | Routes from parents, and how many shortest routes there are | 2 |
| 8 | Implement | Union-find with union by size and path compression | 4 |
| 9 | Read the code | An AI-written BFS that "returns the distances" | 2 |
| 10 | Read the code | An AI-written Dijkstra that "examines every edge once" | 3 |
PROOFS
Exercise 1 · BFS finds a route with the fewest edges. Run BFS from a start vertex s on an undirected graph (the version of video 2: a vertex is marked, given level[v] = level[u] + 1 and a parent when it is put into the queue). (a) Prove that the levels of the vertices, in the order in which they are taken out of the queue, never decrease and that two vertices that are next to each other in that order differ by at most 1. (Hint: induction on the number of vertices taken out; look at the queue.) (b) Let δ(v) be the smallest number of edges on a route from s to v. Prove that level[v] = δ(v) for every reachable vertex v. First show level[v] ≥ δ(v) (what route does the parent chain give?), then level[v] ≤ δ(v) by induction on δ(v), using (a). (c) Show that the adjacency list of every reachable vertex is read exactly once, so BFS costs V + 2E list entries in an undirected graph. Check it for the example graph from A: order, levels and the number of entries. (d) The example graph has three routes from A to G with three edges. Name them, and say which one BFS returns when the neighbours are visited in alphabetical order, and what its weight is compared with the cheapest route.
Solution
(a) Claim: at every moment the queue is [v₁, …, v_k] with level(v₁) ≤ … ≤ level(v_k) ≤ level(v₁) + 1, and the vertices taken out earlier have levels ≤ level(v₁). Initially the queue holds only s. Taking out v₁ and appending its newly discovered neighbours, each of level level(v₁) + 1: the new vertices come after v_k, and level(v₁) + 1 ≥ level(v_k) (v_k has level at most level(v₁) + 1), so the queue is still non-decreasing; the new first element v₂ has level ≥ level(v₁) and the new last one has level level(v₁) + 1 ≤ level(v₂) + 1. If nothing is appended the claim holds trivially. So the levels never decrease along the order in which vertices leave the queue and neighbours in that order differ by at most 1. ∎ (test_the_levels_along_the_visit_order_never_decrease_and_grow_by_at_most_one.)
(b) level[v] ≥ δ(v): following parents from v leads back to s along level[v] edges (each parent is one level closer: level[parent] = level[v] − 1), so there is a route with level[v] edges and δ(v) ≤ level[v]. level[v] ≤ δ(v), by induction on d = δ(v): for d = 0, v = s and level[s] = 0. For d ≥ 1 take a neighbour u of v on a route with d edges, so δ(u) = d − 1 and, by the induction hypothesis, level[u] = d − 1. When u is taken out, either v has been discovered before, by some w that left the queue before u, and by (a) level[w] ≤ level[u], so level[v] = level[w] + 1 ≤ d; or v is discovered now and level[v] = level[u] + 1 = d. Together level[v] = δ(v). ∎ (Checked against Floyd–Warshall on 300 random graphs.)
(c) Every vertex is put into the queue at most once (it is marked the moment it is put in) and taken out once, and the adjacency list of the vertex that is taken out is read once: V list fetches and Σ deg(v) = 2E entries in all, V + 2E. Example graph from A: order A B D C E F G, levels A 0, B 1, D 1, C 2, E 2, F 2, G 3, the seven lists hold 22 entries (2 · 11), so 7 + 22 = 29 steps (test_every_adjacency_list_is_read_exactly_once).
(d) Routes with three edges: A B E G (6 + 7 + 10 = 23), A D E G (3 + 13 + 10 = 26) and A D F G (3 + 5 + 12 = 20). In alphabetical order A takes B before D, B discovers E before D can, so G's parent is E and E's parent is B: BFS returns A B E G, weight 23, while the cheapest route is A D F G with weight 20: the fewest edges is not the cheapest (video 2).
Exercise 2 · Dijkstra: the popped distance is final, and what it costs. Dijkstra's algorithm of video 3 keeps a priority queue of pairs (distance, vertex) with lazy deletion: a vertex can be in the queue several times, an entry of a vertex that is already settled is skipped ("stale"), and all weights are ≥ 0. (a) State the invariant "when a vertex u is taken out of the queue for the first time, dist[u] is the true shortest distance δ(u)" and prove it. (Hint: suppose u is the first vertex for which it fails; follow a true shortest route from s to u until it leaves the set of settled vertices, and look at the first unsettled vertex y on it; compare dist[y] and dist[u]. Where do you use that weights are not negative?) (b) Prove that the keys taken out of the queue never decrease, and that the number of pushes is at most 2E + 1 for an undirected graph. Conclude the running time O((V + E) log V) with a binary heap. Check the pushes, pops and the single stale entry on the example graph from A (video 3: 8 pushes, 8 pops, 1 stale). (c) The directed graph S→A 2, S→B 3, A→C 3, B→A −2 has a negative arc. Run the algorithm by hand (a settled vertex is never looked at again), say where the invariant of (a) breaks, and give the distance it reports for C and the true one.
Solution
(a) Invariant: when a vertex is taken out of the queue for the first time, its dist equals δ, the true distance. Suppose u is the first vertex for which this fails, taken out with dist[u] > δ(u). Take a true shortest route s = x₀, …, x_m = u. Let y = x_j be the first vertex on it that is not yet settled (one exists, because u itself is unsettled; the source is settled first, so j ≥ 1 and the vertex before y, x_{j−1}, is settled). By the minimality of u, dist[x_{j−1}] = δ(x_{j−1}), and when x_{j−1} was settled it relaxed the arc to y, so dist[y] ≤ δ(x_{j−1}) + w(x_{j−1}, y) = δ(y) (the prefix of a shortest route is a shortest route), hence dist[y] = δ(y). Non-negative weights: the rest of the route from y to u has weight ≥ 0, so δ(y) ≤ δ(u) < dist[u]. Then the queue holds an entry of y with key dist[y] < dist[u] (or y is u), but u was taken out first although its key is not smaller: contradiction. ∎ The one place where the weights matter is the step δ(y) ≤ δ(u).
(b) Keys never decrease: the key taken out is the smallest in the queue; new entries have key d + w ≥ d (non-negative w), so nothing smaller than the key just taken out ever enters (test_popped_distances_never_decrease_and_are_final). Pushes: one push for the source and one for each improving relaxation; an arc u→v is relaxed only when u is settled, once per arc, and an undirected edge gives two arcs, so pushes ≤ 2E + 1 (and pops = pushes). The queue never holds more than 2E + 1 entries, log(2E + 1) = O(log V) since E ≤ V², so V settlings plus 2E relaxations cost O((V + E) log V). Example from A (dijkstra_demo): 8 pushes, 8 pops, 1 stale entry (E with key 16, because E later became 13 through B), distances A 0, B 6, C 14, D 3, E 13, F 8, G 20, bound 2 · 11 + 1 = 23.
(c) Start S: A = 2, B = 3. Take S's smallest, A (2): C = 5. Take B (3): the arc B→A has weight −2, so A could be 1, and the entry (1, A) is pushed, but A is already settled, so it is skipped as stale and never looked at again: C stays 5. The true distance is 4 (S, B, A, C: 3 − 2 + 3), found by Bellman–Ford (test_a_negative_arc_breaks_dijkstra_but_not_bellman_ford). The invariant of (a) breaks at the first pop of A: dist[A] = 2 but δ(A) = 1; the step "the rest of the route has weight ≥ 0" is false when an arc is negative.
Exercise 3 · The cut property, and why Kruskal is correct. In this exercise all edge weights are different, so a connected graph has exactly one minimum spanning tree (MST). (a) A cut is a split of the vertices into two non-empty sets S and V∖S. Prove the cut property: the lightest edge e that has one end in S and one end outside belongs to every MST. (Hint: take an MST T without e; adding e to T closes a cycle; where else does that cycle cross the cut?) (b) Kruskal's algorithm takes the edges in order of weight and accepts an edge when its two ends are in different trees of the forest built so far. Use (a) with a well-chosen cut to prove that every accepted edge belongs to the MST; then show that the accepted edges form a spanning tree. (The cut: the vertices of the tree that contains one end of the edge.) (c) For the example graph and the cut S = {A, D, F}: list the edges crossing the cut with their weights and say which one the cut property puts into the MST. Check that it is among the six edges Kruskal accepts in video 4.
Solution
(a) Let e = {u, v} be the lightest edge across the cut (S, V∖S), u ∈ S, v ∉ S, and let T be an MST without e. T is a tree, so T + e contains exactly one cycle, through e. The cycle goes from u ∈ S to v ∉ S and back, so it crosses the cut at least twice: besides e it uses another edge f across the cut. Weights are distinct and e is the lightest edge across the cut, so w(e) < w(f). Removing f breaks the cycle: T′ = T + e − f is connected, has V − 1 edges, so it is a spanning tree, and w(T′) = w(T) + w(e) − w(f) < w(T). That contradicts the minimality of T, so every MST contains e. ∎ (Exhaustively checked for all cuts of random 5-vertex graphs: test_the_lightest_edge_across_every_cut_is_in_the_minimum_spanning_tree.)
(b) Kruskal accepts e = {u, v} when u and v are in different trees. Let S be the tree that contains u (all vertices connected to u by accepted edges so far). Any edge e′ with one end in S and the other outside has not been accepted. If w(e′) < w(e), e′ was considered earlier, when its ends were in different trees (trees only grow, and the ends are in different trees now, so they were then), so it would have been accepted, joining its ends: contradiction. So e is the lightest edge across the cut (S, V∖S), and by (a) it belongs to the MST (test_every_edge_kruskal_accepts_is_the_lightest_edge_leaving_its_own_component). Every accepted edge joins two different trees, so the accepted edges never form a cycle; a rejected edge has both ends in the same tree, so when the list is used up every edge's ends are connected: the accepted edges connect the graph, form no cycle, and are V − 1 in number: a spanning tree consisting only of MST edges, i.e. the MST. ∎
(c) Edges across S = {A, D, F}: A-B 6, E-F 9, B-D 11, F-G 12, D-E 13. The lightest is A-B 6, so it belongs to the MST, and it is the fourth edge Kruskal accepts in video 4 (A-D 3, C-E 4, D-F 5, A-B 6, B-E 7, E-G 10: total 35).
DESIGNS
Exercise 4 · Shortest route when the vertices carry the weights. In a road network every town v charges a toll c(v) ≥ 0 each time you pass through it, the start and the destination included, and the roads themselves are free. The cost of a route is the sum of the tolls of all the towns on it. (a) Show how to turn this into an ordinary shortest-path problem with weights on the edges and then run Dijkstra's algorithm unchanged (or nearly). Which weight gets an arc u→v, and with what distance does the source start? Why is it important that tolls are not negative? (b) Use the example graph without its edge weights and the tolls A 1, B 5, C 2, D 1, E 3, F 4, G 1. Run your method by hand from A: which towns are settled in which order, and what is the cheapest route from A to G and its cost? Compare with the route BFS finds (the fewest edges) and with A D F G. (c) What changes if the toll is only paid when you leave a town? And if there were a toll for changing from road type to road type at a town? (A sentence each: what would the graph look like?)
Solution
(a) Give the arc u→v the weight c(v), the toll of the town you enter, and start Dijkstra at the source with distance c(source) (the start's toll is paid too). A route s = x₀, …, x_m then costs c(x₀) + Σ c(x_i) for i ≥ 1, exactly the sum of all the tolls. The arc weights are ≥ 0 exactly when the tolls are, and non-negative weights are what the invariant of Exercise 2 needs. (ex9_designs.py: cheapest_route_with_tolls.)
(b) Distances: A = 1 settled first. A relaxes B = 1 + 5 = 6 and D = 1 + 1 = 2. D (2) is settled: E = 2 + 3 = 5, F = 2 + 4 = 6 (B stays 6). E (5): C = 5 + 2 = 7, G = 5 + 1 = 6 (F stays 6). Then B, F and G (all 6; with ties broken by the letter, G is settled last of the three), then C (7). The route is A D E G, cost 1 + 1 + 3 + 1 = 6. BFS (fewest edges, alphabetical) finds A B E G, which costs 1 + 5 + 3 + 1 = 10, and A D F G costs 1 + 1 + 4 + 1 = 7. Both A D E G and A D F G have three edges, and A B E G also: BFS cannot tell them apart, tolls can (test_tolls_reduce_to_edge_weights, and against brute force on 200 random graphs).
(c) Toll paid when you leave a town: arc u→v gets weight c(u) and the source starts at 0; the destination's own toll is never paid (the route cost is the sum of the tolls of all towns except the last). A cost for changing road type at a town is not a property of a vertex or of an edge alone but of a pair of consecutive edges: make a vertex for every pair (town, road type you arrived by), with arcs between the pairs at the same town costing the change: the graph gets bigger (V × types vertices) but Dijkstra is again unchanged.
Exercise 5 · Finding a cycle in a directed graph. A directed graph is given as the list of out-neighbours of each vertex. Decide whether it contains a cycle and, if it does, return one. (a) The undirected test of video 2 ("skip the vertex we came from; any other vertex we have already seen means a cycle") gives a wrong answer for the directed graph A→B, A→C, B→C. Explain why, and give the rule that is correct for directed graphs: three states for a vertex (not started, being explored, finished) and what must be found. (b) Write the algorithm in pseudocode so that it returns the cycle as a list of vertices (hint: remember the DFS parent of every vertex), state its running time, and say why the answer is correct in both directions (cycle found ⇒ cycle exists, no cycle found ⇒ none exists). (c) Run it on A→B→C→A and on A→B, A→C, B→C. Which arc is the "back" arc in the first?
Solution
(a) In A→B, A→C, B→C, DFS from A reaches B, then C (via B), finishes C and B, and goes back to A, where it meets C again through the arc A→C. C is "already seen", so the undirected rule says "cycle". But C is finished: nothing leads back from C to A, so it is not a cycle: in a directed graph two different routes to the same vertex are fine (test_undirected_trick_does_not_work_for_directed_graphs). The correct rule: colour every vertex white (not started), gray (the call is still running, i.e. it is on the current DFS path) or black (finished). A cycle exists exactly when an arc leads from the vertex being explored to a gray vertex (a "back arc").
(b) visit(u): colour u gray; for each arc u→v: if v is gray, return the cycle: follow parent from u back to v, reverse it; if v is white, set parent[v] = u and visit(v), returning a cycle found there; finally colour u black. Start visit from every white vertex. Each vertex and arc is handled once: O(V + E). Back arc found ⇒ cycle: v is gray, so v is an ancestor of u on the DFS path, and the path from v down to u plus the arc u→v is a cycle. Cycle exists ⇒ found: take a cycle and let x be the first of its vertices that DFS starts. At that moment all other vertices of the cycle are white and connected to x by a path of white vertices, so all of them become descendants of x (the white-path theorem): visit(x) has not returned while they are explored, so x is gray when the last vertex of the cycle looks at its arc back to x. (Checked against Kahn's algorithm, which removes vertices without incoming arcs, on 500 random digraphs, and the returned cycle is verified arc by arc: test_found_cycle_is_a_real_cycle_and_agrees_with_kahn.)
(c) A→B→C→A: visit(A) colours A gray, B gray, C gray; the arc C→A goes to a gray vertex: the back arc; the cycle is [A, B, C]. A→B, A→C, B→C: C is finished (black) when the arc A→C is looked at, no back arc, no cycle.
IMPLEMENTATIONS (WITH MILESTONES)
Each file lists its milestones at the top. Do them in order and run the tests after each one.
Exercise 6 · BFS with levels and parents — ex9_bfs.pyEx9Bfs.java. Implement bfs(graph, start) returning the visit order, the level (number of edges from the start) and the parent of every reachable vertex. The tests check the order, levels and parents of the example graph from A, compare the levels with a brute-force (Floyd–Warshall) computation on random graphs, and count how often a vertex's adjacency list is fetched: exactly once per reachable vertex, also on a complete graph of 40 vertices where a search that forgets to mark vertices in time makes the queue explode.
Solution
The reference solution (solutions/py/ex9_bfs.py, solutions/java/Ex9Bfs.java) is the textbook loop: put start in the queue with level 0, parent None; take the front vertex u, append it to the order, and for every neighbour v that has no level yet set level[v] = level[u] + 1, parent[v] = u and enqueue it. The test "lookups == V" fails for the version that marks a vertex when it is taken out of the queue (that version puts a vertex into the queue once for every neighbour processed before it, so on the complete graph of 40 vertices it makes 781 insertions (1 + 40 · 39 / 2) instead of 40 and, worse, reports levels up to 39 where every vertex is one edge from the start: see Exercise 9).
Exercise 7 · Routes from parents, and counting them — ex9_paths.pyEx9Paths.java. path_from_parents follows the parents back from the target; shortest_path runs the search and returns a route with the fewest edges (or nothing); count_shortest_paths returns, for every reachable vertex, the number of different routes with the fewest edges, in a single BFS (hint: add the count of u to every neighbour one level further away). The tests include the example graph from A (G is reached in 3 different ways) and compare with a brute force over all simple routes of random graphs.
Solution
path_from_parents follows parent from the target back to the start and reverses the list; None if the target has no parent entry. shortest_path is the BFS plus that. count_shortest_paths keeps ways[v]: when u is taken out and a neighbour v is one level further (level[v] == level[u] + 1, either just discovered or discovered earlier), ways[v] += ways[u], since every shortest route to u extends to a shortest route to v and every shortest route to v arrives through such a u. On the example graph from A: A 1, B 1, D 1, C 1, E 2, F 1, G 3: E is reached as A B E and A D E, F only as A D F, G as A B E G, A D E G and A D F G, the three routes of Exercise 1(d).
Exercise 8 · Union-find — ex9_unionfind.pyEx9UnionFind.java. Implement find, union and connected, then union by size (the smaller tree hangs under the bigger one), then path compression. The tests merge 1,024 singletons in blocks of 1, 2, 4, 8, … and demand that the deepest vertex is at most 10 steps from its root, flatten a chain of 10 vertices with one find, and compare with a naive labelling on random operations.
Solution
find walks up to the root and counts the links it follows, then walks the path again and points every vertex on it straight at the root (path compression). union finds both roots, returns False if they are equal, otherwise hangs the root of the smaller tree under the other one (the first argument's root stays on a tie) and adds the sizes; sets goes down by one. With union by size a tree of n elements has depth at most log₂ n (an element's depth increases only when its tree is hung under a tree at least as big, so the tree at least doubles each time): 1,024 singletons merged in blocks of 1, 2, 4, 8, … give a deepest vertex exactly 10 steps from its root; on a chain 0→1→…→9, find(0) makes every vertex a direct child of 9 (depth ≤ 1), and a second find(0) follows one link.
READ THE CODE
An AI assistant wrote each piece of code together with a claim. In both exercises the claim is wrong because of a bug. Find a small input that shows it (compare with a brute-force answer), say what exactly goes wrong and why the small examples you first try do not show it, and fix it. Use the habits from the chapter: compare with a brute-force answer, try more than the smallest graphs, and count the work, not just the time.
Exercise 9 — bfs_levels_9 in read/py/ex9_read.py / Ex9Read.bfsLevels9 in read/java/Ex9Read.java. The comment claims that it returns the visit order and the number of edges from the start to every vertex, and that marking a vertex visited when it is taken from the queue is just as good as marking it when it is put in.
Solution
What is wrong. The visit order is right (the first time a vertex is taken out is the first time it was put in, so the order is a BFS order; checked for all graphs on up to 5 vertices). The levels are wrong: level[v] = level[u] + 1 runs every time a vertex is put in the queue, and because visited is only set when it is taken out, a vertex that is already in the queue gets its level overwritten by every later vertex that finds it. The first level written is right (d + 1); a later one is wrong whenever the later vertex is deeper than the first: whenever an edge joins two vertices of the same level. Smallest example: the triangle 0-1-2 from 0: true levels {0: 0, 1: 1, 2: 1}, reported {0: 0, 1: 1, 2: 2} (vertex 1 is taken out first and finds 2 not yet visited, which is already queued by 0). On the example graph from A the edge B-D joins two level-1 vertices and the damage spreads along the whole graph: reported A 0, B 1, D 2, C 2, E 3, F 4, G 5 instead of A 0, B 1, D 1, C 2, E 2, F 2, G 3 (test_the_triangle_is_the_smallest_failure_and_the_example_graph_fails).
Why small examples do not show it. The levels are wrong exactly when an edge joins two vertices of the same level in the true BFS, i.e. when the component of the start contains an odd cycle (checked for every graph on 3, 4, 5 vertices). So trees, paths, even cycles, grids and every bipartite graph give the right answer. Even among small graphs the bug is rare at first: 1 of the 8 graphs on 3 vertices, 22 of 64 on 4, 621 of 1,024 on 5, so a test with a path or a tree, or with an even number of vertices in a cycle, passes. A random test against brute-force distances catches it quickly. A second, smaller problem: the queue can hold a vertex several times, so it needs Θ(E) space instead of Θ(V).
Fix. Mark at discovery: set level[v] (and a parent) only if v not in level, and drop the visited set and the continue at the pop. Then every vertex is put in the queue once, with the right level, and the adjacency list of each vertex is read once.
Exercise 10 — Dijkstra10 in read/py/ex9_read.py / Ex9Read.Dijkstra10 in read/java/Ex9Read.java. The comment claims that it is Dijkstra's algorithm with a binary heap and lazy deletion, that every edge is examined once and so the cost is O((V + E) log V), and that the distances are exact. (scans counts the adjacency entries looked at.)
Solution
What is wrong. Nothing is checked when an entry is taken out of the heap, so a stale entry (a vertex that has since become cheaper and has already been settled) is processed again: its whole adjacency list is scanned once more, although no relaxation can succeed from it (its key is not smaller than the current distance). The distances are still exact, so a correctness test passes, but the claim "every edge is examined once, O((V + E) log V)" is false. On the example graph from A, the single stale entry (E with key 16) costs 5 extra scans: 27 instead of 22 = 2E (test_on_the_example_graph_the_stale_entry_costs_five_extra_scans). How bad can it get? Take a vertex S joined to u₁ … u_k (weights 1 … k) and every u_i joined to t₀ … t_{m−1} with weight 3k − 2i: each t_j gets a better distance from every u_i in turn, so it is pushed k times, and each of its k − 1 stale entries scans its k neighbours again. For k = m = 10 / 20 / 40 (E = 110 / 420 / 1,640): the buggy version makes 1,120 / 8,440 / 65,680 scans, the fixed one 220 / 840 / 3,280 (2E): about k/2 times more, i.e. Θ(V·E) instead of Θ(E) (test_the_cost_grows_like_V_times_E_instead_of_E).
Why small examples do not show it. On paths and trees no vertex is ever pushed twice, so there are no stale entries, the two versions do exactly the same work (a path of 20 vertices: 38 scans for both), and the distances are always right. Only a count of the work, not the answer, shows the problem: that is why the exercise gives you scans.
Fix. Skip stale entries: after taking out (d, u), if d > dist[u]: continue (or keep a set of settled vertices, as the chapter's code does).