From unweighted to weighted: why BFS isn't enough anymore
BFS finds shortest paths in unweighted graphs by exploring in strict distance order — every edge "costs" 1, so the first time you reach a node is necessarily via the fewest edges. The moment edges have different weights, that guarantee breaks: a path with more edges can easily have a smaller total weight than a path with fewer edges. You need algorithms that reason about cumulative edge weight rather than edge count. This subtopic covers the three that matter for interviews, each suited to a different constraint on the graph.
Dijkstra's algorithm — non-negative weights, single source
The idea: maintain a running best-known distance to every vertex (initialized to ∞, except the source at 0), and repeatedly select the unfinalized vertex with the smallest known distance, "finalize" it, and relax all of its outgoing edges (i.e., check whether going through this vertex improves the distance to its neighbors). A min-heap makes "find the smallest unfinalized distance" efficient.
import heapq
def dijkstra(n, graph, source):
# graph[u] = list of (v, weight)
dist = [float('inf')] * n
dist[source] = 0
heap = [(0, source)]
visited = set()
while heap:
d, u = heapq.heappop(heap)
if u in visited:
continue # stale heap entry — a shorter path already finalized u
visited.add(u)
for v, weight in graph[u]:
if d + weight < dist[v]:
dist[v] = d + weight
heapq.heappush(heap, (dist[v], v))
return distWhy negative weights break it — the argument to state explicitly: Dijkstra's correctness relies on a greedy claim: once a vertex is popped from the heap with the smallest current distance, that distance is final and can never be improved later. This is only true if every edge weight is non-negative — because if all remaining edges add non-negative weight, no future path through an unfinalized (necessarily-larger-or-equal-distance) vertex could possibly produce a smaller distance to the vertex you just finalized. A negative edge shatters this: a longer, seemingly-worse path could later take a negative-weight shortcut and undercut a distance you already "locked in" and will never revisit. Dijkstra has no mechanism to revisit a finalized vertex, so it produces a wrong, no-error, silently-incorrect answer on graphs with negative edges — this is the single most important fact to say out loud about Dijkstra in an interview.
Bellman-Ford — handles negative weights, detects negative cycles
The idea: instead of greedily finalizing vertices, simply relax every edge in the graph, and repeat this for V - 1 rounds. Why V - 1? Because the longest possible simple shortest path (no repeated vertices) has at most V - 1 edges, and each full round of relaxation is guaranteed to correctly extend the shortest-path prefix by at least one more edge for every vertex.
def bellman_ford(n, edges, source):
# edges = list of (u, v, weight)
dist = [float('inf')] * n
dist[source] = 0
for _ in range(n - 1):
updated = False
for u, v, weight in edges:
if dist[u] != float('inf') and dist[u] + weight < dist[v]:
dist[v] = dist[u] + weight
updated = True
if not updated:
break # optimization: stop early if nothing changed this round
# One more round: if anything still improves, a negative cycle is reachable from source
for u, v, weight in edges:
if dist[u] != float('inf') and dist[u] + weight < dist[v]:
return None # negative cycle detected
return distThe extra V-th round is the negative-cycle detector: if a valid shortest path can have at most V - 1 edges, then any further improvement after V - 1 full rounds means you're riding a cycle that keeps reducing the "distance," which is only possible if that cycle has negative total weight. This is a distinct and important skill from just computing distances — many interview questions specifically ask you to detect whether a negative cycle exists (e.g., in currency-arbitrage-style problems), not just compute shortest paths assuming one doesn't.
Floyd-Warshall — all pairs at once, O(V³)
The idea: dynamic programming over an increasing set of allowed intermediate vertices. Define dist[i][j] as the shortest path from i to j using only vertices {1, ..., k} as intermediates; at each step k, ask whether routing through vertex k improves any pair: dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]).
def floyd_warshall(n, edges):
INF = float('inf')
dist = [[INF] * n for _ in range(n)]
for i in range(n):
dist[i][i] = 0
for u, v, weight in edges:
dist[u][v] = min(dist[u][v], weight) # keep the smaller if there are multi-edges
for k in range(n):
for i in range(n):
if dist[i][k] == INF:
continue # skip: no path through k helps from i
for j in range(n):
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
return dist # dist[i][i] < 0 for some i implies a negative cycleReach for Floyd-Warshall specifically when you need every pair's shortest distance and V is small (roughly V ≤ 400–500, since it's O(V³)) — running Dijkstra from every vertex costs O(V · E log V), which can actually be worse than O(V³) on dense graphs, and Floyd-Warshall's implementation is dramatically simpler (three nested loops, no heap). It also handles negative edge weights (though not negative cycles reachable in a way that makes distances undefined — those show up as a negative value on the diagonal, dist[i][i] < 0).
Decision table: which algorithm, when
| Situation | Algorithm | Why |
|---|---|---|
| Single source, all edge weights ≥ 0 | Dijkstra | Fastest option for this common case: O((V + E) log V) with a binary heap |
| Single source, some edge weights < 0 | Bellman-Ford | Dijkstra's greedy finalization is provably wrong here |
| Need to detect a negative cycle | Bellman-Ford | The extra V-th relaxation round is a direct negative-cycle test |
| Need shortest paths between every pair of vertices | Floyd-Warshall (small V) or Dijkstra from every vertex (sparse, larger V, non-negative weights) | Floyd-Warshall is O(V³) and trivial to implement; repeated Dijkstra is O(V·E log V), better when the graph is sparse |
| Unweighted graph (or all edges cost 1) | Plain BFS | Dijkstra with unit weights reduces to BFS but with unnecessary O(log V) heap overhead |
| Edge weights are only 0 or 1 | 0-1 BFS (deque, push 0-weight to front) | O(V + E), avoids the heap entirely — a specialized trick worth knowing exists, even if it's less commonly required in interviews |
| "At most k stops/edges" constraint | Bellman-Ford-style, capped at k rounds (or BFS/Dijkstra over an expanded (node, stops_used) state space) | Dijkstra's standard form has no notion of "steps used," so the state needs augmenting |
| Unit-cost moves on an invented state (locks, word transforms, keys on a grid) | BFS / bidirectional BFS on the state graph — see State-space & Implicit Graph BFS | Same shortest-path idea; the modeling work is defining the vertex, not picking Dijkstra |
Senior composition patterns
Two variants show up constantly once the textbook algorithms are solid:
Counting shortest paths. Run Dijkstra (or BFS if unweighted) as usual, but keep a parallel ways[v] array: when you find a strictly better distance to v, set ways[v] = ways[u]; when you find an equal distance, add ways[u] into ways[v] (mod 10⁹+7 if required). Do not finalize a node in a way that drops equal-cost arrivals — the stale-entry skip still applies for worse distances, but equal distances must still contribute to the count.
Minimize the bottleneck (max edge on the path), not the sum. Classic prompt: "swim when the water level is at least the max height on your path — minimize that level." Two equivalent approaches:
- Dijkstra keyed on bottleneck — heap priority is
max(cost_so_far, grid[nr][nc])instead ofcost_so_far + weight. First time you pop the destination, that bottleneck is optimal. - Binary search on the answer + BFS — guess level
mid; BFS only through cells≤ mid; search the minimum feasiblemid.
Both are O(V log V + E log V) or O(E log W) respectively; saying both options is a strong Senior signal. This is not an MST problem even when it appears next to spanning-tree drills — you need a path from A to B with a min-max objective, not a global connector of all vertices.
Complexity, precisely
| Algorithm | Time | Space |
|---|---|---|
| Dijkstra (binary heap) | O((V + E) log V) | O(V + E) |
| Dijkstra (no heap, dense graph) | O(V²) | O(V) |
| Bellman-Ford | O(V · E) | O(V) |
| Floyd-Warshall | O(V³) | O(V²) |
The heap-based Dijkstra bound comes from: each vertex is popped once (O(V log V)), and each edge can trigger at most one heap push (O(E log V)) — dominating for typical sparse graphs where E is the larger term. Bellman-Ford's O(VE) comes directly from V - 1 rounds, each doing O(E) work relaxing every edge.
Pitfalls and interview gotchas
- Using Dijkstra on a graph with negative weights and getting a wrong answer with no error. This is the single most tested conceptual gotcha in this subtopic — always ask about the sign of edge weights before choosing an algorithm.
- Forgetting the "stale heap entry" check in Dijkstra. Because you can push a vertex to the heap multiple times before it's finalized (once per improving relaxation), you must skip a popped vertex if it's already been visited/finalized — otherwise you redo work and, worse, can corrupt results in variants that track path counts or additional state.
- Off-by-one on Bellman-Ford's round count. It's
V - 1rounds to guarantee correctness, plus one extra round specifically to detect negative cycles — conflating these two (e.g., stopping afterV - 1rounds and assuming "no more updates possible" without the extra check) misses cycle detection entirely. - Floyd-Warshall's loop order matters.
kmust be the outermost loop, notiorj— the DP invariant ("using only intermediates from{1..k}") is only valid if you fully complete eachkbefore moving to the next. Swapping loop order silently produces incorrect results without any error. - Disconnected graphs / unreachable vertices. Represent "no path yet" as infinity consistently, and guard against infinity-plus-weight overflowing into a false "improvement" (
if dist[u] != INF and ...— skip the check entirely rather than letINF + weightwrap or compare incorrectly). - Self-loops and multi-edges with weights. A self-loop with positive weight is irrelevant (never helps); with negative weight, it's itself a negative cycle. Multiple edges between the same pair of nodes just need "keep the minimum weight" when building your representation — don't silently overwrite with the last one seen.
- Confusing shortest path with minimum spanning tree. They sound similar and both commonly use a heap, but they optimize different things: shortest path minimizes total weight from one source to each destination independently, while MST (Advanced Niche Algorithms) minimizes the total weight to connect all vertices together. Dijkstra and Prim's are structurally nearly identical algorithms (grow a frontier via a min-heap) but their priority key differs: Dijkstra keys on cumulative distance from source, Prim's keys on the single edge weight crossing into the frontier. Min-bottleneck path problems (Swim in Rising Water) are still shortest-path variants — not MST.
Where this reappears
The heap mechanics here are exactly the ones from Heap Fundamentals & Top-K Pattern — if Dijkstra's use of a min-heap feels unfamiliar, that's the prerequisite to revisit. Prim's algorithm (Minimum Spanning Tree, in Advanced Niche Algorithms) reuses this same frontier-expansion-via-heap skeleton with one small but consequential change to the priority key, which is the cleanest way to see the shortest-path/spanning-tree distinction concretely. When the "graph" is something you invent (password dials, bus rides, key bitmasks), start from State-space & Implicit Graph BFS and only upgrade to a heap when transition costs stop being uniform.
Further Resources (Optional)
- CP-Algorithms — Dijkstra AlgorithmArticle20m
- CP-Algorithms — Bellman-Ford AlgorithmArticle15m
- CP-Algorithms — Floyd-Warshall AlgorithmArticle15m
- CP-Algorithms — Maximum Flow (Edmonds-Karp / Ford-Fulkerson)Article30m
- VisuAlgo — Single-Source Shortest Paths (SSSP) visualizationReference25m
- USACO Guide — Shortest Paths with Non-Negative Edge WeightsCourse20m
- WilliamFiset — Dijkstra's Shortest Path Algorithm (YouTube)Video15m
- WilliamFiset — Bellman-Ford Algorithm: Shortest Path & Negative Cycles (YouTube)Video12m
- USACO Guide — Shortest Paths with Negative Edge Weights (Bellman-Ford, negative cycles, SPFA)Course25m
- Book: Algorithms (Sedgewick & Wayne, 4th ed.) — §4.4 "Shortest Paths" (Dijkstra, Bellman-Ford, DAG shortest paths; pp. 638-680)Book40m
- Book: Introduction to Algorithms (CLRS, 4th ed.) — Ch. 22 "Single-Source Shortest Paths" (Bellman-Ford §22.1, Dijkstra §22.3; pp. 604-645) + Ch. 23 "All-Pairs Shortest Paths" (Floyd-Warshall; pp. 646-669)Book50m
- Book: The Algorithm Design Manual (Skiena, 3rd ed.) — Ch. 6 "Weighted Graph Algorithms" (pp. 191-229)Book35m
Practice Problems
Interview relevancy:!!!Critical — must-have!!Important — highly recommended!Good to have~Niche — rarely asked
- Network Delay TimeMedium!!!2/525m
- Path with Maximum ProbabilityMedium!3/525m
- Cheapest Flights Within K StopsMedium!!3/530m
- Path With Minimum EffortMedium!!3/530m
- Find the City With the Smallest Number of Neighbors at a Threshold DistanceMedium!3/530m
- Number of Ways to Arrive at DestinationMedium!!3/530m
- Swim in Rising WaterHard!!4/545m
Optional Practice (Extra Reps)
For once you've cleared the main set above and want more reps on this pattern. These don't count toward the roadmap's progress stats — solve them purely for your own benefit.
- Minimum Cost to Make at Least One Valid Path in a GridHard~4/545m
- Cycle FindingCSES~3/530m