Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Dijkstra’s Shortest Paths
The algorithm every routing table, map app and network protocol leans on: single-source shortest paths on a graph with non-negative weights. Greedily settle the closest unsettled node, relax its edges, repeat. Return the distance to every node. Solve it in Python or TypeScript, with hidden tests.
The problem
You are given n nodes (0…n-1), a list of directed weighted edges [u, v, w] with w ≥ 0, and a source src. Return a list dist where dist[i] is the length of the shortest path from src to node i, or -1 if i is unreachable.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement dijkstra(n, edges, src): keep tentative distances (∞ except the source at 0). Repeatedly pick the unsettled node with the smallest distance, settle it, and relax each outgoing edge (dist[v] = min(dist[v], dist[u] + w)). Return distances, using -1 for anything still ∞.
Examples and constraints 4 examples
n = 5, edges = [[0,1,4],[0,2,1],[2,1,2],[1,3,1],[2,3,5]], src = 0[0, 3, 1, 4, -1]n = 4, edges = [[0,1,10],[0,2,1],[2,1,1],[1,3,1]], src = 0[0, 2, 1, 3]n = 3, edges = [[0,1,1],[1,2,1]], src = 2[-1, -1, 0]n = 1, edges = [], src = 0[0]- All edge weights are non-negative — this is what lets the greedy "settle the closest node" step be correct.
- Edges are directed; there may be several edges into a node.
- Unreachable nodes return
-1. The source’s own distance is 0.
Hints 4 ideas
- Build an adjacency list from the edges. Start with
dist[src] = 0and all others "infinity". - On each of the n rounds, choose the unsettled node with the minimum tentative distance (a priority queue makes this fast; a linear scan is fine for small graphs).
- Settling a node with non-negative weights means its distance is final — relax its outgoing edges to possibly improve neighbours.
- If the smallest remaining distance is infinity, the rest are unreachable — stop and mark them
-1.
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious correct approach relaxes every edge, over and over, until no distance changes (Bellman-Ford). It works because a shortest path uses at most V − 1 edges, so V − 1 full passes are enough to carry every distance along it. It costs O(V·E) time and O(V) space. It is too slow because it ignores the one thing this problem promises, that weights are non-negative, and keeps re-relaxing edges out of nodes whose distance is already final. The faster shortcut, a plain BFS that fixes each node the first time it is reached, is wrong: on the first example it gives node 1 distance 4 from the direct edge instead of 3 through node 2.
The approach
With non-negative weights, the unsettled node with the smallest tentative distance can be finalized: any other route to it must leave the settled set through some unsettled node that is already at least as far away, and the remaining edges cannot shorten it. Dijkstra repeats that one step.
The reference keeps an adjacency list adj, tentative distances dist (INF except dist[src] = 0) and a settled flag per node. Each of the n rounds scans for the unsettled u with the smallest dist, marks it settled, and relaxes every (v, w) in adj[u]: if dist[u] + w < dist[v], lower dist[v]. If no unsettled node has a finite distance (u == -1), the rest are unreachable and the loop breaks. Finally every INF becomes -1. The linear scan makes this the array version, the right choice for dense graphs; a binary heap replaces the scan on sparse ones.
Walk through an example
On the first example, settling node 2 at distance 1 is what lowers node 1 from 4 to 3, and node 4 is never reached.
| step | settle (dist) | relaxations | dist after |
|---|---|---|---|
| 1 | 0 (0) | 1: ∞ → 4, 2: ∞ → 1 | [0, 4, 1, ∞, ∞] |
| 2 | 2 (1) | 1: 4 → 3, 3: ∞ → 6 | [0, 3, 1, 6, ∞] |
| 3 | 1 (3) | 3: 6 → 4 | [0, 3, 1, 4, ∞] |
| 4 | 3 (4) | no outgoing edges | [0, 3, 1, 4, ∞] |
| 5 | none: u == -1 | break, node 4 is unreachable | returns [0, 3, 1, 4, -1] |
Complexity
The reference’s array version is O(V² + E) time: V rounds that each scan V entries, plus each edge relaxed once. A binary heap with lazy deletion gives O((V + E) log V), better when E is far below V². Both use O(V + E) space for adj and the distances. Bellman-Ford is O(V·E) time and O(V) space.
Edge cases to test
- The source is a sink:
dijkstra(3, [[0,1,1],[1,2,1]], 2)returns[-1, -1, 0]. Edges are directed, so nothing flows back from node 2. - One node, no edges:
dijkstra(1, [], 0)returns[0]. - Zero-weight edges are allowed:
dijkstra(3, [[0,1,0],[1,2,0]], 0)returns[0, 0, 0]. The algorithm needs w ≥ 0, not w > 0. - Parallel edges and self-loops:
dijkstra(2, [[0,1,5],[0,1,2]], 0)returns[0, 2], and a self-loop such as[0,0,3]never lowers anything. Keep every edge inadj; relaxation picks the cheaper one. - One negative edge breaks it: with
[[0,1,1],[0,2,5],[2,1,-10],[1,3,1]]from node 0, the reference settles node 3 at 2, later lowers node 1 to −5 without re-relaxing its edges, and returns[0, -5, 5, 2]. Bellman-Ford gives[0, -5, 5, -4].
Common mistakes
- Running it on graphs with negative edges — that breaks “settled = final”; use Bellman-Ford instead.
- In the heap version, not skipping stale entries: a node can be pushed several times, and only the copy matching its current distance should be expanded.
- Re-processing settled nodes, or forgetting to mark a node settled when popped.
- Adding both directions for every edge. The edges here are directed; treating them as undirected gives node 2 in the source-is-a-sink case a route back to 0 and 1.
Follow-ups an interviewer asks next
- How do you return the shortest path itself?
- Keep a
parentarray. Whenever relaxation lowersdist[v]throughu, setparent[v] = u. To rebuild the route to a target, follow parents back to the source and reverse the list. In the first example the route to node 3 is 0 → 2 → 1 → 3, with length 1 + 2 + 1 = 4. - Only one target matters. Can you stop early?
- Yes. Return as soon as the target is settled, because a settled distance is final. With a good lower-bound estimate of the remaining distance, A* goes further by ordering the frontier by distance so far plus that estimate. Bidirectional Dijkstra searches from both ends and can stop once the two frontiers’ smallest distances add up to at least the best path found.
- What if some weights are negative?
- Then a node settled early can later be reached more cheaply, so the invariant fails. Bellman-Ford relaxes every edge V − 1 times in O(V·E) and detects a negative cycle with one extra pass. For all-pairs distances on such a graph, Johnson’s algorithm runs Bellman-Ford once to reweight the edges to non-negative values, then Dijkstra from every source.
- All weights are small integers. Anything faster?
- If every weight is 0 or 1, 0-1 BFS with a deque runs in O(V + E): push a neighbor reached by a 0-weight edge to the front and one reached by a 1-weight edge to the back. For integer weights up to C, Dial’s algorithm keeps one bucket per distance value and runs in O(V·C + E).
Where this shows up
Shortest path is everywhere: map and road routing, link-state network routing (OSPF), game pathfinding (A* is Dijkstra plus a heuristic), and least-cost dependency analysis. The priority-queue-driven “expand the frontier by cheapest next” pattern generalizes across search.
Reference solution
Reference solution — Python
def dijkstra(n, edges, src):
INF = float("inf")
adj = [[] for _ in range(n)]
for u, v, w in edges:
adj[u].append((v, w))
dist = [INF] * n
dist[src] = 0
settled = [False] * n
for _ in range(n):
u, best = -1, INF # pick closest unsettled node
for i in range(n):
if not settled[i] and dist[i] < best:
best, u = dist[i], i
if u == -1:
break # remaining nodes unreachable
settled[u] = True
for v, w in adj[u]: # relax outgoing edges
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
return [d if d < INF else -1 for d in dist]
print(dijkstra(5, [[0,1,4],[0,2,1],[2,1,2],[1,3,1],[2,3,5]], 0)) # -> [0, 3, 1, 4, -1]Reference solution — TypeScript
function dijkstra(n: number, edges: number[][], src: number): number[] {
const INF = Infinity;
const adj: [number, number][][] = Array.from({ length: n }, () => []);
for (const [u, v, w] of edges) adj[u].push([v, w]);
const dist = new Array(n).fill(INF);
dist[src] = 0;
const settled = new Array(n).fill(false);
for (let round = 0; round < n; round++) {
let u = -1, best = INF; // closest unsettled node
for (let i = 0; i < n; i++) {
if (!settled[i] && dist[i] < best) { best = dist[i]; u = i; }
}
if (u === -1) break; // rest unreachable
settled[u] = true;
for (const [v, w] of adj[u]) { // relax edges
if (dist[u] + w < dist[v]) dist[v] = dist[u] + w;
}
}
return dist.map((d) => (d < INF ? d : -1));
}
console.log(dijkstra(5, [[0,1,4],[0,2,1],[2,1,2],[1,3,1],[2,3,5]], 0)); // -> [0,3,1,4,-1]Questions about Dijkstra’s Shortest Paths
Why does Dijkstra’s algorithm fail with negative edge weights?
Dijkstra finalizes a node as soon as it is the closest unsettled node, assuming any other route must pass through a node that is already farther away and can only get longer from there. A negative edge breaks that assumption, because a longer partial route can become shorter later. The reference shows it: with edges 0 to 1 of weight 1, 0 to 2 of weight 5, 2 to 1 of weight minus 10, and 1 to 3 of weight 1, it settles node 3 at distance 2, but the true distance is minus 4. Use Bellman-Ford when weights can be negative.
What is the time complexity of Dijkstra’s algorithm?
It depends on how you find the closest unsettled node. Scanning an array, as the reference does, costs O(V) per round over V rounds, so the total is O(V squared plus E). A binary heap makes each extraction and each distance update O(log V), for O((V plus E) log V) in total, which is better on sparse graphs. A Fibonacci heap improves the bound to O(E plus V log V), although binary heaps usually run faster in practice. Space is O(V plus E) for the adjacency list and the distance array.
What is the difference between Dijkstra’s algorithm and BFS?
BFS finds shortest paths when every edge has the same weight, because it reaches nodes in order of hop count. Dijkstra generalizes it to non-negative weights by always expanding the node with the smallest total distance instead of the fewest hops. On a weighted graph BFS gives wrong answers: on the first example it would fix node 1 at distance 4 from the direct edge, while the true distance is 3 through node 2. When every weight is 1, the two return the same distances, and BFS is faster because it needs no priority queue.
Why does the reference use a linear scan instead of a heap?
The scan is short and easy to check, and it is the better choice on dense graphs, where E is close to V squared: O(V squared) of scanning beats the O(E log V) a heap pays for its updates. On a sparse graph such as a road network, where each node has only a few edges, the heap version wins by a wide margin. Both versions return the same distances, so the hidden tests pass with either one, and the heap version is what most interviewers expect you to write.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Two SumThe classic warm-up: find the two numbers that add up to a target. Brute force is O(n²) — a hash map gets you to one pass, O(n). Solve it in Python or TypeScript, right in your browser, with hidden tests and a reveal-solution button.Read →
- Valid ParenthesesThe canonical stack problem: decide whether every bracket is closed by the right type, in the right order. A stack turns nested matching into a single pass. Solve it in Python or TypeScript with hidden tests.Read →
- Fizz BuzzThe famous screening question. Print 1…n, but multiples of 3 become "Fizz", of 5 become "Buzz", and of both become "FizzBuzz". Easy — the catch is testing divisibility in the right order. Solve it in Python or TypeScript.Read →
- Number of IslandsThe canonical connected-components question. Flood-fill each unvisited patch of land with BFS or DFS, count how many floods you started — grid traversal, visited bookkeeping, and the classic diagonal trap. Solve it in Python or TypeScript.Read →
- Merge IntervalsCalendar apps and memory allocators run on this: sort by start, sweep once, grow or close the current block. Touching endpoints merge, nested intervals vanish — the classic that punishes off-by-one thinking. Solve it in Python or TypeScript.Read →
- Top-K Frequent ElementsCount, then rank — the two-step pattern behind trending topics and hot-key detection. A hash map counts; a (-count, value) sort ranks with a clean tie-break; buckets get you to O(n) if you want the flex. Solve it in Python or TypeScript.Read →