Algorithm

Dijkstra's Algorithm

Graphs Pattern

Dijkstra's Algorithm

Given a weighted graph and a source vertex, find the minimum total weight of a path from the source to every other vertex.

Every edge weight must be non-negative. Under that condition the algorithm settles vertices one at a time in increasing order of distance, and each vertex's distance is final the moment it is settled.

Vertices with no path from the source keep a distance of infinity, which callers usually translate into -1 or a similar sentinel.

CONSTRAINTS
  • V = number of vertices, E = number of edges
  • All edge weights must be non-negative — the correctness argument fails otherwise
  • The graph may be directed or undirected
  • Unreachable vertices retain a distance of infinity
  • O((V + E) log V) time with a binary heap, O(V + E) space
EXAMPLE 1
Input: Directed edges (u, v, w): (0,1,4), (0,2,1), (2,1,2), (1,3,3), (2,3,8). Source 0.
Output: [0, 3, 1, 6]
Vertex 1 is reachable directly at cost 4, but going through vertex 2 costs 1 + 2 = 3, which is cheaper. Vertex 3 is then reached via 1 at 3 + 3 = 6, beating the direct route from 2 at 1 + 8 = 9. The cheapest path is not the one with fewest edges.
EXAMPLE 2
Input: Directed edges (0,1,1), (1,2,1), (0,2,10). Source 0.
Output: [0, 1, 2]
Reaching vertex 2 through vertex 1 takes two edges costing 2 in total, while the single direct edge costs 10. Counting edges would give the wrong answer here — this is exactly why breadth-first search cannot be used on a weighted graph.
EXAMPLE 3
Input: Directed edges (0,1,5). Source 0, three vertices total.
Output: [0, 5, infinity]
Vertex 2 has no incoming edge from anywhere reachable, so no path exists and its distance stays infinite. Problems typically ask for -1 in this situation, so the translation must be explicit.
EXAMPLE 4
Input: Directed edges (0,1,0), (1,2,0). Source 0.
Output: [0, 0, 0]
Zero-weight edges are permitted, since the requirement is non-negative rather than strictly positive. All three vertices sit at distance 0 from the source, and the algorithm handles this without any special case.
Are all edge weights guaranteed non-negative?
They must be, and it is the first thing to confirm. A single negative edge can make the algorithm return a wrong answer silently — no crash, no warning. If negatives are possible, use Bellman-Ford instead.
What should I return for a vertex with no path from the source?
Its distance stays at infinity internally. Whether the caller wants -1, null, or infinity is a convention to agree on, and forgetting to convert is a common source of wrong output.
Can there be several edges between the same pair of vertices?
Yes, and the algorithm handles it with no special case — relaxation keeps whichever is best. There is no need to deduplicate the input.
Do I need the actual path, or only the distances?
This version returns distances. Recovering the path means storing, for each vertex, which predecessor produced its best distance, then walking those predecessors back from the target.

Breadth-first search finds shortest paths when every edge costs the same. This problem is what happens when that assumption is removed, and understanding precisely which part of the BFS argument breaks is what makes the replacement make sense.

Why counting edges stops working

BFS is correct because of a ring argument: everything one edge from the source is discovered first, then everything two edges away, and so on. A vertex first appears in the ring matching its true distance. That argument silently assumes one edge equals one unit of cost.

Now give edges weights. Consider three vertices with a direct edge from 0 to 2 of weight 10, and a two-edge route 0 to 1 to 2 costing 1 each.

BFS reaches vertex 2 in one hop and reports a distance of one edge. But the cheapest route costs 2, using two edges. Fewest edges and cheapest total are simply different questions, and BFS answers the wrong one.

So the frontier can no longer be ordered by hop count. It must be ordered by accumulated cost.

The greedy claim, and its proof

Here is the idea the whole algorithm rests on.

Keep a tentative best-known distance for every vertex, starting at 0 for the source and infinity elsewhere. Now repeatedly take the unsettled vertex with the smallest tentative distance and declare that distance final.

That declaration needs justifying, and the argument is short enough to give in an interview.

Let u be the unsettled vertex with the smallest tentative distance d. Suppose some cheaper path to u exists. That path starts at the source, which is settled, and ends at u, which is not, so somewhere along it there is a first unsettled vertex — call it x, possibly u itself. The portion of the path from the source to x consists entirely of settled vertices, so x's tentative distance is already at most the cost of that portion. And since u was chosen as the smallest tentative distance among unsettled vertices, x's tentative distance is at least d.

So the path costs at least d up to x, plus the weight of the remaining segment from x to u. Because every weight is non-negative, that remaining segment costs at least 0, so the whole path costs at least d. No cheaper path exists, and d is final.

Notice exactly where non-negativity entered: it is what guarantees the rest of the path cannot reduce the total. With a negative edge available, a longer route could dip below d after passing through x, and the entire argument collapses. This is not a technicality — it is the whole reason the algorithm is restricted to non-negative weights.

Relaxation

The updating step has a standard name. Relaxing an edge from u to v with weight w means asking whether routing through u improves the known distance to v:

if dist[u] + w < dist[v], then dist[v] becomes dist[u] + w.

Each time a vertex is settled, every edge leaving it is relaxed. That is the only way distances ever improve.

