Dijkstra's Algorithm with Binary Heap

Coding · Medium · Free problem

Implement Dijkstra's algorithm using a binary heap (min-priority queue) to compute single-source shortest paths in a weighted directed graph with nonnegative edge weights.

Your implementation should:

  1. Compute the shortest distance from a source vertex $s$ to every other vertex in the graph.
  2. Support path reconstruction -- given any reachable vertex $t$, return the actual shortest path from $s$ to $t$.
  3. Handle edge cases correctly: disconnected components (unreachable vertices), parallel edges between the same pair of vertices, and zero-weight edges.

After implementing, prove that Dijkstra's algorithm is correct when all edge weights are nonnegative, and explain why it fails when negative edges are present.

Constraints: -

\leq n \leq 10^5$ (number of vertices) - $0 \leq m \leq 2 \times 10^5$ (number of edges) - $0 \leq w(u, v) \leq 10^9$ for every edge $(u, v)$ - The graph may be disconnected - Parallel edges and self-loops are allowed

Example 1: - Input: $n = 5$, edges $= [(0,1,4), (0,2,1), (2,1,2), (1,3,1), (2,3,5), (3,4,3)]$, source $= 0$ - Output: distances $= [0, 3, 1, 4, 7]$, path to vertex $4$: $[0, 2, 1, 3, 4]$ - Explanation: The shortest path to vertex 1 goes through vertex 2 (cost

+ 2 = 3$), which is cheaper than the direct edge (cost 4).

Example 2: - Input: $n = 3$, edges $= [(0,1,5)]$, source $= 0$ - Output: distances $= [0, 5, \infty]$, vertex 2 is unreachable - Explanation: There is no path from vertex 0 to vertex 2 in this disconnected graph.

Hints

  1. The greedy insight is that the unvisited vertex with the smallest tentative distance already has its true shortest distance -- think about why nonnegativity guarantees this.
  2. Use a min-heap with lazy deletion: push new $(\text{dist}, v)$ pairs freely and skip entries where the popped distance exceeds the current best. This avoids needing a decrease-key operation.
  3. For the correctness proof, consider the first edge $(x, y)$ on the true shortest path where $x$ is finalized but $y$ is not. Use nonnegativity to argue $y$ should have been extracted before $u$.

Worked Solution

How to Think About It: Dijkstra's is the workhorse of shortest-path algorithms for nonnegative weights. The core idea is greedy: the vertex with the smallest tentative distance is guaranteed to already have its true shortest distance locked in, because no future relaxation through a longer path can improve it (this is where nonnegativity is critical). A binary heap lets you extract the minimum efficiently, and the "lazy deletion" pattern (push duplicates and skip stale entries) is the cleanest way to implement it without a decrease-key operation.

Algorithm: Maintain an array $\text{dist}[]$ initialized to $\infty$ except $\text{dist}[s] = 0$, and a predecessor array $\text{prev}[]$ for path reconstruction. Push $(0, s)$ into a min-heap. Repeatedly extract the minimum-distance pair $(d, u)$. If $d > \text{dist}[u]$, this is a stale entry -- skip it. Otherwise, for each neighbor $v$ of $u$, if $\text{dist}[u] + w(u,v) < \text{dist}[v]$, update $\text{dist}[v]$, set $\text{prev}[v] = u$, and push $(\text{dist}[v], v)$ into the heap. Continue until the heap is empty.

Code:

```python import heapq from typing import List, Tuple, Optional

def dijkstra(n: int, edges: List[Tuple[int, int, int]], source: int ) -> Tuple[List[float], List[Optional[int]]]: # Build adjacency list adj = [[] for _ in range(n)] for u, v, w in edges: adj[u].append((v, w))

dist = [float('inf')] * n prev = [None] * n dist[source] = 0 heap = [(0, source)] # (distance, vertex)

while heap: d, u = heapq.heappop(heap) if d > dist[u]: continue # stale entry, skip for v, w in adj[u]: nd = d + w if nd < dist[v]: dist[v] = nd prev[v] = u heapq.heappush(heap, (nd, v))

return dist, prev

def reconstruct_path(prev: List[Optional[int]], target: int) -> List[int]: if prev[target] is None and target != 0: return [] # unreachable path = [] cur = target while cur is not None: path.append(cur) cur = prev[cur] return path[::-1] ```

Correctness Proof (sketch):

We prove by induction that when a vertex $u$ is extracted from the heap, $\text{dist}[u]$ equals the true shortest-path distance $\delta(s, u)$.

  • *Base case:* The source $s$ is extracted first with $\text{dist}[s] = 0 = \delta(s, s)$.
  • *Inductive step:* Suppose all previously extracted vertices have correct distances. When $u$ is extracted, assume for contradiction that $\text{dist}[u] > \delta(s, u)$. Then there exists a shorter path $P$ from $s$ to $u$. Let $(x, y)$ be the first edge on $P$ where $x$ has been extracted but $y$ has not. Since $x$ was extracted earlier, $\text{dist}[x] = \delta(s, x)$ by hypothesis, and edge $(x, y)$ was relaxed when $x$ was processed, so $\text{dist}[y] \leq \delta(s, x) + w(x, y) = \delta(s, y)$. But since all weights are nonnegative, $\delta(s, y) \leq \delta(s, u) < \text{dist}[u]$, so $y$ would have been extracted before $u$ -- contradiction.

Why negative edges break this: If an edge $(x, y)$ has negative weight, then $\delta(s, y) \leq \delta(s, x) + w(x,y)$ no longer implies $\delta(s, y) \leq \delta(s, u)$. A vertex extracted with some distance might later be reachable via a cheaper path through a negative edge that was not yet discovered.

Edge Cases: - *Disconnected vertices:* Remain at $\text{dist} = \infty$, $\text{prev} = \text{None}$. The algorithm handles this naturally since they are never pushed onto the heap. - *Parallel edges:* The adjacency list stores all of them. When relaxing, the shortest one wins automatically. - *Zero-weight edges:* Perfectly fine -- they do not violate the nonnegative assumption. The algorithm treats them like any other edge. A cycle of zero-weight edges does not cause infinite loops because we skip stale heap entries. - *Self-loops:* A self-loop $(u, u, w)$ with $w \geq 0$ never improves $\text{dist}[u]$, so it is harmlessly skipped.

Complexity: $O(m \log n)$ time -- each edge causes at most one heap insertion, and each extraction is $O(\log n)$. Space is $O(n + m)$ for the adjacency list and heap.

Answer: Dijkstra's algorithm with a binary heap computes SSSP in $O(m \log n)$ time for nonnegative weights. The greedy property -- extracting the minimum-distance vertex commits to its final distance -- holds precisely because edge weights are nonnegative.

Intuition

Dijkstra's algorithm is the canonical example of a greedy algorithm on graphs. The key property that makes it work is that nonnegative edge weights create a monotone structure: once you have found the cheapest way to reach a vertex, no future discovery can improve it, because every additional edge you traverse can only add cost. This is the same reason BFS works for unweighted graphs -- Dijkstra is just BFS generalized to nonnegative weights, with a priority queue replacing the FIFO queue.

In practice, the "lazy heap" variant shown here is almost always what you should implement. The textbook version with decrease-key is cleaner in theory but messier in code (most standard libraries do not support decrease-key natively). The lazy version pushes duplicate entries and filters stale ones on extraction -- the overhead is at most a constant factor in the heap size. This pattern shows up constantly in quant tech interviews, both as a standalone question and as a subroutine in more complex graph problems (network flows, arbitrage detection on spread graphs, order routing).

Open the full interactive solver →