leetcode / Advanced graphs / Dijkstra's algorithm core

Dijkstra's algorithm

Dijkstra's algorithm finds the cheapest route from one starting node to every other node when no edge has a negative cost. It keeps a heap of the best known distance to each frontier node and always finalizes the cheapest one. The same loop, with a different meaning for "cost", solves minimum-effort paths and rising-water grids.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The loop
  5. Stale entries
  6. Invariant and proof
  7. Early exit and variants
  8. Worked example
  9. Implementation
  10. Variations and edge cases
  11. Complexity and tradeoffs
  12. Problems
  13. Medium
  14. Hard
  15. Also uses this pattern
  16. Connections
  17. Read first
  18. Read next
  19. Check yourself
  20. References

Problem

You have a graph whose edges carry costs (travel time, price, effort) and a start node. The question is the cheapest way to reach one node or all of them. Breadth-first search finds fewest edges, not least cost, and trying every path is exponential. The costs are never negative, which is the property that makes a greedy choice safe.

SignalPatternWhy
"Cheapest", "fastest" or "shortest" over weighted edges that are never negativeDijkstra with a min-heapThe cheapest unfinalized node can never be improved later.
"Time for a signal to reach all nodes"Dijkstra, answer is the largest distanceEvery node needs a final distance; the slowest one decides.
Grid where a route's cost is its worst step or highest cellDijkstra with a max-so-far costReplace "sum of weights" by "max along the path"; the greedy argument still holds.
All edges cost 1 (or 0 and 1)Plain BFS (or 0-1 BFS)No heap is needed when costs are uniform.
Negative edge, or at most k edgesBellman-FordDijkstra's greedy finalization breaks with negative weights.

Intuition

Imagine lighting a fuse at the start node. The fire runs along every edge at a speed set by its cost, and the order in which nodes catch fire is the order of their shortest distance. Dijkstra simulates exactly that. At any moment the node that will catch fire next is the cheapest one among those reachable from nodes already burning, and a heap hands you that node in O(log n).

Why can the first pop be trusted? Suppose node u is the cheapest frontier node with distance d. Any other route to u would have to leave the burning region through some other frontier node whose distance is already at least d, and then pay a cost that is not negative, so it arrives at u no earlier than d. That argument is the whole proof, and it is exactly where a negative edge breaks it.

A weighted directed graph with its cheapest route highlighted Six nodes and the edges 0 to 1 costing 4, 0 to 2 costing 1, 2 to 1 costing 2, 1 to 3 costing 1, 2 to 3 costing 5, 3 to 4 costing 3, 2 to 5 costing 8, 4 to 5 costing 1. Starting from node 0 the final distances are node 0 at 0, node 1 at 3, node 2 at 1, node 3 at 4, node 4 at 7, node 5 at 8. The cheapest route to node 5 is 0, then 2, then 1, then 3, then 4, then 5. 4 1 2 1 5 3 8 1 0 d=0 1 d=3 2 d=1 3 d=4 4 d=7 5 d=8

Edge labels are costs and every edge is directed from the first node listed to the second (away from node 0, except 2 → 1); thick edges form the cheapest route to node 5. The diagram scrolls sideways on a narrow screen.

Mechanics

The loop

Keep dist, the final distance of each settled node, and a heap of (distance, node) pairs. Push the start with distance 0. Repeatedly pop the smallest pair. If its node is already settled, skip it. Otherwise record its distance, and for each outgoing edge push (distance + cost, neighbor) when that beats the best known distance to the neighbor.

Stale entries

Python's heap has no decrease-key. Instead, whenever a better route is found, push another entry and leave the old one in the heap. When the old entry is eventually popped the node is already settled, and the pop is skipped. This costs at most one push per edge, so the heap holds at most E entries and each push or pop is O(log E) = O(log V).

Invariant and proof

