Trees & Graphs
Advanced 12 min read

Shortest paths (Dijkstra)

In one line

Dijkstra's algorithm finds the cheapest path from one source to every node when edge weights are non-negative, by always settling the closest unsettled node next using a min-heap.

What it is

In a weighted graph, the shortest path is the one with the smallest total weight, not the fewest edges. Dijkstra's algorithm solves the single-source version: given a start node, find the cheapest distance to every other node.

It keeps a tentative distance for every node (infinity at first, 0 for the source) and a min-heap of (distance, node). It repeatedly takes the node with the smallest tentative distance. Because all weights are non-negative, no later path can make that node cheaper, so its distance is final. It then relaxes the node's edges: if going through it makes a neighbour cheaper, update the neighbour and push it.

With a binary heap it runs in O((V + E) log V). It requires non-negative weights. With negative weights use Bellman-Ford, O(V x E); for all pairs on small graphs use Floyd-Warshall, O(V^3); for unweighted graphs plain BFS is enough.

Why interviewers ask

Network delay time, cheapest flights, path with minimum effort and many routing questions are Dijkstra or a variant. Interviewers test whether you know when BFS stops working, why negative edges break Dijkstra, how to handle stale heap entries, and how to adapt the state (for example adding a stop count).

How it works

  1. 1Set dist[source] = 0 and every other distance to infinity. Push (0, source) onto a min-heap.
  2. 2Pop the entry with the smallest distance d for node u. If d is larger than dist[u], it is stale (a cheaper path was already found): skip it.
  3. 3Otherwise u is settled. For each edge u to v with weight w, compute d + w. If it is smaller than dist[v], set dist[v] = d + w, record u as v's parent, and push (d + w, v).
  4. 4Repeat until the heap is empty, or stop early when the target is popped if you only need one destination.
  5. 5Rebuild the path by following parent pointers back from the target.
  6. 6The algorithm is correct because, with non-negative weights, the smallest tentative distance cannot be improved by going through any node that is farther away.

Worked example · Four nodes, one detour that is cheaper

  1. Directed edges: A to B (4), A to C (1), C to B (2), B to D (1), C to D (5). Source A.
  2. Start: dist A = 0, others infinity. Heap [(0, A)].
  3. Pop (0, A). Relax: C = 1, B = 4. Heap [(1, C), (4, B)].
  4. Pop (1, C). C to B gives 1 + 2 = 3, which beats 4, so B = 3. C to D gives 1 + 5 = 6, so D = 6. Heap [(3, B), (4, B), (6, D)].
  5. Pop (3, B). B to D gives 3 + 1 = 4, beating 6, so D = 4. Heap [(4, B), (4, D), (6, D)].
  6. Pop (4, B): 4 > dist B = 3, stale, skip. Pop (4, D): settled at 4. Pop (6, D): stale, skip.
  7. Final: A 0, C 1, B 3, D 4. The path to D is A, C, B, D (1 + 2 + 1), cheaper than the direct-looking A, B, D (4 + 1 = 5).

Picture it

Imagine pouring water into a pipe network at the source. The water reaches junctions in order of how far they are along the pipes. Dijkstra simulates that: whichever junction the water would reach next is settled next.

Reference code

import heapq

def dijkstra(n, adj, src):
    # adj[u] = list of (v, weight), every weight >= 0
    dist = [float('inf')] * n
    dist[src] = 0
    heap = [(0, src)]
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue                  # stale entry: u already settled cheaper
        for v, w in adj[u]:
            nd = d + w
            if nd < dist[v]:          # relax the edge
                dist[v] = nd
                heapq.heappush(heap, (nd, v))
    return dist                       # inf means unreachable

Python's heapq has no decrease-key, so the code pushes a new entry on every improvement and skips stale ones when popped (lazy deletion). The heap holds at most O(E) entries, giving O((V + E) log V) time and O(V + E) space.

Key patterns

Classic single-source

When: Network delay time, cheapest cost from a source

How: Standard Dijkstra; the answer is max(dist) or dist[target], and infinity means unreachable.

Minimax path (bottleneck)

When: Path with minimum effort, swim in rising water

How: Replace d + w with max(d, w); the greedy order still works because the cost never decreases along a path.

State-expanded Dijkstra