To always know which unsettled vertex is nearest, keep candidates in a min-heap — a structure whose smallest element is always available at the top, with insertion and removal costing O(log n).

python
import heapq

def dijkstra(adj, source, V):
    dist = [float('inf')] * V
    dist[source] = 0
    heap = [(0, source)]                      # (distance, vertex)

    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue                          # stale entry: a better route was found later
        for v, w in adj[u]:
            nd = d + w
            if nd < dist[v]:                  # relax
                dist[v] = nd
                heapq.heappush(heap, (nd, v))
    return dist
Crucial Notethe distance comes first in each tuple. Heaps compare tuples left to right, so ordering by (vertex, distance) would sort by label and produce nonsense. This is an easy typo with no error message.
Crucial Notethe if d > dist[u]: continue line is essential, and it exists because this implementation never removes outdated entries from the heap. When a vertex's distance improves, a new entry is pushed while the old, larger one remains. That stale entry will eventually be popped, and processing it would relax edges using an inflated distance. Comparing against the current best and skipping is called lazy deletion, and it is far simpler than trying to update a heap entry in place. Omit it and the algorithm still terminates with correct distances in most cases, but it wastes work and breaks any logic that assumes each vertex is settled once.

Cost: each edge can push at most one heap entry, so the heap holds up to E items and each push and pop costs log E, which is O(log V) since E is at most V². Total O((V + E) log V) time and O(V + E) space.

Worth knowing: without a heap, scanning all V vertices each round to find the nearest gives O(V²), which is actually faster on a dense graph where E approaches V². The heap version wins on sparse graphs, which is the common case.

Worked example

Directed edges (0,1,4), (0,2,1), (2,1,2), (1,3,3), (2,3,8), source 0.

Distances start at [0, ∞, ∞, ∞] and the heap holds (0, 0).

- Pop (0, 0). Not stale. Relax 0→1 with weight 4: ∞ improves to 4, push (4, 1). Relax 0→2 with weight 1: ∞ improves to 1, push (1, 2). Distances [0, 4, 1, ∞]. Heap: (1,2), (4,1).
- Pop (1, 2). The heap correctly hands back vertex 2 before vertex 1, since 1 < 4. Relax 2→1 with weight 2: 1 + 2 = 3, which beats the current 4, so vertex 1 improves to 3 and (3, 1) is pushed. The old (4, 1) stays in the heap as garbage. Relax 2→3 with weight 8: ∞ improves to 9, push (9, 3). Distances [0, 3, 1, 9]. Heap: (3,1), (4,1), (9,3).
- Pop (3, 1). Fresh, since dist[1] is 3. Relax 1→3 with weight 3: 3 + 3 = 6, beating 9, so vertex 3 improves to 6 and (6, 3) is pushed. Distances [0, 3, 1, 6].
- Pop (4, 1). Here 4 > dist[1], which is 3 — stale, so skip it. This is the lazy-deletion guard doing its job. Processing it would have relaxed 1→3 using a distance of 4, giving 7, which is worse and would not have been recorded, but on other graphs a stale entry can genuinely corrupt results.
- Pop (6, 3). Fresh. Vertex 3 has no outgoing edges.
- Pop (9, 3). Stale, 9 > 6. Skip. Heap empties.

Final: [0, 3, 1, 6]. Vertex 1 was reached more cheaply by a two-edge route than by its direct edge, and vertex 3 likewise.

Recognising it, and knowing when it fails

Signals: shortest, cheapest, fastest, minimum cost or time over a graph whose edges carry non-negative weights. Road networks, network latency, flight prices, grids where cells cost different amounts to enter.

Three boundaries are worth being able to state.

Equal weights. If every edge costs the same, plain BFS gives the same answer with no heap and no log factor. Reaching for Dijkstra there is overkill, though not wrong. A special case worth knowing: when weights are only 0 and 1, a deque — pushing zero-weight neighbours to the front and one-weight to the back — gives O(V + E). That is called 0-1 BFS.

Negative weights. The greedy argument fails, as shown above, and the algorithm returns wrong answers without complaining. Use Bellman-Ford, which relaxes every edge V-1 times and also detects negative cycles.

All pairs. Running Dijkstra from every vertex costs O(V × (V + E) log V). On a dense graph, Floyd-Warshall's O(V³) is simpler and often faster.

One more upgrade worth naming: when there is a single known destination rather than all of them, A\* adds an optimistic estimate of the remaining distance to each heap key. As long as that estimate never overstates the true remainder, the answer stays optimal while the search is pulled toward the goal and touches far fewer vertices.

Train the reflex: the moment edges carry unequal non-negative costs and the question asks for a minimum total, stop counting hops. Order the frontier by accumulated cost, settle the nearest vertex, and relax its edges.

Interactive Strategy Visualization

Dijkstra's Shortest Path

Greedy Relaxation
Optimal Distance
41238
A
D: 0
B
D: ∞
C
D: ∞
D
D: ∞
NodeShortest Distance
A0
B
C
D

1. The Initial State

We start at node A. We know the distance to A is 0, but all other nodes are 'infinitely' far away because we haven't explored them yet.

Exponential Enumerate Every Path
O(V²) Scan For The Nearest Unsettled Vertex
O((V + E) log V) Binary Heap Frontier