DSA Graphs
Dijkstra's Algorithm
BFS counts hops. The courier app charges by road time. Dual conflict: the unweighted shortest path is correct for edges, wrong for minutes. You need distances under .
Dijkstra grows a : each extracted minimum distance is final. updates tentative neighbors through a that may hold .
Algorithm journey
Check weights ≥ 0
Otherwise the settled-set proof fails.
dist[s]=0, others ∞
Push s into the min-heap.
Extract minimum
Non-stale pop finalizes that vertex.
Improve neighbors; push new states.
What actually happens
Naive idea: BFS for “shortest.” Wrong when edge weights differ. Or: mark a vertex settled on first discovery (as in BFS). Wrong for weighted graphs: the first path found need not be cheapest.
Better idea: always expand the unsettled vertex with the smallest tentative distance. Under non-negative weights, that distance cannot improve later, so the vertex joins the settled set.
The improved design still fails on negative edges, on int overflow that wraps a huge cost into a “better” distance, and if you stop when the target is first pushed instead of when it is settled (popped as minimum).
Invariant
After each non-stale extract-min of u, dist[u] equals the true shortest-path distance from the source (under non-negative weights). The settled set only grows; distances of settled vertices never decrease again.
How to choose Dijkstra
1. All weights 1 (or unweighted)? Prefer BFS: same answer, simpler and often faster.
2. Non-negative weights? Dijkstra. Negatives present? Bellman–Ford or constrained variants.
3. Dense graph? O(V²) array Dijkstra can beat a binary heap when E ≈ V².
Quantitative check
Binary heap: O((V+E) log V). At V = 10⁵, E = 10⁶, expect on the order of 10⁶ log(10⁵) ≈ 2×10⁷ heap operations: usually acceptable. Skipping the stale-entry guard can multiply pops with obsolete states on dense graphs.
Interactive Dijkstra demo
Click "Run Dijkstra" to see how the algorithm explores the graph. Notice how it prioritizes nodes with the smallest current distance.
Priority Queue
Lazy priority-queue trace
Step through the implementation pattern used in interviews and production libraries: insert a new state after every improvement, then discard obsolete states when they reach the top.
Dijkstra lazy-heap trace
Follow successful relaxations, parent changes, and the duplicate heap entries that make decrease-key unnecessary.
Only S is reachable. The heap contains one candidate: (S, 0).
Min-heap snapshot
Parent chain to T
unreachable
Rewrite a parent only on a strict distance improvement.
Preconditions and result contract
Weight contract
Every edge reachable from the source must have weight ≥ 0. Validate this if callers can provide arbitrary data.
Distance type
Use long for accumulated Java costs and checked addition when input magnitude is not tightly bounded.
Unreachable
A vertex left at INF has no source path. Keep that state separate from every valid numeric distance.
Path ties
Strict < returns one shortest path. Define a tie rule if the lexicographically smallest path or path count matters.
Algorithm steps
Initialize
Set distance to start node as 0, and infinity for all others. Add start node to a priority queue.
Pick Smallest
Extract the node with the smallest distance from the priority queue.
Relax Neighbors
For each neighbor, calculate new_dist = current_dist + edge_weight. If new_dist < old_dist, update the distance and add to queue.
Repeat
Repeat until the priority queue is empty or the destination is reached.
The limitation
Dijkstra fails if there are negative edge weights. Why? Once a node is marked "visited", Dijkstra assumes it found the shortest path. A negative edge later could contradict this assumption. (Use Bellman-Ford for negative weights).
Where it shows up
Google Maps uses variants of Dijkstra (like A*) to find the fastest route.OSPF (Open Shortest Path First) protocol uses it to route data packets across the internet efficiently.
Correctness sketch & implementation details
Non-negative weights guarantee that the shortest path to a node found when it is extracted from the priority queue cannot later be improved: any alternative route would have to leave through an edge of non-negative length, so it could not beat the already-finalized distance.
Lazy heap: pushing every relaxation is fine; stale entries (larger distance than dist[u]) are discarded on pop, simpler than decrease-key in many standard libraries.
Store a parent map during relaxation to reconstruct paths. For single-target search, stop when the target is popped (not merely when it is first pushed).
Cut argument
- Assume
uis the current minimum non-stale heap entry but a cheaper source-to-u path exists. - On that alleged path, take the first vertex
ynot yet settled and its settled predecessorx. - When
xwas settled, relaxation madedist[y] ≤ dist[x] + w(x,y). - The remaining edges are non-negative, so
dist[y]cannot exceed the alleged cheaper cost tou. That contradicts choosinguas the minimum.
A stale heap record is not a competing answer; it is an old snapshot. The authoritative state is dist[u], so poppedDistance != dist[u] is enough to skip it. A separate visited set is optional and often less clear.
Complexity analysis
Time Complexity
- •O((V + E) log V)
Using a Binary Heap (Priority Queue)
- •O(V²)
Using a simple array (good for dense graphs)
Space Complexity
- •O(V + E)
To store the graph and distances
Implementation
We use a Min-Heap (Priority Queue) to efficiently get the node with the smallest distance.
Python
import heapq
def dijkstra(graph, start):
"""
Dijkstra's Algorithm for Shortest Paths
Args:
graph: Dictionary {node: {neighbor: weight}}
start: Starting node
Returns:
Dictionary {node: shortest_distance}
"""
# Priority queue stores tuples: (distance, vertex)
# Initialize with start node at distance 0
pq = [(0, start)]
# Dictionary to store shortest distance to each node
# Initialize all to Infinity
distances = {vertex: float('infinity') for vertex in graph}
distances[start] = 0
while pq:
# 1. Get the node with the smallest distance
current_dist, current_node = heapq.heappop(pq)
# Optimization: If we found a shorter path before, skip this stale entry
if current_dist > distances[current_node]:
continue
# 2. Explore neighbors
for neighbor, weight in graph[current_node].items():
distance = current_dist + weight
# 3. Relaxation: If we found a shorter path, update it
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(pq, (distance, neighbor))
return distances
# Example Usage
graph = {
'A': {'B': 4, 'C': 2},
'B': {'D': 3, 'E': 1},
'C': {'B': 1, 'D': 5},
'D': {'E': 2},
'E': {}
}
shortest_paths = dijkstra(graph, 'A')
print(f"Shortest distances from A: {shortest_paths}")
# Output: Shortest distances from A: {'A': 0, 'B': 3, 'C': 2, 'D': 6, 'E': 4}Java
import java.util.*;
class Node implements Comparable<Node> {
public String id;
public long distance;
public Node(String id, long distance) {
this.id = id;
this.distance = distance;
}
@Override
public int compareTo(Node other) {
return Long.compare(this.distance, other.distance);
}
}
public class Dijkstra {
public static Map<String, Long> dijkstra(Map<String, Map<String, Integer>> graph, String start) {
// Priority queue to store nodes sorted by distance
PriorityQueue<Node> pq = new PriorityQueue<>();
pq.add(new Node(start, 0));
// Map to store shortest distance to each node
Map<String, Long> distances = new HashMap<>();
for (String node : graph.keySet()) {
distances.put(node, Long.MAX_VALUE);
}
distances.put(start, 0L);
while (!pq.isEmpty()) {
// 1. Get the node with the smallest distance
Node current = pq.poll();
String currentNode = current.id;
long currentDist = current.distance;
// Optimization: Skip stale entries
if (currentDist > distances.get(currentNode)) {
continue;
}
// 2. Explore neighbors
Map<String, Integer> neighbors = graph.get(currentNode);
if (neighbors != null) {
for (Map.Entry<String, Integer> neighborEntry : neighbors.entrySet()) {
String neighbor = neighborEntry.getKey();
int weight = neighborEntry.getValue();
if (weight < 0) {
throw new IllegalArgumentException("Dijkstra requires non-negative weights");
}
long newDist = Math.addExact(currentDist, (long) weight);
// 3. Relaxation
if (newDist < distances.get(neighbor)) {
distances.put(neighbor, newDist);
pq.add(new Node(neighbor, newDist));
}
}
}
}
return distances;
}
public static void main(String[] args) {
Map<String, Map<String, Integer>> graph = new HashMap<>();
// Building the graph
graph.put("A", new HashMap<>(Map.of("B", 4, "C", 2)));
graph.put("B", new HashMap<>(Map.of("D", 3, "E", 1)));
graph.put("C", new HashMap<>(Map.of("B", 1, "D", 5)));
graph.put("D", new HashMap<>(Map.of("E", 2)));
graph.put("E", new HashMap<>());
Map<String, Long> shortestPaths = dijkstra(graph, "A");
System.out.println("Shortest distances from A: " + shortestPaths);
// Output: Shortest distances from A: {A=0, B=3, C=2, D=6, E=4}
}
}Java template: distances + paths
This array-based template validates the weight contract, uses a lazy min-heap, performs checked long addition, and refuses to fabricate a path for an unreachable or corrupt parent chain.
import java.util.*;
final class DijkstraPaths {
static final long INF = Long.MAX_VALUE;
static final class Edge {
final int to;
final long weight;
Edge(int to, long weight) {
this.to = to;
this.weight = weight;
}
}
static final class State implements Comparable<State> {
final int vertex;
final long distance;
State(int vertex, long distance) {
this.vertex = vertex;
this.distance = distance;
}
public int compareTo(State other) {
return Long.compare(distance, other.distance);
}
}
static final class Result {
final int source;
final long[] distance;
final int[] parent;
Result(int source, long[] distance, int[] parent) {
this.source = source;
this.distance = distance;
this.parent = parent;
}
}
static Result shortestPaths(List<Edge>[] graph, int source) {
int n = graph.length;
if (source < 0 || source >= n) throw new IllegalArgumentException("bad source");
for (List<Edge> edges : graph) {
for (Edge edge : edges) {
if (edge.weight < 0) {
throw new IllegalArgumentException("negative edge");
}
}
}
long[] dist = new long[n];
int[] parent = new int[n];
Arrays.fill(dist, INF);
Arrays.fill(parent, -1);
dist[source] = 0L;
PriorityQueue<State> minHeap = new PriorityQueue<>();
minHeap.add(new State(source, 0L));
while (!minHeap.isEmpty()) {
State current = minHeap.poll();
int u = current.vertex;
if (current.distance != dist[u]) continue; // stale heap entry
for (Edge edge : graph[u]) {
long candidate = Math.addExact(dist[u], edge.weight);
if (candidate >= dist[edge.to]) continue;
dist[edge.to] = candidate;
parent[edge.to] = u;
minHeap.add(new State(edge.to, candidate));
}
}
return new Result(source, dist, parent);
}
static List<Integer> pathTo(Result result, int target) {
if (target < 0 || target >= result.distance.length) {
throw new IllegalArgumentException("bad target");
}
if (result.distance[target] == INF) return List.of();
List<Integer> reversed = new ArrayList<>();
boolean[] seen = new boolean[result.distance.length];
for (int at = target; at != -1; at = result.parent[at]) {
if (seen[at]) throw new IllegalStateException("parent cycle");
seen[at] = true;
reversed.add(at);
if (at == result.source) {
Collections.reverse(reversed);
return reversed;
}
}
throw new IllegalStateException("parent chain missed source");
}
}Complexity trade-offs and failure modes
| Choice / bug | Effect | Guidance |
|---|---|---|
| Adjacency list + binary heap | O((V+E) log V), O(V+E) graph space | Default for sparse graphs and standard libraries. |
| Matrix + linear minimum scan | O(V²), O(V²) graph space | Competitive when E≈V²; avoids heap overhead. |
| No stale-entry guard | Still often correct, but repeatedly scans old states and can destroy practical performance. | Compare the popped snapshot with authoritative dist[u]. |
| Visited on insertion | Locks the first discovered path, which need not be cheapest. | Finalize only a current minimum pop. |
| Negative edge | Invalidates the greedy proof even if a sample happens to pass. | Reject input or select DAG/Bellman–Ford logic. |
int accumulated cost | Wraparound can make a huge path look negative and win the heap. | Use long and bounded or checked addition. |
Short takeaway
Dijkstra computes shortest paths under non-negative weights by growing a settled set via extract-min. Use a priority queue, discard stale entries, and switch algorithms when negatives appear.
Exercises
- Give a tiny graph with one negative edge where Dijkstra settles a vertex too early. Which vertex enters the settled set with the wrong distance?
- Trace a lazy heap on a diamond where a cheaper path arrives after an expensive one was pushed. Which pops are stale, and what is dist[] after each?
- V = 10⁴ dense (E ≈ V²). Compare binary-heap Dijkstra with the O(V²) array variant. Which would you pick and why?
Primary sources
- Dijkstra, E. W. (1959): A note on two problems in connexion with graphs.
- Cormen et al., Introduction to Algorithms (CLRS): Dijkstra's algorithm and the settled-set proof.
- Fredman & Tarjan: Fibonacci heaps (theoretical decrease-key; contrast with lazy binary heaps in practice).