Context:FIT2004_MOC · single-source shortest distance when Dijkstra is illegal — negative edges allowed, and a negative cycle is reported rather than silently wrong
Parent Framework:Graph
Quick Revision
🎯 Objective: relax every edge, ∣V∣−1 times ➔ every shortest distance is final; one extra pass that still improves something proves a negative cycle.
📦 Core Components: ① init Θ(V) | ② distance calculation Θ(VE) | ③ negative-cycle check Θ(E) — the check is not optional, it is half the algorithm.
⚡ Key Constraint:O(VE) buys negative edges. It is not greedy — nothing is ever finalised — so the price of dropping Dijkstra’s non-negativity precondition is a factor of logVV.
📝 How It Works
1. Why It Is Not Greedy (LO1)
Dijkstra finalises, Bellman-Ford does not ➔ Dijkstra serves the closest vertex and freezes it; Bellman-Ford keeps every estimate mutable for the whole run, so a late-arriving negative edge can still lower it.
The cost of giving up the greedy step ➔ with no “closest vertex” to serve there is no priority queue and no ordering to exploit ⟹ every edge must be re-examined every round.
Dynamic Programming is what pays for it ➔ round i computes dist(i)[v]= the cheapest walk s⇝v using at most i edges, built from round i−1‘s answers. Optimal substructure without the greedy-choice property is exactly the greedy-vs-DP boundary.
Relaxation is identical to Dijkstra’s ➔ if dist[u] + w < dist[v]: dist[v] = dist[u] + w; pred[v] = u. Only the schedule of relaxations differs.
2. Why Exactly ∣V∣−1 Rounds
A shortest path is simple ➔ if it repeated a vertex it would contain a cycle; a non-negative cycle can be deleted without getting worse, and a negative one means no shortest path exists at all.
Simple ⟹ at most ∣V∣−1 edges ➔ “this is the maximum number of jumps without a cycle” — going u⇝v you can pass through at most ∣V∣−1 edges before revisiting a vertex.
One round extends every path by one edge ➔ after round i every distance realisable in ≤i edges is correct, so round ∣V∣−1 closes the longest possible simple path.
Round order does not matter for correctness ➔ only for how fast it converges; a lucky edge order finishes in one pass, an adversarial one needs all ∣V∣−1.
3. The Negative-Cycle Check — Repeat It ONE MORE TIME
Why a negative cycle kills the problem ➔ each lap round the cycle lowers the total, so the infimum is −∞ ⟹ there is no shortest distance, and returning a number would be a lie.
The test ➔ run the relaxation pass a ∣V∣-th time. If any edge still relaxes, a path of ≥∣V∣ edges is beating every simple path ⟹ it must contain a cycle, and that cycle must be negative.
The lecture’s phrasing ➔ “if a cycle exists, this additional traversal will form the biggest cycle” — round ∣V∣ is the first round that can only be reporting a non-simple walk.
Report, do not repair ➔ the unit’s pseudocode raises error "Graph contains a negative-weight cycle"; it does not return distances.
Only cycles REACHABLE from the source are found ➔ a vertex still at ∞ never satisfies dist[u] + w < dist[v] in a meaningful way, so an unreachable negative cycle is invisible. Detecting any negative cycle needs a weight-0 super source (§5) or Floyd-Warshall’s diagonal.
4. The Required Early-Termination Optimisation
The observation ➔ if a full pass over all E edges relaxes nothing, no later pass can either — the state is a fixed point ⟹ break.
Implementation ➔ one boolean changed per round, set on every successful relaxation, tested at the end of the round.
What it buys ➔ best case collapses from Θ(VE) to Θ(V+E)(one productive pass + one confirming pass); the worst case is unmoved.
Free negative-cycle diagnosis ➔ if changed is still true at the end of round ∣V∣−1, the extra check pass is already implied — the two halves of the algorithm become one loop with a round counter.
Scope ➔ the unit outline lists this optimisation as a requirement, discussed in the studio/applied session and the bonus video; drill the exact form from that video before the competency test.
5. Any Negative Cycle, and Which Vertices Form It (applied P2, P3)
Super source ➔ add q with a weight-0 edge to every vertex and run Bellman-Ford once from q ⟹ every vertex is reachable, so every negative cycle is seen. O(VE), vs O(V2E) for one run per source.
The disconnected caveat ➔ the V added edges make it O(V(E+V))=O(VE+V2), which collapses to O(VE) only when G is connected (E≥V−1) — V isolated vertices with E=0 is the case that needs the V2.
An updated vertex need not lie ON the cycle ➔ the ∣V∣-th pass also lowers vertices downstream of the cycle, and may leave some cycle vertices unchanged — which ones depends on the edge order. Never report the updated set as the cycle.
Extract by walking pred back ➔ from any vertex updated in the ∣V∣-th pass, follow pred until a vertex u repeats; the cycle is u plus every vertex between its two occurrences. A visited array costs Θ(V) auxiliary.
O(1)-auxiliary variant ➔ step back ∣V∣−1 times first — guaranteed to land on the cycle — then follow pred once round until returning to that vertex.
Cost ➔ O(VE) run +O(V) walk ⟹ O(VE).
⚙️ Core Implementation
🔹 Bellman-Ford (unit pseudocode + early exit)
Code
def bellman_ford(vertices, edges, source): # Step 1: initialise O(V) dist = [INF] * len(vertices) pred = [None] * len(vertices) dist[source] = 0 # Step 2: relax every edge, V-1 times O(V) x O(E) for i in range(1, len(vertices)): changed = False for (u, v, w) in edges: if dist[u] != INF and dist[u] + w < dist[v]: dist[v] = dist[u] + w pred[v] = u changed = True if not changed: # fixed point reached early return dist, pred # no negative cycle is possible # Step 3: one more pass = negative-cycle check O(E) for (u, v, w) in edges: if dist[u] != INF and dist[u] + w < dist[v]: error("Graph contains a negative-weight cycle") return dist, pred
💡 Common Mistake:Omitting the dist[u] != INF guard ➔ ∞+(−4)<∞ evaluates true with a large sentinel integer, so unreached vertices leak finite garbage distances and the cycle check fires on a graph that has no cycle.
💡 Common Mistake:Returning distances after detecting a cycle ➔ the answer to “what is the shortest distance” is that none exists; a table of numbers is wrong, not partially right.
🔹 Extract one negative cycle (applied P3)
Code
def negative_cycle(n, edges): # vertices 0..n-1, edges (u, v, w) q = n # super source: weight-0 edge to every vertex aug = edges + [(q, v, 0) for v in range(n)] dist = [INF] * (n + 1) pred = [None] * (n + 1) dist[q] = 0 for _ in range(n): # |V'| - 1 = n rounds for (u, v, w) in aug: if dist[u] != INF and dist[u] + w < dist[v]: dist[v] = dist[u] + w pred[v] = u x = None for (u, v, w) in aug: # the check pass if dist[u] != INF and dist[u] + w < dist[v]: dist[v] = dist[u] + w pred[v] = u x = v # updated, but maybe OFF the cycle if x is None: return None # no negative cycle anywhere in G for _ in range(n): # step back onto the cycle, O(1) aux x = pred[x] cycle, y = [x], pred[x] while y != x: # one lap round the cycle cycle.append(y) y = pred[y] return cycle[::-1] # in edge direction
💡 Common Mistake:Returning x itself as a cycle vertex ➔ the vertex that changed in the check pass may only hang off the cycle; the backward steps are what guarantee you are on it.
⚖️ Complexity
Adjacency list(an edge list is enough — Bellman-Ford never asks for a vertex’s neighbours).
Auxiliary is Θ(V), not Θ(VE) ➔ only dist and pred are stored; the ∣V∣−1 rounds overwrite one array in place. The trace table’s V×V grid is a revision device, not the implementation.
All-pairs by repetition ➔ O(V)×O(VE)=O(V2E)=O(V4) dense ⟹ strictly worse than Floyd-Warshall’s Θ(V3).
When It Flips: the hinge is the sign of the weights, then the number of sources, then density. One negative edge moves you from Dijkstra to Bellman-Ford; needing all pairs moves you to Floyd-Warshall on a dense graph and to Johnson's — one Bellman-Ford, then V Dijkstras — on a sparse one. V separate Bellman-Fords is never the answer.
Final Extracted Output: the checking pass still improvesu and v ⟹ negative cycle, so the algorithm returns an error, not the i=4 column. The culprit is u→w→x→v→u at 3−1−1−4=−3, and every lap round it drives u and v down by a further 3.
Read the trap ➔ the i=4 column looks like a finished answer. Without step 3 you would hand it in. A Bellman-Ford answer is only an answer once the extra pass changes nothing.
Read the DP layering ➔ x is ∞ until i=3 because the shortest walk s→u→w→x needs three edges; a vertex first becomes finite in round = its hop count from s.
Applied Exercise — bound the damage of a single negative edge
Problem: show a graph where Dijkstra is wrong but Bellman-Ford is right, using only the graph above’s shape.
Take Dijkstra: Bellman-Ford: s→u(5),s→v(8),v→u(−4) only (no cycle).serves u at 5,finalises it,then serves v at 8;v→u is skipped as u is visited⇒dist[u]=5.i=1⇒u=5,v=8;i=2⇒8+(−4)=4<5⇒u=4.
Final Extracted Output:dist[u]=4 via s→v→u. The recipe: a cheap-looking direct edge that is beaten by a longer route ending in a negative edge — exactly the greedy-finalisation failure, with no cycle needed.
Applied Exercise — the updated vertex is off the cycle (applied P3)
Problem:s→a(0), s→b(0), s→c(0), s→d(0)(s already plays the super source), d→a(−1), d→c(−5), c→b(1), b→d(2). Output the vertices of a negative cycle.
After round ∣V∣−1:Round ∣V∣:Walk pred from a:Cycle:a=−1(d),b=−4(c),c=−5(d),d=−2(b)a=−2−1=−3,c=−2−5=−7;b,dunchangeda←d←b←c←d⇒drepeatsd→c→b→d=−5+1+2=−2
Final Extracted Output: the cycle is {d,c,b}. Read the trap ➔ the updated set {a,c} contains a, which is off the cycle (it only hangs off d), and misses b and d, which are on it.
⚠️ Common Mistakes
💡 Looping ∣V∣ times “to be safe” ➔ the ∣V∣-th pass is the cycle check; folding it into the main loop destroys the diagnosis and silently returns −∞-bound garbage.
💡 “Bellman-Ford handles negative cycles” ➔ it detects them. No algorithm can return a shortest distance that does not exist.
💡 Claiming it finds every negative cycle ➔ only those reachable from the source. Say so, and offer the weight-0 super source or Floyd-Warshall as the fix.
💡 Reporting the vertices updated in the ∣V∣-th pass as the cycle ➔ downstream vertices update too and some cycle vertices may not; walk pred back to a repeat (§5).
💡 Quoting O(V2) or O(ElogV) ➔ the bound is Θ(VE); the dense instantiation is Θ(V3), and it must be stated with E named (Graph §3).
💡 Storing a V×V table ➔ auxiliary space is Θ(V); the grid exists only for hand-tracing.
🧠 Active Recall
Why exactly ∣V∣−1 rounds — not ∣V∣, not ∣E∣?
Answer
Short answer: a shortest path is simple, and a simple path in a ∣V∣-vertex graph has at most ∣V∣−1 edges.
Why:Each round extends reach by one edge ➔ after round i, every distance achievable with ≤i edges is already correct, so ∣V∣−1 rounds cover the longest path that can exist. Fewer rounds is unsafe ➔ a path of exactly ∣V∣−1 edges is legal and would be missed. More rounds is not “safer”, it is the test ➔ any improvement in round ∣V∣ can only come from a walk with a repeated vertex, i.e. a negative cycle ➔ §3.
Dijkstra is O(ElogV) and Bellman-Ford is Θ(VE). What exactly did the extra factor buy, and which line of code spends it?
Hint: which vertex is finalised, and when?
Answer
Short answer: it bought the removal of the non-negativity precondition; the spend is the outer for i in 1..V-1 loop, which exists because no vertex is ever finalised.
Why:Dijkstra’s logV is a queue cost, not a correctness cost ➔ its cheapness comes from serving each vertex once, which is only sound when a finalised estimate can never fall. A negative edge makes an estimate fall after finalisation ➔ so the repair is to re-examine every edge until the estimates stop moving, and the number of rounds needed is bounded by the longest simple path. The relaxation line is identical ➔ what changed is the schedule, which is why the two algorithms share their pred backtracking and their DP framing ➔ Dynamic Programming.
Your run finishes and the ∣V∣-th pass improves one vertex by 3. What do you write, and what must you NOT write?
Answer
Short answer: write “a negative cycle reachable from s exists, so no shortest distance is defined” — do not write a distance table.
Why:The improvement certifies a non-simple walk ➔ after ∣V∣−1 rounds every simple path is already accounted for, so a further improvement uses ≥∣V∣ edges and must repeat a vertex; the repeated segment is a cycle whose total weight is negative. The infimum is −∞ ➔ each lap subtracts the same amount again, so no finite number is the answer. Scope the claim ➔ reachable from s; a negative cycle in an unreachable component is not detected by this run ➔ §5 super source, or Floyd-Warshall §4.
The ∣V∣-th pass improves vertex a. Is a on the negative cycle, and how do you output the cycle's vertices in O(VE)?
Answer
Short answer: not necessarily — walk pred back from a until a vertex repeats; the vertices between its two occurrences are the cycle.
Why:Improvements ripple downstream ➔ every vertex reachable from the cycle keeps falling, so a may only hang off it (applied P3: a hangs off d). The pred chain must enter the cycle ➔ each improvement was caused by its predecessor, the source never improves, and with finitely many vertices the chain must repeat. Cost ➔ an O(V) walk on top of the O(VE) run — with a visited array, or step back ∣V∣−1 times and lap once for O(1) auxiliary.