When: Cheapest flights within K stops, paths with fuel or keys

How: Make the node a tuple like (city, stops used) so the extra constraint is part of the state. Bellman-Ford limited to K + 1 rounds also works for the K-stops problem.

0-1 BFS

When: All weights are 0 or 1

How: Deque instead of heap: 0-weight edges go to the front, 1-weight to the back. O(V + E).

Know the alternatives

When: Negative edges or all-pairs

How: Bellman-Ford O(V x E) handles negative weights and detects negative cycles; Floyd-Warshall O(V^3) gives all pairs for small V.

Real-world use

Internet routing (OSPF and IS-IS)

Link-state routing protocols like OSPF and IS-IS give every router a map of the network with link costs; each router runs Dijkstra's shortest path first algorithm to build its routing table.

Maps and navigation

Google Maps, Apple Maps and OpenStreetMap routers such as OSRM build on Dijkstra with speed-ups like A* and contraction hierarchies, because plain Dijkstra over a continent-sized road graph would be too slow per query.

Ride-hailing and delivery ETAs

Uber and delivery platforms compute travel times over road graphs weighted by live traffic to estimate arrival times and to match drivers to riders.

Games

Strategy and role-playing games compute movement costs over terrain (road 1, forest 3, swamp 5) with Dijkstra or A* so units take the cheapest route rather than the fewest tiles.

In a project

A logistics startup routes vans between 5,000 warehouses and drop points connected by 40,000 road segments, with travel time in minutes per segment, and wants the fastest route and an ETA for every order.

  1. Store the road network as a weighted adjacency list, using travel minutes from the traffic feed as weights (always non-negative).
  2. Run Dijkstra from the depot once each morning to get distances to all drop points, reusing one run for every order from that depot.
  3. Keep parent pointers to rebuild each route for the driver app.
  4. For ad hoc point-to-point queries, stop as soon as the target is popped, and add an A* heuristic (straight-line distance divided by maximum speed) to explore fewer nodes.
  5. Re-run for affected depots when the traffic feed changes a segment's time by more than a threshold.

Impact: A single O((V + E) log V) run, about 45,000 x 13 operations, finishes in milliseconds and gives exact fastest routes for every order, replacing fixed manual routes and making ETAs more accurate.

Trade-offs

Use it when

  • Weights are non-negative and differ (time, distance, cost)
  • You need distances from one source to many or all nodes
  • You can model extra constraints as part of the node state

Avoid it when

  • Any edge weight is negative: results can be wrong; use Bellman-Ford
  • All edges have equal weight: BFS is simpler and O(V + E)
  • You need all pairs on a small dense graph: Floyd-Warshall is simpler
  • Huge road networks with many queries: plain Dijkstra is too slow; use A*, bidirectional search or contraction hierarchies

Cost: O((V + E) log V) with a binary heap and O(V + E) space. Bellman-Ford O(V x E). Floyd-Warshall O(V^3) time, O(V^2) space.

Common mistakes

  • Using Dijkstra with negative edge weights; a settled node could later be improved, so the result is wrong.
  • Not skipping stale heap entries, which redoes work and can hurt performance badly.
  • Marking a node visited when it is pushed (as in BFS) instead of when it is popped; a cheaper path may still arrive.
  • Using BFS on a weighted graph and returning the fewest edges instead of the cheapest cost.
  • For cheapest flights within K stops, keeping only the best cost per city, which discards paths with fewer stops that are needed later.

Interview questions

Quick revision

  • Non-negative weights only.
  • Min-heap of (dist, node); pop smallest, skip if stale, relax edges.
  • Settle on pop, not on push.
  • O((V + E) log V) time with a binary heap.
  • Equal weights: BFS. 0/1 weights: 0-1 BFS with a deque.
  • Negative weights: Bellman-Ford O(V x E). All pairs small graph: Floyd-Warshall O(V^3).
  • Parent pointers rebuild the path.
  • Constraints like stops or fuel go into the state.

Check yourself

1. Edges: S to A (1), S to B (4), A to B (2). What is the shortest distance from S to B?

2. Why does the code skip a popped entry when d > dist[u]?

3. Which situation makes Dijkstra's algorithm give wrong answers?

4. What is the time complexity of Dijkstra with a binary heap?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free