DSA Tutorial
🔍

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

VariantTimeSpaceNotes
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
ArrayO(V²)O(V)Better for dense: E ≈ V²
With path reconstructionO((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_MAX before 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.

Suggested Quiz

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?

1/6