Shortest Paths
Find the cheapest route between nodes when edges carry weights, choosing Dijkstra, Bellman-Ford or 0-1 BFS based on the weights and constraints.
Instinct Weighted edges plus "minimum cost, time or effort to reach" - grab a min-heap and always expand the cheapest unsettled node next (Dijkstra).
Memory hack Pop the cheapest, skip the stale, relax the rest - Dijkstra is a ripple in a pond that touches near shores before far ones.
At a glance
When to use it
- Edges carry different costs such as time, price, effort or probability
- You need the cheapest route from one source to a node or to every node
- Weights are non-negative, or there is a cap on stops or edges used
- Path cost is a sum, product or running max that never improves as the path grows
Key idea
Dijkstra pops the cheapest unsettled node from a min-heap and relaxes its edges, so each node is finalised once in O((V + E) log V) instead of exploring every path; Bellman-Ford trades speed for negative weights and hop limits.
Typical complexity
O((V + E) log V) time, O(V + E) space
| # | Common problems | Level | Our solution | Practice |
|---|---|---|---|---|
| 1 | Network Delay Time | Medium | Read solution | LeetCode ↗ |
| 2 | Path with Maximum Probability | Medium | Read solution | LeetCode ↗ |
| 3 | Cheapest Flights Within K Stops | Medium | Read solution | LeetCode ↗ |
| 4 | Path With Minimum Effort | Medium | Read solution | LeetCode ↗ |
| 5 | Swim in Rising Water | Hard | Read solution | LeetCode ↗ |
The instinct
Plain BFS finds the fewest edges. The moment edges carry different costs (time, price, effort, risk), fewest edges no longer means cheapest, and you need a shortest-path algorithm. The default is Dijkstra: keep a min-heap of (costSoFar, node), pop the cheapest, and use it to improve its neighbours. Because every weight is non-negative, the first time a node leaves the heap its cost is final.
Two follow-up questions pick the exact tool: Can weights be negative? and Is there a limit on the number of edges/stops? Either one moves you to Bellman-Ford.
Recognise it
| Signal in the problem | Why it points here |
|---|---|
| "minimum time for a signal to reach all nodes" | Single-source shortest path; answer is the largest final distance (Network Delay Time). |
| "cheapest price", "minimum cost from src to dst" with weighted edges | Classic weighted shortest path. |
| "at most K stops / K edges" | Hop-limited path: Bellman-Ford with K+1 rounds, or BFS by level carrying cost. |
| "maximum probability of success" | Dijkstra with a max-heap and multiplication instead of addition (Path with Maximum Probability). |
| "minimise the maximum height difference / water level along a path" | Bottleneck path: path cost is a max, not a sum, but it never decreases, so Dijkstra still works (Path With Minimum Effort, Swim in Rising Water). |
| Grid where moving is free in some directions and costs 1 in others | 0-1 BFS with a deque. |
| Negative edge weights, or "detect a negative cycle" | Dijkstra breaks; use Bellman-Ford (or SPFA). |
| "shortest distance between every pair" with n ≤ ~400 | Floyd-Warshall, O(n³). |
Not this pattern if...
- All edges cost the same: plain BFS is simpler and O(V + E).
- You need to connect all nodes cheaply (total edge weight), not travel between two: that is a minimum spanning tree.
- The graph is a DAG: relax edges in topological order in O(V + E), no heap needed.
- You need the longest simple path in a general graph: that is NP-hard, not a shortest-path variant.
Memory hack
"Pop the cheapest, skip the stale, relax the rest." Those three verbs are the whole loop body of lazy Dijkstra.
Analogy: drop a stone in a pond where some channels are shallow (fast) and some are muddy (slow). The ripple reaches each shore at its true earliest time, and once it arrives there it cannot arrive "earlier" later. The heap is just a list of ripple fronts sorted by arrival time.
Say it out loud in the interview:
- "Edges are weighted and non-negative, so Dijkstra; negative or hop-limited, so Bellman-Ford."
- "What is the cost of a path - a sum, a product, or a max along the way? It just has to never get better as the path grows."
- "I will use lazy deletion: if a popped distance is larger than the best known, skip it."
How it works
Every node starts at infinity except the source at 0. Each pop gives the cheapest node still waiting; for each outgoing edge you check whether going through this node beats the neighbour's best known cost (that check-and-update is called relaxation). Improved neighbours are pushed again, so old, larger entries linger in the heap and are discarded when popped.
Trace on edges A→B 4, A→C 1, C→B 2, B→D 1, C→D 5, source A:
| Step | Pop | Action | A | B | C | D |
|---|---|---|---|---|---|---|
| 0 | - | start | 0 | ∞ | ∞ | ∞ |
| 1 | (0, A) | relax B=4, C=1 | 0 | 4 | 1 | ∞ |
| 2 | (1, C) | B: 1+2=3 < 4, D=6 | 0 | 3 | 1 | 6 |
| 3 | (3, B) | D: 3+1=4 < 6 | 0 | 3 | 1 | 4 |
| 4 | (4, B) | stale (4 > 3), skip | 0 | 3 | 1 | 4 |
| 5 | (4, D) | settled, no out-edges | 0 | 3 | 1 | 4 |
| 6 | (6, D) | stale, skip | 0 | 3 | 1 | 4 |
Notice step 2: the direct edge A→B looked best at first, but the detour through C was cheaper. That is exactly why BFS fails here.
Bellman-Ford instead relaxes every edge, round after round. After round k, each distance is the best path using at most k edges - which is precisely the "at most K stops" constraint in Cheapest Flights Within K Stops. Copy the distance array each round so one round cannot chain several edges.
Java template
import java.util.*; class ShortestPath { // Dijkstra with lazy deletion. adj.get(u) holds int[]{v, w}; weights must be >= 0. static long[] dijkstra(List<List<int[]>> adj, int src) { long[] dist = new long[adj.size()]; Arrays.fill(dist, Long.MAX_VALUE); dist[src] = 0; PriorityQueue<long[]> pq = new PriorityQueue<>((a, b) -> Long.compare(a[0], b[0])); pq.offer(new long[]{0, src}); while (!pq.isEmpty()) { long[] top = pq.poll(); int u = (int) top[1]; if (top[0] > dist[u]) continue; // stale entry // optional: if (u == target) return early for (int[] e : adj.get(u)) { long nd = dist[u] + e[1]; // combine: sum / max / product if (nd < dist[e[0]]) { dist[e[0]] = nd; pq.offer(new long[]{nd, e[0]}); } } } return dist; // Long.MAX_VALUE = unreachable } // Bellman-Ford limited to at most maxEdges edges. edges[i] = {from, to, w}. static int[] bellmanFord(int n, int[][] edges, int src, int maxEdges) { final int INF = Integer.MAX_VALUE; int[] dist = new int[n]; Arrays.fill(dist, INF); dist[src] = 0; for (int round = 0; round < maxEdges; round++) { int[] next = dist.clone(); // read old, write new for (int[] e : edges) { if (dist[e[0]] != INF && dist[e[0]] + e[2] < next[e[1]]) { next[e[1]] = dist[e[0]] + e[2]; } } dist = next; } return dist; } }
Complexity
| Algorithm | Time | Space | Use when |
|---|---|---|---|
| Dijkstra (binary heap, lazy) | O((V + E) log V) | O(V + E) | Non-negative weights |
| 0-1 BFS (deque) | O(V + E) | O(V) | Weights are only 0 or 1 |
| Bellman-Ford | O(V · E), or O(K · E) with a hop limit | O(V) | Negative weights, hop limits, cycle detection |
| Floyd-Warshall | O(V³) | O(V²) | All pairs, small V |
On a grid, V = rows × cols and E ≈ 4V.
Variations
- Sum of weights (classic): Network Delay Time - run Dijkstra once and return the maximum distance, or -1 if any node is unreachable.
- Hop-limited: Cheapest Flights Within K Stops - Bellman-Ford for K + 1 rounds, or Dijkstra whose state is
(node, stopsUsed). - Bottleneck / minimax: Path With Minimum Effort and Swim in Rising Water - path cost is
max(edge)along the path. Dijkstra works as is; binary search on the answer plus BFS, or union-find over sorted edges, also work. - Multiplicative: Path with Maximum Probability - use a max-heap and multiply; probabilities in (0, 1] only shrink, which keeps the greedy argument valid.
- 0-1 BFS: Minimum Cost to Make at Least One Valid Path in a Grid - push 0-cost moves to the front of a deque and 1-cost moves to the back.
- State-expanded graph: add extra dimensions to the node (keys collected, fuel left, stops used) and run Dijkstra on
(node, extra)pairs.
Common mistakes
Dijkstra is only correct with non-negative edge weights. A single negative edge can make an already-settled node cheaper later, and the algorithm will never revisit it. Reach for Bellman-Ford instead.
- Forgetting the stale check. Without
if (d > dist[u]) continue, you re-expand outdated entries; it is usually still correct but can blow up the running time. - Marking visited on push instead of pop. A node's distance is final only when it is popped, not when it is first discovered.
- Updating in place in hop-limited Bellman-Ford. Writing into the same array lets one round use several edges, silently breaking the "at most K stops" rule. Clone first.
- Integer overflow.
INF + wwraps negative; guard with anINFcheck or uselong. - Using plain Dijkstra with a stop limit. Discarding a path because it is more expensive can throw away the one that had stops left; track stops in the state or use Bellman-Ford.
All practice problems
5 problems on this site use this pattern, easiest first. Solve them in order and say the instinct out loud before you code.