Dijkstra's Algorithm
The Core Idea
Dijkstra's algorithm finds the shortest path from a single source vertex to all other vertices in a weighted graph with non-negative edge weights. It works by greedily settling vertices — always processing the closest unvisited vertex next.
GREEDY INVARIANT: When vertex u is extracted from the min-heap with distance d, dist[u] = d is the TRUE shortest distance from source to u. u is permanently settled — never updated again. WHY THE INVARIANT HOLDS (non-negative edges only): Any alternative path to u must go through some other unvisited vertex v. All unvisited vertices have dist[v] >= dist[u] (heap extracts minimum). Any path through v has total length >= dist[v] + 0 >= dist[u]. So no alternative path can be shorter. QED. With NEGATIVE edges: dist[v] + negative_edge < dist[u] is possible. Invariant breaks → algorithm produces wrong answers.
Algorithm Steps
INPUT:
Weighted directed graph (non-negative weights).
Source vertex s.
INITIALISE:
dist[s] = 0, dist[all others] = ∞
min-heap = {(0, s)}
parent[] = {-1} for all vertices (for path reconstruction)
LOOP:
While heap is not empty:
(d, u) = extract-min from heap
If d > dist[u]: skip (stale entry — shorter path already found)
For each edge (u, v, weight):
If dist[u] + weight < dist[v]:
dist[v] = dist[u] + weight
parent[v] = u
push (dist[v], v) to heap ← lazy: don't remove old entry
OUTPUT:
dist[] = shortest distances from source to all vertices
parent[] = predecessor on shortest path (for reconstruction)
Step-by-Step Trace
Graph (directed, weighted):
0 ──4── 1
│ │ \
1 2 5
│ │ \
2 ──2── 1 3 ──3── 4
Adjacency list:
0 → [(1,4), (2,1)]
1 → [(3,1)]
2 → [(1,2), (3,5)]
3 → [(4,3)]
4 → []
Dijkstra from source=0:
INIT: dist=[0,∞,∞,∞,∞], heap=[(0,0)], parent=[-1,-1,-1,-1,-1]
━━━ Extract (0, 0): u=0, d=0 ━━━
0 ≤ dist[0]=0 → process (not stale)
Edge 0→1 w=4: 0+4=4 < ∞ → dist[1]=4, parent[1]=0, push(4,1)
Edge 0→2 w=1: 0+1=1 < ∞ → dist[2]=1, parent[2]=0, push(1,2)
dist=[0,4,1,∞,∞] heap=[(1,2),(4,1)]
━━━ Extract (1, 2): u=2, d=1 ━━━
1 ≤ dist[2]=1 → process
Edge 2→1 w=2: 1+2=3 < 4 → dist[1]=3, parent[1]=2, push(3,1) ← UPDATE!
Edge 2→3 w=5: 1+5=6 < ∞ → dist[3]=6, parent[3]=2, push(6,3)
dist=[0,3,1,6,∞] heap=[(3,1),(4,1),(6,3)]
━━━ Extract (3, 1): u=1, d=3 ━━━
3 ≤ dist[1]=3 → process
Edge 1→3 w=1: 3+1=4 < 6 → dist[3]=4, parent[3]=1, push(4,3) ← UPDATE!
dist=[0,3,1,4,∞] heap=[(4,1),(4,3),(6,3)]
━━━ Extract (4, 1): u=1, d=4 ━━━
4 > dist[1]=3 → STALE, skip
━━━ Extract (4, 3): u=3, d=4 ━━━
4 ≤ dist[3]=4 → process
Edge 3→4 w=3: 4+3=7 < ∞ → dist[4]=7, parent[4]=3, push(7,4)
dist=[0,3,1,4,7] heap=[(6,3),(7,4)]
━━━ Extract (6, 3): u=3, d=6 ━━━
6 > dist[3]=4 → STALE, skip
━━━ Extract (7, 4): u=4, d=7 ━━━
7 ≤ dist[4]=7 → process, no outgoing edges
heap=[]
FINAL: dist=[0,3,1,4,7]
Path to 4: backtrack parent[] → 4←3←1←2←0 → reverse → [0,2,1,3,4]
Complete Implementation
1import java.util.*;
2
3public class DijkstrasAlgorithm {
4
5 // ─── Core Dijkstra — O((V+E) log V) ───────────────────────────────
6 public static int[] dijkstra(int V,
7 List<List<int[]>> adj, // [neighbour, weight]
8 int source) {
9 int[] dist = new int[V];
10 Arrays.fill(dist, Integer.MAX_VALUE);
11 dist[source] = 0;
12
13 // Min-heap: [distance, vertex]
14 PriorityQueue<int[]> pq = new PriorityQueue<>(
15 Comparator.comparingInt(a -> a[0])
16 );
17 pq.offer(new int[]{0, source});
18
19 while (!pq.isEmpty()) {
20 int[] top = pq.poll();
21 int d = top[0];
22 int u = top[1];
23
24 if (d > dist[u]) continue; // Stale entry — skip
25
26 for (int[] edge : adj.get(u)) {
27 int v = edge[0], w = edge[1];
28 if (dist[u] + w < dist[v]) {
29 dist[v] = dist[u] + w;
30 pq.offer(new int[]{dist[v], v});
31 }
32 }
33 }
34
35 return dist;
36 }
37
38 // ─── Dijkstra with Path Reconstruction ────────────────────────────
39 public static List<Integer> shortestPath(int V,
40 List<List<int[]>> adj,
41 int source, int target) {
42 int[] dist = new int[V];
43 int[] parent = new int[V];
44 Arrays.fill(dist, Integer.MAX_VALUE);
45 Arrays.fill(parent, -1);
46 dist[source] = 0;
47
48 PriorityQueue<int[]> pq = new PriorityQueue<>(
49 Comparator.comparingInt(a -> a[0])
50 );
51 pq.offer(new int[]{0, source});
52
53 while (!pq.isEmpty()) {
54 int[] top = pq.poll();
55 int d = top[0], u = top[1];
56 if (d > dist[u]) continue;
57
58 for (int[] edge : adj.get(u)) {
59 int v = edge[0], w = edge[1];
60 if (dist[u] + w < dist[v]) {
61 dist[v] = dist[u] + w;
62 parent[v] = u;
63 pq.offer(new int[]{dist[v], v});
64 }
65 }
66 }
67
68 // Reconstruct path: target → source via parent[], then reverse
69 List<Integer> path = new ArrayList<>();
70 if (dist[target] == Integer.MAX_VALUE) return path; // Unreachable
71
72 for (int v = target; v != -1; v = parent[v]) {
73 path.add(0, v); // Prepend to get source→target order
74 }
75 return path;
76 }
77
78 // ─── Dijkstra with Array (dense graphs) O(V²) ─────────────────────
79 public static int[] dijkstraArray(int V, int[][] weightMatrix, int source) {
80 int[] dist = new int[V];
81 boolean[] settled = new boolean[V];
82
83 Arrays.fill(dist, Integer.MAX_VALUE);
84 dist[source] = 0;
85
86 for (int count = 0; count < V; count++) {
87 // Find unvisited vertex with minimum distance — O(V)
88 int u = -1;
89 for (int i = 0; i < V; i++) {
90 if (!settled[i] && (u == -1 || dist[i] < dist[u])) u = i;
91 }
92
93 if (dist[u] == Integer.MAX_VALUE) break; // All remaining unreachable
94 settled[u] = true;
95
96 // Relax all edges from u
97 for (int v = 0; v < V; v++) {
98 if (weightMatrix[u][v] > 0 && dist[u] + weightMatrix[u][v] < dist[v]) {
99 dist[v] = dist[u] + weightMatrix[u][v];
100 }
101 }
102 }
103 return dist;
104 }
105
106 public static void main(String[] args) {
107 int V = 5;
108 List<List<int[]>> adj = new ArrayList<>();
109 for (int i = 0; i < V; i++) adj.add(new ArrayList<>());
110
111 // Directed: 0→1:4, 0→2:1, 2→1:2, 1→3:1, 2→3:5, 3→4:3
112 adj.get(0).add(new int[]{1, 4}); adj.get(0).add(new int[]{2, 1});
113 adj.get(2).add(new int[]{1, 2}); adj.get(1).add(new int[]{3, 1});
114 adj.get(2).add(new int[]{3, 5}); adj.get(3).add(new int[]{4, 3});
115
116 System.out.println("Distances: " + Arrays.toString(dijkstra(V, adj, 0)));
117 // [0, 3, 1, 4, 7]
118
119 System.out.println("Path 0→4: " + shortestPath(V, adj, 0, 4));
120 // [0, 2, 1, 3, 4]
121
122 System.out.println("Unreachable: " + shortestPath(V, adj, 4, 0));
123 // [] (no path from 4 to 0 in directed graph)
124 }
125}Output:
Distances: [0, 3, 1, 4, 7]
Path 0→4: [0, 2, 1, 3, 4]
Unreachable: []
Application 1: Network Delay Time
Problem: Given n nodes and travel times as directed edges [u, v, w], find the minimum time for all nodes to receive a signal sent from node k. Return -1 if not all reachable.
MODEL:
Run Dijkstra from k.
All nodes receive signal at time dist[i].
"All nodes received" = max of all dist[i].
If any dist[i] = ∞: some node unreachable → return -1.
Example: n=4, edges=[[2,1,1],[2,3,1],[3,4,1]], k=2
Dijkstra from 2:
dist[2]=0, dist[1]=1 (2→1:1), dist[3]=1 (2→3:1), dist[4]=2 (2→3→4:1+1=2)
max(dist) = 2 → signal reaches all nodes in time 2.
1import java.util.*;
2
3public class NetworkDelayTime {
4
5 public static int networkDelayTime(int[][] times, int n, int k) {
6 // Build adjacency list (1-indexed → use n+1 size)
7 List<List<int[]>> adj = new ArrayList<>();
8 for (int i = 0; i <= n; i++) adj.add(new ArrayList<>());
9
10 for (int[] t : times) {
11 adj.get(t[0]).add(new int[]{t[1], t[2]}); // u→v:w
12 }
13
14 // Dijkstra from source k
15 int[] dist = new int[n + 1];
16 Arrays.fill(dist, Integer.MAX_VALUE);
17 dist[k] = 0;
18
19 PriorityQueue<int[]> pq = new PriorityQueue<>(
20 Comparator.comparingInt(a -> a[0])
21 );
22 pq.offer(new int[]{0, k});
23
24 while (!pq.isEmpty()) {
25 int[] top = pq.poll();
26 int d = top[0], u = top[1];
27 if (d > dist[u]) continue;
28
29 for (int[] edge : adj.get(u)) {
30 int v = edge[0], w = edge[1];
31 if (dist[u] + w < dist[v]) {
32 dist[v] = dist[u] + w;
33 pq.offer(new int[]{dist[v], v});
34 }
35 }
36 }
37
38 // Find max delay (1-indexed — skip dist[0])
39 int maxDelay = 0;
40 for (int i = 1; i <= n; i++) {
41 if (dist[i] == Integer.MAX_VALUE) return -1; // Unreachable
42 maxDelay = Math.max(maxDelay, dist[i]);
43 }
44 return maxDelay;
45 }
46
47 public static void main(String[] args) {
48 int[][] times1 = {{2,1,1},{2,3,1},{3,4,1}};
49 System.out.println(networkDelayTime(times1, 4, 2)); // 2
50
51 int[][] times2 = {{1,2,1},{2,3,2},{1,3,4}};
52 System.out.println(networkDelayTime(times2, 3, 1)); // 3
53
54 // Unreachable case
55 int[][] times3 = {{1,2,1}};
56 System.out.println(networkDelayTime(times3, 3, 1)); // -1 (node 3 unreachable)
57 }
58}Output:
2
3
-1
Application 2: Cheapest Flights Within K Stops
Problem: Find cheapest flight from src to dst with at most k stops (k+1 flights). Standard Dijkstra needs modification — limit path length.
MODIFICATION:
State: (cost, node, stops_remaining)
Start: (0, src, k) — at source with k stops left
Relax: for each neighbour v with weight w:
if stops > 0: push (cost+w, v, stops-1)
Extract: minimum cost state where we reach dst
DON'T use simple visited[] — same node can be reached with different stops remaining.
Example: n=3, flights=[[0,1,100],[1,2,100],[0,2,500]], src=0, dst=2, k=1
(0, 0, 1) → explore 0
0→1:100 → (100, 1, 0)
0→2:500 → (500, 2, 0) ← direct flight
(100, 1, 0) → explore 1, stops=0 means 0 more stops allowed
1→2:100 → but stops=0 means this would be k+1=2 stops? NO:
stops_remaining means stops (layovers) still usable.
Going from 1→2 with stops=0 means: we've already made 1 stop (at 1),
this flight arrives at dst. k=1 stop means 1 layover → 2 total flights. ✓
(100+100, 2, -1)... actually: push if stops > 0 BEFORE arrival.
Answer: 200 (via 0→1→2)
1import java.util.*;
2
3public class CheapestFlights {
4
5 public static int findCheapestPrice(int n, int[][] flights,
6 int src, int dst, int k) {
7 List<List<int[]>> adj = new ArrayList<>();
8 for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
9 for (int[] f : flights) adj.get(f[0]).add(new int[]{f[1], f[2]});
10
11 // dist[node] = minimum cost to reach node within current stop limit
12 int[] dist = new int[n];
13 Arrays.fill(dist, Integer.MAX_VALUE);
14 dist[src] = 0;
15
16 // Queue: [cost, node, stops_remaining]
17 Queue<int[]> queue = new LinkedList<>();
18 queue.offer(new int[]{0, src, k});
19
20 while (!queue.isEmpty()) {
21 int[] curr = queue.poll();
22 int cost = curr[0];
23 int node = curr[1];
24 int stops = curr[2];
25
26 if (stops < 0) continue; // Used up all stops
27
28 for (int[] edge : adj.get(node)) {
29 int next = edge[0], price = edge[1];
30 int newCost = cost + price;
31
32 if (newCost < dist[next]) {
33 dist[next] = newCost;
34 queue.offer(new int[]{newCost, next, stops - 1});
35 }
36 }
37 }
38
39 return dist[dst] == Integer.MAX_VALUE ? -1 : dist[dst];
40 }
41
42 public static void main(String[] args) {
43 int[][] flights1 = {{0,1,100},{1,2,100},{0,2,500}};
44 System.out.println(findCheapestPrice(3, flights1, 0, 2, 1)); // 200
45
46 int[][] flights2 = {{0,1,100},{1,2,100},{0,2,500}};
47 System.out.println(findCheapestPrice(3, flights2, 0, 2, 0)); // 500 (direct)
48
49 // No valid path
50 int[][] flights3 = {{0,1,100},{1,2,100}};
51 System.out.println(findCheapestPrice(3, flights3, 2, 0, 1)); // -1
52 }
53}Output:
200 (0→1→2 with 1 stop = 2 flights)
500 (direct flight 0→2, 0 stops)
-1 (unreachable)
Dijkstra Variants Summary
STANDARD DIJKSTRA: State: (distance, vertex) Visit: skip if d > dist[u] Update: dist[v] = dist[u] + weight Use: single-source shortest path, positive weights DIJKSTRA + PATH RECONSTRUCTION: Add parent[] array parent[v] = u when dist[v] is updated Backtrack target → source via parent[], reverse DIJKSTRA WITH ARRAY (no heap): O(V²) — better for dense graphs Scan all unvisited vertices to find minimum each time No heap operations needed MODIFIED DIJKSTRA (k stops / k hops limit): State: (cost, vertex, remaining_steps) Don't use settled[] — same vertex can be in multiple states Useful when path length is also constrained BIDIRECTIONAL DIJKSTRA: Run Dijkstra from source AND target simultaneously Stop when searches meet ~2× faster in practice MULTI-SOURCE DIJKSTRA: Push all sources at distance 0 into the heap initially Equivalent to single source with a virtual super-source
Complexity Summary
| Variant | Time | Space | Notes |
|---|---|---|---|
| Heap (binary) | O((V+E) log V) | O(V + E) | Standard; E entries in heap |
| Heap (Fibonacci) | O(V log V + E) | O(V) | Theoretical; complex implementation |
| Array | O(V²) | O(V) | Better for dense: E ≈ V² |
| With path reconstruction | O((V+E) log V) | O(V) | Extra parent[] array |
| Modified (k stops) | O(E × k) | O(V) | State = (node, stops) |
Common Mistakes
Not skipping stale heap entries. When a shorter path to vertex v is found, a new (dist[v], v) is pushed but the old entry remains. When the old entry is extracted, d > dist[v] — skip it. Without this check, you reprocess vertices with outdated distances, causing wrong relaxations.
Using settled[] array in modified Dijkstra (k stops). In standard Dijkstra, once a vertex is settled its distance is final. In the k-stops variant, the same vertex can be reached with different numbers of remaining stops — each is a distinct state. Marking vertices as "settled" after first extraction causes incorrect answers.
Integer overflow when dist[u] = INT_MAX. dist[u] + weight overflows if dist[u] == INT_MAX and weight > 0. Always guard with if dist[u] != INT_MAX before relaxation. In Java: use Integer.MAX_VALUE / 2. In Python: use float('inf') which handles arithmetic correctly.
Forgetting to handle unreachable vertices in Network Delay Time. If dist[i] == ∞ for any node, return -1. Not checking this gives the wrong "maximum" (infinity or INT_MAX).
1-indexed vs 0-indexed mismatch. LeetCode problems often use 1-indexed nodes. Build the adjacency list with size n+1 and initialise dist[] with size n+1. Loop from 1 to n (inclusive) when computing the maximum delay. Off-by-one errors here are silent — no crash, just wrong answers.
Interview Questions
Q: Explain the Dijkstra greedy invariant and why it holds only for non-negative weights.
When vertex u is extracted from the min-heap, all other vertices in the heap have distance ≥ dist[u]. Any path to u through an unprocessed vertex v must have total length ≥ dist[v] ≥ dist[u]. Since edge weights are non-negative, extending any path through v can only increase the distance. Therefore no alternative path can improve dist[u] — it's final. With a negative edge w(v,u) < 0, the path source→...→v→u might have total length dist[v] + w(v,u) < dist[u], breaking the invariant.
Q: What is "lazy deletion" in Dijkstra's heap implementation?
Lazy deletion keeps outdated heap entries without removing them. When vertex v's distance is updated, a new (dist[v], v) is pushed into the heap without removing the old (old_dist, v). When the old entry is eventually popped, the check if d > dist[u]: skip detects and discards it. The alternative — decrease-key operation — requires finding and removing the old entry, which is O(V) in a binary heap (or O(log V) with an indexed heap). Lazy deletion is simpler and the extra stale entries add at most E extra heap entries, making total heap size O(E) and total time O(E log E) = O(E log V).
Q: Dijkstra vs BFS for shortest paths — when is each appropriate?
BFS finds shortest paths in unweighted graphs (all edges = weight 1) in O(V+E) — optimal for this case. Dijkstra finds shortest paths in weighted graphs (all weights non-negative) in O((V+E) log V). For an unweighted graph, Dijkstra reduces to BFS (all relaxations give distance + 1). Use BFS when all edge weights are equal; use Dijkstra when edges have different positive weights. For 0-1 weighted graphs, use 0-1 BFS with a deque — it's O(V+E), faster than Dijkstra's O((V+E) log V).
Summary
Dijkstra's algorithm finds single-source shortest paths in O((V+E) log V) using a min-heap. The greedy invariant — extracted vertices have final distances — holds only for non-negative edge weights.
Three implementations:
- ›Heap — O((V+E) log V), best for sparse graphs; standard choice
- ›Array — O(V²), best for dense graphs (E ≈ V²)
- ›Modified — extend state for constrained paths (k stops, k hops)
Key implementation details:
- ›Skip stale entries:
if d > dist[u]: continue - ›Path reconstruction: parent[v] = u when dist[v] updated; backtrack from target
- ›Guard against overflow: check
dist[u] != INT_MAXbefore relaxing
Classic applications:
- ›Network delay time → Dijkstra + max of all distances
- ›Cheapest flights with k stops → Modified Dijkstra with stop counter in state
- ›Single-source shortest path → Standard Dijkstra
- ›All-pairs → Run Dijkstra V times (or use Floyd-Warshall)
In the next topic you will explore Bellman-Ford Algorithm — handling negative edge weights and detecting negative cycles.
Dijkstra's greedy invariant states: once a vertex is extracted from the min-heap, its distance is final. Why does this break with negative edge weights?