Every time a node is popped and not skipped, its distance is final, and the settled distances come out in non-decreasing order. Induction: assume it holds for all settled nodes so far. The popped pair is the smallest in the heap, and any unsettled node's true shortest route must cross from the settled set to the unsettled set by one edge, so its true distance is at least the smallest heap entry. Non-negative edges never reduce a distance, which is the step a negative edge violates.

Early exit and variants

If you only need one target, stop when it is first popped; its distance is final then. The cost need not be a sum: any combination cost(path + edge) that never decreases as the path grows works, such as the maximum step on the path, or the maximum cell value seen.

The order Dijkstra settles the nodes and their distances Nodes are settled in the order node 0 at distance 0, node 2 at distance 1, node 1 at distance 3, node 3 at distance 4, node 4 at distance 7, node 5 at distance 8. The distances never decrease. The run pushed 9 entries onto the heap and popped 9, of which 3 were stale and skipped. settle order (bar length = distance) node 0 0 node 2 1 node 1 3 node 3 4 node 4 7 node 5 8

The bars only ever grow: that monotone order is the invariant in action.

Worked example

On the graph above, starting from node 0, the nodes settle in this order with these distances: node 0 at 0; node 2 at 1; node 1 at 3; node 3 at 4; node 4 at 7; node 5 at 8. The final distance array is 0, 3, 1, 4, 7, 8. The cheapest route to node 5 is 0 → 2 → 1 → 3 → 4 → 5, costing 8. Note that the direct edge 0 → 1 costs 4, but going through node 2 costs only 3, so node 1 is pushed twice: the cost-4 entry goes stale.

Heap workCount
Pushes9
Pops9
Stale pops skipped3

Network delay. With travel times 2→1 (1), 2→3 (1), 3→4 (1) and a signal sent from node 2 on 4 nodes, the last node to hear is node 4 after 2. Unreachable nodes make the answer -1: with only the edge 1→2 and 3 nodes, a signal from node 1 never reaches node 3, so the answer is -1.

Minimum effort. On the height grid [[1, 2, 2], [3, 8, 2], [5, 3, 5]] the best route's biggest height jump is 2. Rising water. On [[0, 2], [1, 3]] you can reach the far corner at time 3, because the final cell only opens then.

Why negative edges break it. Take edges 0→1 (1), 0→2 (2) and 2→1 (-5). Dijkstra settles node 1 at distance 1 first, but the true shortest distance is -3, via node 2. The settled value can no longer be improved, so the answer is wrong.

Implementation

import heapq

def dijkstra(graph, source):
    # graph[u] is a list of (v, cost) with cost >= 0
    dist = {}
    heap = [(0, source)]
    while heap:
        d, u = heapq.heappop(heap)
        if u in dist:
            continue
        dist[u] = d
        for v, cost in graph[u]:
            if v not in dist:
                heapq.heappush(heap, (d + cost, v))
    return dist


def network_delay_time(times, n, k):
    graph = {i: [] for i in range(1, n + 1)}
    for u, v, w in times:
        graph[u].append((v, w))
    dist = dijkstra(graph, k)
    return max(dist.values()) if len(dist) == n else -1

Variations and edge cases

def minimum_effort(heights):
    rows, cols = len(heights), len(heights[0])
    best = {(0, 0): 0}
    heap = [(0, 0, 0)]
    while heap:
        effort, r, c = heapq.heappop(heap)
        if (r, c) == (rows - 1, cols - 1):
            return effort
        if effort > 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 < rows and 0 <= nc < cols:
                step = max(effort, abs(heights[nr][nc] - heights[r][c]))
                if step < best.get((nr, nc), float("inf")):
                    best[(nr, nc)] = step
                    heapq.heappush(heap, (step, nr, nc))


