Advanced graphs: Dijkstra, MST, Bellman-Ford¶
At a glance
Priority: P1 · Complexity: ⅘ · Est. time: 4 h · Phase: 4 · Prereqs: Graphs, Heap You're done when: you can choose between BFS, 0-1 BFS, Dijkstra, Bellman-Ford and Floyd-Warshall from the constraints, write Dijkstra and Kruskal/Prim from memory, and recognise minimax-path problems as Dijkstra variants.
Why it matters¶
Weighted graphs model routing (roads, shipping lanes, network hops), cost minimisation and dependency scheduling. Interviews at FAANG include these as Medium/Hard: Network Delay Time, Cheapest Flights Within K Stops, Swim in Rising Water, Alien Dictionary, Reconstruct Itinerary. In production, Dijkstra/A* powers routing engines, OSPF/IS-IS use it, Bellman-Ford underlies distance-vector routing, and MSTs appear in network design and clustering.
Core concepts¶
Pattern recognition signals¶
| When you see… | Think… |
|---|---|
| Shortest path with non-negative weights | Dijkstra |
| Edge weights only 0/1 | 0-1 BFS with a deque |
| Negative edges, or "at most K edges/stops" | Bellman-Ford (K rounds) |
| All-pairs shortest, V ≤ ~400 | Floyd-Warshall O(V³) |
| "Connect all nodes at minimum total cost" | MST (Kruskal or Prim) |
| "Minimise the maximum edge/effort along the path" | Minimax: Dijkstra with max instead of +, or binary search + BFS |
| "Order characters/tasks from pairwise constraints" | Build the graph, then topological sort |
| "Use every edge exactly once" | Eulerian path (Hierholzer) |
| "Critical link / bridge / articulation point" | Tarjan low-link |
| Geometry/grid with a heuristic and a single goal | A* |
Template 1: Dijkstra with a heap (lazy deletion), O((V+E) log V)¶
import heapq
from collections import defaultdict
def dijkstra(n, edges, src):
g = defaultdict(list)
for u, v, w in edges:
g[u].append((v, w))
dist = [float('inf')] * n
dist[src] = 0
h = [(0, src)]
while h:
d, u = heapq.heappop(h)
if d > dist[u]:
continue # stale entry
for v, w in g[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
heapq.heappush(h, (nd, v))
return dist
Correctness needs non-negative weights: once popped, dist[u] is final because any other path to u goes through a node with distance ≥ d and adds non-negative weight.
Template 2: Bellman-Ford with K limit (Cheapest Flights Within K Stops)¶
def find_cheapest_price(n, flights, src, dst, k):
dist = [float('inf')] * n
dist[src] = 0
for _ in range(k + 1): # k stops = k+1 edges
nxt = dist[:] # copy: use only last round's values
for u, v, w in flights:
if dist[u] + w < nxt[v]:
nxt[v] = dist[u] + w
dist = nxt
return -1 if dist[dst] == float('inf') else dist[dst]
O(K·E). Negative-cycle detection: a V-th round that still relaxes means a negative cycle is reachable.
Template 3: minimax path (Path With Minimum Effort / Swim in Rising Water)¶
def minimum_effort_path(h):
R, C = len(h), len(h[0])
best = [[float('inf')] * C for _ in range(R)]
best[0][0] = 0
pq = [(0, 0, 0)]
while pq:
eff, r, c = heapq.heappop(pq)
if (r, c) == (R - 1, C - 1):
return eff
if eff > best[r][c]:
continue
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < R and 0 <= nc < C:
ne = max(eff, abs(h[nr][nc] - h[r][c])) # max instead of +
if ne < best[nr][nc]:
best[nr][nc] = ne
heapq.heappush(pq, (ne, nr, nc))
The same algorithm works because the "cost" is monotone non-decreasing along a path.
Template 4: MST, Kruskal (edge sort + DSU) and Prim (heap)¶
def kruskal(n, edges): # edges: (w, u, v)
dsu = DSU(n) # from the graphs page
total = used = 0
for w, u, v in sorted(edges):
if dsu.union(u, v):
total += w; used += 1
if used == n - 1:
break
return total if used == n - 1 else -1 # -1: graph disconnected
def prim(n, g): # g[u] = [(v, w)]
seen, h, total = set(), [(0, 0)], 0
while h and len(seen) < n:
w, u = heapq.heappop(h)
if u in seen: continue
seen.add(u); total += w
for v, wv in g[u]:
if v not in seen:
heapq.heappush(h, (wv, v))
return total if len(seen) == n else -1
Kruskal: O(E log E), best for sparse graphs. Prim (array version): O(V²), best for dense graphs (Min Cost to Connect All Points).
Template 5: Hierholzer's Eulerian path (Reconstruct Itinerary)¶
from collections import defaultdict
def find_itinerary(tickets):
g = defaultdict(list)
for a, b in sorted(tickets, reverse=True): # pop() yields smallest first
g[a].append(b)
route = []
def dfs(u):
while g[u]:
dfs(g[u].pop())
route.append(u) # post-order
dfs("JFK")
return route[::-1]
Algorithm selection table¶
| Algorithm | Handles | Time | Notes |
|---|---|---|---|
| BFS | unweighted | O(V+E) | |
| 0-1 BFS | weights 0/1 | O(V+E) | deque: 0-edges appendleft |
| Dijkstra | w ≥ 0 | O((V+E) log V) | fails with negatives |
| Bellman-Ford | negatives, K-limited | O(VE) | detects negative cycles |
| SPFA | negatives (heuristic) | avg fast, worst O(VE) | avoid in interviews |
| Floyd-Warshall | all pairs, negatives | O(V³) | V ≤ ~500 |
| A* | single goal + heuristic | depends | admissible, consistent heuristic |
| Kruskal / Prim | MST | O(E log E) / O(E log V) | undirected only |
| Tarjan bridges | bridges/articulation | O(V+E) | DFS low-link |
flowchart TD
A[Weighted shortest path] --> B{Negative weights or hop limit K?}
B -- yes --> C[Bellman-Ford K rounds]
B -- no --> D{Weights all 0/1?}
D -- yes --> E[0-1 BFS]
D -- no --> F{All pairs?}
F -- "yes, small V" --> G[Floyd-Warshall]
F -- no --> H[Dijkstra / A*]
I[Connect all cheaply] --> J{Dense?}
J -- yes --> K[Prim O V^2]
J -- no --> L[Kruskal + DSU]
Common bugs¶
- Dijkstra with negative edges (silent wrong answers).
- Missing the
if d > dist[u]: continuestale check (works but slower, and can be wrong for some counting variants). - Bellman-Ford K-limit without copying
dist(lets a path use more than K edges in one round). - Alien Dictionary: forgetting the invalid-prefix case ("abc" before "ab"), and characters with no edges (still in the output).
- Directed vs undirected edges when building the graph (MST is undirected).
- Using Kruskal without checking the result is spanning (disconnected input).
Resources¶
| Resource | Type | Why this one | Level | Cost |
|---|---|---|---|---|
| cp-algorithms: Dijkstra |
article | Correctness proof and dense/sparse variants | advanced | free |
| cp-algorithms: Bellman-Ford | article | Negative-cycle detection and path recovery | advanced | free |
| cp-algorithms: Kruskal / Prim | article | Both MST algorithms with proofs | advanced | free |
| cp-algorithms: 0-1 BFS |
article | The deque trick most people miss | advanced | free |
| cp-algorithms: Euler path | article | Hierholzer explained | advanced | free |
| cp-algorithms: Bridge searching | article | Tarjan low-link | advanced | free |
| VisuAlgo: SSSP and MST | interactive | Animated Bellman-Ford and Dijkstra | intermediate | free |
| Jeff Erickson: Shortest paths |
book | The unifying "relax" framework | advanced | free |
| NeetCode roadmap: Advanced Graphs | video | Interview-focused walkthroughs | intermediate | freemium |
| Red Blob Games: A* |
interactive | Best visual on heuristics | intermediate | free |
Hands-on lab¶
| # | LC | Problem | Difficulty | Set | Key insight |
|---|---|---|---|---|---|
| 1 | 743 | Network Delay Time | Medium | NC150 | Dijkstra from k; answer = max dist, -1 if unreachable. |
| 2 | 1584 | Min Cost to Connect All Points | Medium | NC150 | Dense graph -> O(n^2) Prim beats Kruskal's sort. |
| 3 | 787 | Cheapest Flights Within K Stops | Medium | NC150 | Bellman-Ford k+1 rounds on a copy of dist. |
| 4 | 1514 | Path with Maximum Probability | Medium | NC250+ | Dijkstra with max-heap on product. |
| 5 | 1631 | Path With Minimum Effort | Medium | NC250+ | Minimax path: Dijkstra on max-edge (or BS + BFS). |
| 6 | 332 | Reconstruct Itinerary | Hard | NC150 | Hierholzer's Eulerian path, lexical order, post-order append. |
| 7 | 778 | Swim in Rising Water | Hard | NC150 | Minimax Dijkstra on max(elevation). |
| 8 | 269 | Alien Dictionary | Hard | NC150 | Edges from first differing char; topo sort; prefix trap. (Premium) |
| 9 | 2392 | Build a Matrix With Conditions | Hard | NC250+ | Two independent topological sorts (rows, cols). |
| 10 | 1192 | Critical Connections in a Network | Hard | NC250+ | Tarjan bridges: low[v] > disc[u]. |
| 11 | 1489 | Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree | Hard | NC250+ | Kruskal with edge excluded / forced. |
Stretch exercise: implement A* on a grid with Manhattan heuristic and compare expanded nodes with Dijkstra on 20 random mazes. Then implement Floyd-Warshall on a 200-node graph and cross-check with running Dijkstra from every node.
Questions¶
L1 — Recall¶
Q1. Why does Dijkstra fail with negative edges?
Answer
It finalises a node when popped, assuming no later path can be shorter. A negative edge can make a later, longer-hop path cheaper, violating that assumption. Use Bellman-Ford, or reweight with Johnson's algorithm for all-pairs.
Q2. What is the cut property behind MST algorithms?
Answer
For any cut of the graph, the minimum-weight edge crossing the cut belongs to some MST. Kruskal picks the globally lightest edge not forming a cycle. Prim grows one tree by the lightest crossing edge.
Q3. Complexity of Dijkstra with a binary heap vs Fibonacci heap vs array?
Answer
Binary heap: O((V+E) log V). Fibonacci heap: O(E + V log V) (rarely faster in practice). Array scan: O(V²), which is best for dense graphs.
L2 — Apply¶
Q4. Implement Network Delay Time.
Answer
def network_delay_time(times, n, k):
g = defaultdict(list)
for u, v, w in times:
g[u].append((v, w))
dist = {}
h = [(0, k)]
while h:
d, u = heapq.heappop(h)
if u in dist: continue
dist[u] = d
for v, w in g[u]:
if v not in dist:
heapq.heappush(h, (d + w, v))
return max(dist.values()) if len(dist) == n else -1
Q5. Alien Dictionary: outline the algorithm and the traps.
Answer
For each adjacent word pair, find the first differing character c1≠c2 and add the edge c1→c2 (dedupe). If no difference exists and the first word is longer, the input is invalid, so return "". Include all characters as nodes (even with no edges). Run Kahn's algorithm: if the output length is less than the number of unique characters, there's a cycle, so return "". Not unique in general (any valid order is accepted).
Q6. Trace 0-1 BFS on edges 0→1 (w=0), 0→2 (w=1), 1→2 (w=1), 2→3 (w=0).
Answer
Deque [0], dist0=0. Pop 0: 0→1 weight 0 gives dist1=0 (appendleft); 0→2 weight 1 gives dist2=1 (append). Pop 1 (front): 1→2 gives 0+1=1, not less than 1. Pop 2: 2→3 weight 0 gives dist3=1 (appendleft). Result [0,0,1,1].
L3 — Design & trade-offs¶
Q7. Path With Minimum Effort: Dijkstra vs binary search + BFS vs union-find.
Answer
Dijkstra with max: O(RC log RC). Binary search on effort, with a BFS feasibility check: O(RC log maxH), simpler to reason about and no heap. Union-find: sort edges by weight and add until the start and end connect, O(RC log RC), which is the Kruskal view (the minimax path lies on the MST). All are valid, so mention that the minimax path equals the MST path, which is a nice unifying fact.
Q8. Cheapest flights within K stops: why not plain Dijkstra?
Answer
The best cost to a node with more stops may be cheaper but unusable, while a costlier path with fewer stops may be the only valid one. State must include hops: Dijkstra over (node, stops) or track stops in the heap entries with a best-stops-per-node prune, or Bellman-Ford with K+1 rounds, which fits naturally. Complexity O(K·E) versus O(K·E log) for the state-space Dijkstra.
Q9. Kruskal vs Prim: how do you choose?
Answer
Sparse graphs or edges already in a list: Kruskal (sort plus DSU). Dense graphs or an implicit complete graph (points in a plane): Prim with an O(V²) array, avoiding sorting E ≈ V² edges. Streaming edges or needing an incremental MST: Kruskal-style with a link-cut tree, or offline. Both give the same total weight when weights are distinct.
L4 — Staff-level ambiguity¶
Q10. Design routing for a container-shipping network (ports, legs with cost, transit time, capacity, schedule) to quote the cheapest route with ≤ 3 transshipments in < 100 ms.
Answer
A time-expanded graph (a node per port and departure) handles schedules, so waiting is an edge. The hop limit becomes a layered Dijkstra state (node, transshipments). Multi-criteria (cost vs time) needs Pareto-frontier labels, or a weighted objective with user-chosen weights. Precompute for latency: contraction hierarchies or landmark (ALT) A* preprocessing for large graphs, though a network of ~1k ports plus ~10k weekly legs is small enough that a plain layered Dijkstra runs in milliseconds. Capacity constraints turn this into a flow or ILP problem, not a shortest path, so separate "quote" (unconstrained fast path) from "book" (feasibility check plus allocation). Cache popular lanes, invalidate on schedule changes. Say what you'd cut for v1.
Q11. An engineer implemented Dijkstra on a graph with a few negative edges 'because it works in tests'. How do you respond?
Answer
Show a 3-node counterexample (A→B 2, A→C 3, C→B −2: Dijkstra finalises B at 2, but the true optimum is 1). Explain that it can pass tests by luck of edge ordering. Give options: Bellman-Ford/SPFA for a single source, Johnson reweighting (potentials) for all-pairs or repeated queries, or rethink the model (are negative costs really discounts? cap or floor them?). Add a property test comparing against Bellman-Ford on random graphs. The influence lesson: replace opinion with a minimal counterexample plus a test.
Real-world use cases¶
- Routing engines: road and maritime routing (Dijkstra/A* with contraction hierarchies).
- Network protocols: OSPF/IS-IS (Dijkstra), RIP (Bellman-Ford).
- Infrastructure design: MST for minimum cable/pipeline networks, and clustering (single-linkage = MST cut).
- Arbitrage/negative-cycle detection in currency graphs (Bellman-Ford), and dependency conflict detection.
Pitfalls & anti-patterns¶
- Choosing Dijkstra by reflex without checking weight signs.
- Ignoring the state dimension (stops, keys, fuel) in the model.
- Building graphs in O(V²) when edges are implicit and sparse.
- Recomputing all-pairs when a single-source query suffices.
Checklist¶
- I can write Dijkstra, Bellman-Ford (K-limited), Kruskal and Prim from memory
- I can recognise minimax-path and stateful-shortest-path problems
- I solved 743, 787 and 269 unaided within 35 min
- I answered all L3 questions out loud in < 3 min each