Find the shortest path from one node to every other node in a weighted graph. It's the algorithm behind your GPS, network routing protocols, and half of all graph problems in interviews.
IntermediateDijkstra's algorithm finds the shortest path from a single source node to all other nodes in a graph with non-negative edge weights (edges where the cost of traversal is zero or positive — never negative). It was created by Dutch computer scientist Edsger W. Dijkstra in 1956. The story goes that he designed it in about 20 minutes while sitting at a café in Amsterdam, thinking about the shortest route between two cities. He didn't even use pencil and paper. He published it three years later and it became one of the most cited algorithms in history.
The key idea: maintain a set of "finalized" nodes whose shortest distance you know for sure. At each step, pick the unfinalized node with the smallest tentative distance (the best distance estimate found so far), finalize it, and update its neighbors. This is a greedy strategy — at each step you make the locally optimal choice. And it works because all edge weights are non-negative, so once you've found the cheapest way to reach a node, no future path can beat it.
Think of it like water spreading outward from a source. The water reaches the closest puddle first, then the next closest, and so on. It never needs to revisit a puddle because water always flows forward.
Dijkstra assumes that once a node is finalized, its distance can't improve. With negative edges, that assumption crumbles. Imagine a node B finalized with distance 5. Later you discover an edge C→B with weight -10 where C has distance 12. The path through C gives B a distance of 2 — but Dijkstra already locked in 5 and moved on. It'll never correct this.
For graphs with negative weights (but no negative cycles — loops where the total weight is negative, making shortest paths undefined because you can loop forever getting shorter), use Bellman-Ford instead. It's slower — O(V·E) instead of O(E log V) — but handles negatives correctly.
Dijkstra's algorithm is essentially "greedy BFS with a priority queue." Here's the process:
dist[current] + edge_weight. If that's less than the neighbor's current distance, update it and add (or re-add) the neighbor to the queue.Relaxation is the core operation. It means "I found a potentially better path — let me check." When processing node u and examining its neighbor v:
dist[u] + weight(u,v) < dist[v], then the path through u to v is shorter than any path we'd found beforedist[v] = dist[u] + weight(u,v) and record that v's shortest path comes through u (for path reconstruction later)The priority queue (also called a min-heap — a tree-based structure where the smallest element is always at the root) is the engine of Dijkstra. Your choice of implementation affects performance:
heapq), you can't efficiently update a node's priority once it's in the heap. The common workaround: push duplicate entries and skip stale ones when you pop them. This is called lazy deletion. When you pop a node, check if it was already finalized. If yes, it's stale — skip it and pop the next one. This is simple and works well in practice.
Dijkstra gives you distances, but often you need the actual path. Keep a predecessor map: when you relax edge u→v, set prev[v] = u. To reconstruct the path to any target, follow the prev pointers backward from target to source, then reverse.
Click a node to set it as the source. Watch Dijkstra work step by step — the algorithm picks the closest unfinalized node, relaxes its neighbors, and updates distances. Finalized nodes turn green, and the current shortest path tree is highlighted.
float('inf'). In Java/C++, use Integer.MAX_VALUE (but watch out for overflow when you add to it).
dist[u] + weight when dist[u] is MAX_VALUE causes overflow to negative numbers — which then passes the < dist[v] check incorrectly. Guard against this: check dist[u] != INF before adding, or use long/long long.
| Implementation | Time | Space | Best For |
|---|---|---|---|
| Binary Heap (standard) | O((V + E) log V) | O(V + E) | Sparse graphs. Most common. |
| Fibonacci Heap | O(E + V log V) | O(V + E) | Dense graphs. Theoretical. |
| Array (no heap) | O(V²) | O(V) | Dense graphs (E ≈ V²) |
In practice, the binary heap version handles most cases. Here's the breakdown: every node gets extracted from the heap once (V extractions at O(log V) each), and every edge triggers at most one heap insertion (E insertions at O(log V) each). Total: O((V + E) log V). Since E ≥ V-1 for a connected graph, this simplifies to O(E log V) in most discussions.
| Algorithm | Time | Negative Edges? | Single/All Source |
|---|---|---|---|
| BFS | O(V + E) | N/A (unweighted) | Single source |
| Dijkstra | O((V+E) log V) | No | Single source |
| Bellman-Ford | O(V × E) | Yes (no neg cycles) | Single source |
| Floyd-Warshall | O(V³) | Yes (no neg cycles) | All pairs |
| A* Search | O(E log V)* | No | Single target |
*A* performance depends heavily on the heuristic quality. With a perfect heuristic, it finds the answer immediately. With a bad one, it degrades to Dijkstra.
import heapq
def dijkstra(graph, source):
"""Dijkstra's shortest path from source to all reachable nodes.
graph: adjacency list as dict
{node: [(neighbor, weight), (neighbor, weight), ...]}
source: starting node
Returns:
dist: dict mapping each node to its shortest distance from source
prev: dict mapping each node to its predecessor on the shortest path
(use reconstruct_path() to get the actual path)
"""
dist = {node: float('inf') for node in graph}
dist[source] = 0
prev = {node: None for node in graph}
# Min-heap entries: (distance, node)
# Python's heapq is a min-heap — smallest element comes out first
heap = [(0, source)]
finalized = set() # Nodes whose shortest distance is confirmed
while heap:
d, u = heapq.heappop(heap)
# Lazy deletion: this entry is stale if we already finalized u
# (we pushed a duplicate with a shorter distance earlier)
if u in finalized:
continue
finalized.add(u)
# Relax all neighbors of u
for v, weight in graph[u]:
new_dist = d + weight
if new_dist < dist[v]:
dist[v] = new_dist
prev[v] = u
heapq.heappush(heap, (new_dist, v))
return dist, prev
def reconstruct_path(prev, target):
"""Walk backwards from target to source using the predecessor map.
Returns the path as a list from source to target.
Returns empty list if target is unreachable.
"""
path = []
node = target
while node is not None:
path.append(node)
node = prev[node]
path.reverse()
# If path starts with source, it's valid; otherwise target was unreachable
return path
function dijkstra(graph, source) {
// graph: { node: [[neighbor, weight], ...] }
const dist = {};
const prev = {};
const finalized = new Set();
// Initialize all distances to Infinity
for (const node of Object.keys(graph)) {
dist[node] = Infinity;
prev[node] = null;
}
dist[source] = 0;
// Simple priority queue using sorted array
// (For production code, use a proper min-heap library)
const pq = [[0, source]];
while (pq.length > 0) {
pq.sort((a, b) => a[0] - b[0]);
const [d, u] = pq.shift();
if (finalized.has(u)) continue; // Lazy deletion — skip stale entries
finalized.add(u);
for (const [v, weight] of (graph[u] || [])) {
const newDist = d + weight;
if (newDist < dist[v]) {
dist[v] = newDist;
prev[v] = u;
pq.push([newDist, v]);
}
}
}
return { dist, prev };
}
import heapq
def dijkstra_target(graph, source, target):
"""Dijkstra with early termination — stops as soon as we finalize target.
This is faster when you only care about one destination, because you
skip processing all nodes that are farther away than the target.
"""
dist = {node: float('inf') for node in graph}
dist[source] = 0
heap = [(0, source)]
finalized = set()
while heap:
d, u = heapq.heappop(heap)
if u in finalized:
continue
finalized.add(u)
# Once target is finalized, we know its shortest distance for certain
if u == target:
return d
for v, weight in graph[u]:
new_dist = d + weight
if new_dist < dist[v]:
dist[v] = new_dist
heapq.heappush(heap, (new_dist, v))
return float('inf') # Target unreachable from source
from collections import deque
def bfs_01(graph, source):
"""0-1 BFS: Dijkstra-like shortest path when all weights are 0 or 1.
Uses a deque instead of a heap:
- 0-weight edges: push neighbor to FRONT (it's equally close)
- 1-weight edges: push neighbor to BACK (it's one step farther)
This gives O(V + E) time — faster than Dijkstra's O(E log V).
Perfect for grid problems with "free" vs "costly" moves.
"""
dist = {node: float('inf') for node in graph}
dist[source] = 0
dq = deque([source])
while dq:
u = dq.popleft()
for v, w in graph[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
if w == 0:
dq.appendleft(v) # Free edge — visit soon
else:
dq.append(v) # Cost-1 edge — visit later
return dist
OSPF (Open Shortest Path First) is one of the most widely deployed routing protocols on the internet. Every OSPF router builds a map of the network (the "link-state database") and runs Dijkstra to compute the shortest path to every other router. That's not metaphorical — the routers literally run Dijkstra's algorithm.
Here's a simplified version showing how a router would compute its routing table:
import heapq
def compute_routing_table(network, my_router):
"""Simulate OSPF-style routing table computation.
network: adjacency list of router connections
{"R1": [("R2", 10), ("R3", 5)], ...}
Edge weights represent link costs (based on bandwidth:
cost = reference_bandwidth / link_bandwidth)
my_router: this router's ID
Returns: routing table mapping each destination to
(next_hop, total_cost, full_path)
"""
# Run Dijkstra from this router
dist = {r: float('inf') for r in network}
dist[my_router] = 0
prev = {r: None for r in network}
heap = [(0, my_router)]
finalized = set()
while heap:
cost, router = heapq.heappop(heap)
if router in finalized:
continue
finalized.add(router)
for neighbor, link_cost in network[router]:
new_cost = cost + link_cost
if new_cost < dist[neighbor]:
dist[neighbor] = new_cost
prev[neighbor] = router
heapq.heappush(heap, (new_cost, neighbor))
# Build routing table: for each destination, find next hop
routing_table = {}
for dest in network:
if dest == my_router:
continue
if dist[dest] == float('inf'):
continue # Unreachable
# Trace back from dest to find the first hop after my_router
path = []
node = dest
while node is not None:
path.append(node)
node = prev[node]
path.reverse()
next_hop = path[1] if len(path) > 1 else dest
routing_table[dest] = {
"next_hop": next_hop,
"cost": dist[dest],
"path": path
}
return routing_table
# Example: Small office network
network = {
"Core": [("Sales", 10), ("Eng", 5), ("DC", 2)],
"Sales": [("Core", 10), ("Guest", 20)],
"Eng": [("Core", 5), ("DC", 3), ("Lab", 8)],
"DC": [("Core", 2), ("Eng", 3)],
"Guest": [("Sales", 20)],
"Lab": [("Eng", 8)],
}
table = compute_routing_table(network, "Core")
for dest, info in sorted(table.items()):
print(f" To {dest:6s}: via {info['next_hop']:6s} cost={info['cost']:3d} path={' → '.join(info['path'])}")
# Output:
# To DC : via DC cost= 2 path=Core → DC
# To Eng : via Eng cost= 5 path=Core → Eng
# To Guest : via Sales cost= 30 path=Core → Sales → Guest
# To Lab : via Eng cost= 13 path=Core → Eng → Lab
# To Sales : via Sales cost= 10 path=Core → Sales
Every time a link goes up or down, OSPF routers flood the change across the network and re-run Dijkstra. On modern hardware, this takes microseconds even for networks with thousands of routers. Dijkstra in production, running billions of times a day across the internet.
| Problem | Difficulty | Pattern | Link |
|---|---|---|---|
| Network Delay Time | Medium | Standard Dijkstra | LC #743 |
| Path With Minimum Effort | Medium | Modified weight (max edge) | LC #1631 |
| Cheapest Flights Within K Stops | Medium | Multi-state Dijkstra | LC #787 |
| Swim in Rising Water | Hard | Grid Dijkstra | LC #778 |
| Path with Maximum Probability | Medium | Max-heap Dijkstra | LC #1514 |
| Shortest Path in a Grid with Obstacles | Hard | Multi-state BFS/Dijkstra | LC #1293 |