def swim_in_water(grid):
    n = len(grid)
    seen = {(0, 0)}
    heap = [(grid[0][0], 0, 0)]
    while heap:
        t, r, c = heapq.heappop(heap)
        if (r, c) == (n - 1, n - 1):
            return t
        for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
            nr, nc = r + dr, c + dc
            if 0 <= nr < n and 0 <= nc < n and (nr, nc) not in seen:
                seen.add((nr, nc))
                heapq.heappush(heap, (max(t, grid[nr][nc]), nr, nc))
  • Skip stale entries. Without the settled check the algorithm still works but may reprocess nodes and can blow up on dense graphs.
  • Heap ties. Tuples compare by distance first, then node. Use integer node ids, or add a counter, if nodes are not comparable.
  • Unreachable nodes. They never appear in dist; check the count before taking a maximum.
  • Directed vs. undirected. For an undirected graph add each edge in both directions.
  • Zero-cost edges are fine. A negative edge is not: use Bellman-Ford.
  • Reconstructing the route. Store the parent whenever you push a better distance, then walk parents back from the target.
  • Edge cases: source equals target (distance 0), a single node, parallel edges (the heap picks the cheaper), and disconnected graphs.

Complexity and tradeoffs

  • Time: O((V + E) log V) with a binary heap, since each edge causes at most one push and each push or pop costs O(log V).
  • Space: O(V + E) for the graph, plus O(E) heap entries in the worst case.
  • Baseline. Trying every simple path is exponential. Bellman-Ford handles any weights in O(V · E), slower but more general. A dense graph can use an array scan instead of a heap for O(V²).
  • Versus BFS. With uniform costs BFS is O(V + E) and simpler; the heap only earns its log factor when costs differ.
  • When it fails. Negative edges (use Bellman-Ford), paths limited to k edges (Bellman-Ford rounds, or a state of node plus stops), and all-pairs distances (run it from each node, or use Floyd-Warshall).

Problems

Network Delay Time is the textbook run: distances from one source, answered by the largest. Swim in Rising Water and Path With Minimum Effort keep the same loop but change the cost into a running maximum. Cheapest Flights Within K Stops, homed on the Bellman-Ford page, adds a stop limit that plain Dijkstra cannot enforce.

Medium

  • 743. Network Delay Time NeetCode 150 NeetCode 250
    Run Dijkstra from the source; the answer is the largest final distance, or -1 if some node is never reached.
  • 1631. Path With Minimum Effort NeetCode 250
    A path's effort is its largest single step, so run Dijkstra where a path's cost is its maximum step instead of a sum.

Hard

  • 778. Swim in Rising Water NeetCode 150 NeetCode 250
    Treat a path's cost as its highest cell, and Dijkstra with that max rule finds the lowest possible maximum.

Also uses this pattern

These problems have a different home page, but this idea is part of their solution.

Connections

Read first

Read next

Dijkstra is BFS with a priority queue instead of a plain queue, and it leans on the heap page for its inner loop. Minimizing a maximum along a path can also be solved by binary searching the answer or by adding edges in cost order with union-find, which is why those pages are linked.

Check yourself

Why is a node's distance final the moment it is popped?

Every other unsettled node has a heap entry at least as large, and any route to the popped node must leave the settled region through such a node and then pay non-negative costs, so it cannot be cheaper. In the example node 1 is settled at 3 and nothing later can undercut it.

What does a stale pop mean, and why is it harmless?

A better route to the same node was pushed after this entry, so the node is already settled when the old entry comes out. The settled check skips it. The example run skipped 3 of its 9 pops.

Which part of the proof fails with a negative edge?

The claim that costs after leaving the settled region cannot reduce the total. With edges 0→1 (1), 0→2 (2), 2→1 (-5), Dijkstra settles node 1 at 1 but the true distance is -3.

How would you change the loop to minimize the biggest step on a path instead of the total?

Push max(current, step) instead of current + step. Since that value never decreases as a path grows, the greedy argument holds. The height grid above gives 2.

References

  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms, section 22.3 (Dijkstra's algorithm, with the correctness proof).
  • Dijkstra, "A note on two problems in connexion with graphs", Numerische Mathematik 1, 1959, the original paper.
  • Sedgewick and Wayne, Algorithms, section 4.4 (shortest paths).
  • Skiena, The Algorithm Design Manual, section on shortest paths.
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.