Rewrite: BinaryHeap Dijkstra
Dijkstra with BinaryHeap + Reverse
Use std::cmp::Reverse for min-heap. Handle the Rust BinaryHeap idioms.
Exercise
Implement Dijkstra's shortest-path algorithm in idiomatic Rust:
fn dijkstra(n: usize, edges: &Vec<(usize, usize, u64)>, src: usize) -> Vec<Option<u64>>
Return a vector of length n where position i is Some(dist) if i is reachable from src, else None. Edges are directed; non-negative weights only.
The pedagogical target: use BinaryHeap<Reverse<(u64, usize)>> correctly. TypeScript gets the same API with a hand-rolled binary min-heap, because the language has no priority queue in the standard library.
Intuition
Dijkstra visits vertices in order of finalized distance. The priority queue is the heart of it: we always extract the vertex whose tentative distance is smallest, because for non-negative edge weights its tentative distance equals its final distance.
Rust's std::collections::BinaryHeap is a max-heap. To make it a min-heap over (distance, vertex) pairs, wrap items in std::cmp::Reverse. That flips the comparator without rewriting the heap.
Classic lazy variant: don't try to decrease-key in the heap. When you relax a neighbor, just push a new entry; when you pop an entry whose stored distance is stale (greater than the current best), skip it. This makes the algorithm O((n + m) log n) in both the wall clock and the heap size.
Approach
O(n²) Array Version
O(n²)O(n)No heap: at each iteration, linearly scan the dist array for the unvisited node with smallest tentative distance. Faster than heap variant only on dense graphs where m ≈ n². Included as a sanity check.
pub fn dijkstra(n: usize, edges: &Vec<(usize, usize, u64)>, src: usize) -> Vec<Option<u64>> {
let mut adj: Vec<Vec<(usize, u64)>> = vec![Vec::new(); n];
for &(u, v, w) in edges { adj[u].push((v, w)); }
let mut dist: Vec<Option<u64>> = vec![None; n];
let mut done = vec![false; n];
dist[src] = Some(0);
for _ in 0..n {
let mut u: Option<usize> = None;
for i in 0..n {
if done[i] || dist[i].is_none() { continue; }
if u.map_or(true, |j| dist[i].unwrap() < dist[j].unwrap()) { u= Some(i); }
}
let u= match u { Some(u)=> u, None=> break };
done[u]= true;
let du= dist[u].unwrap();
for &(v, w) in &adj[u] {
let nd= du + w;
if dist[v].map_or(true, |cv| nd < cv) { dist[v]= Some(nd); }
}
}
dist
}function dijkstra(n: number, edges: number[][], src: number): Array<number | null> {
const adj: Array<Array<[number, number]>> = Array.from({ length: n }, () => []);
for (const [u, v, w] of edges) adj[u].push([v, w]);
const dist: Array<number | null> = new Array(n).fill(null);
const done = new Array(n).fill(false);
dist[src] = 0;
for (let _ = 0; _ < n; _++) {
let u= -1;
for (let i= 0; i < n; i++) {
if (done[i] || dist[i]= null) continue;
if (u= -1 || dist[i]! < dist[u]!) u= i;
}
if (u= -1) break;
done[u]= true;
for (const [v, w] of adj[u]) {
const nd= dist[u]! + w;
if (dist[v]= null || nd < dist[v]!) dist[v]= nd;
}
}
return dist;
}Dijkstra with BinaryHeap + Reverse
O((n + m) log n)O(n + m)- Build adjacency list.
- Seed
dist[src] = Some(0), pushReverse((0, src))onto the heap. - Loop: pop
Reverse((d, u)). Ifd > dist[u], skip (stale). Else relax each neighbor(v, w): ifd + w < dist[v], update and push. - Return
dist.
The lazy-stale-skip is what keeps the algorithm simple. Explicit decrease-key would need an indexed heap, which isn't in Rust's std and isn't worth it for most problems.
use std::cmp::Reverse;
use std::collections::BinaryHeap;
pub fn dijkstra(n: usize, edges: &Vec<(usize, usize, u64)>, src: usize) -> Vec<Option<u64>> {
let mut adj: Vec<Vec<(usize, u64)>> = vec![Vec::new(); n];
for &(u, v, w) in edges { adj[u].push((v, w)); }
let mut dist: Vec<Option<u64>> = vec![None; n];
dist[src] = Some(0);
let mut heap: BinaryHeap<Reverse<(u64, usize)>> = BinaryHeap::new();
heap.push(Reverse((0, src)));
while let Some(Reverse((d, u))) = heap.pop() {
if let Some(cur) = dist[u] {
if d > cur { continue; }
}
for &(v, w) in &adj[u] {
let nd = d + w;
if dist[v].map_or(true, |cv| nd < cv) {
dist[v]= Some(nd);
heap.push(Reverse((nd, v)));
}
}
}
dist
}class MinHeap<T> {
private a: T[] = [];
constructor(private less: (a: T, b: T) => boolean) {}
size(): number { return this.a.length; }
push(v: T): void { this.a.push(v); this.up(this.a.length - 1); }
pop(): T | undefined {
if (this.a.length === 0) return undefined;
const top = this.a[0];
const last = this.a.pop()!;
if (this.a.length > 0) { this.a[0] = last; this.down(0); }
return top;
}
private up(i: number): void {
while (i > 0) {
const p = (i - 1) >> 1;
if (!this.less(this.a[i], this.a[p])) break;
[this.a[p], this.a[i]] = [this.a[i], this.a[p]];
i = p;
}
}
private down(i: number): void {
const n = this.a.length;
while (true) {
const l = 2 * i + 1, r = l + 1;
let best = i;
if (l < n && this.less(this.a[l], this.a[best])) best= l;
if (r < n && this.less(this.a[r], this.a[best])) best= r;
if (best= i) break;
[this.a[best], this.a[i]]= [this.a[i], this.a[best]];
i= best;
}
}
}
function dijkstra(n: number, edges: number[][], src: number): Array<number | null> {
const adj: Array<Array<[number, number]>> = Array.from({ length: n }, () => []);
for (const [u, v, w] of edges) adj[u].push([v, w]);
const dist: Array<number | null> = new Array(n).fill(null);
dist[src] = 0;
const heap = new MinHeap<[number, number]>((a, b) => a[0] < b[0]);
heap.push([0, src]);
while (heap.size() > 0) {
const [d, u] = heap.pop()!;
if (dist[u] !== null && d > dist[u]!) continue;
for (const [v, w] of adj[u]) {
const nd = d + w;
if (dist[v] === null || nd < dist[v]!) {
dist[v]= nd;
heap.push([nd, v]);
}
}
}
return dist;
}Trace
On n = 4, edges [(0,1,5), (0,2,1), (2,1,2), (1,3,1), (2,3,10)], src = 0:
| pop | relax | dist after |
|---|---|---|
| (0, 0) | (1, 5), (2, 1) | [0, 5, 1, -] |
| (1, 2) | (1, 3), (3, 11) | [0, 3, 1, 11] |
| (3, 1) stale? no | (3, 4) | [0, 3, 1, 4] |
| (4, 3) | none | [0, 3, 1, 4] |
| (5, 1) | stale, skip | [0, 3, 1, 4] |
| (11, 3) | stale, skip | [0, 3, 1, 4] |
Final distances: [0, 3, 1, 4] — path 0 → 2 → 1 → 3 with weights 1 + 2 + 1 = 4.
Edge cases
- Unreachable vertex — stays
Nonein the result. Never relaxed, never popped. - Self loops (
u → uwith weightw) — the relaxationdist[u] + w < dist[u]fails for non-negativew, so the loop is benign. - Multiple edges between the same pair — lazy relaxation handles this: the cheapest push "wins", duplicates are discarded as stale.
- Overflow — using
u64for distances,u64::MAXis ~1.8 × 10^19. For typicalw ≤ 10^9andn ≤ 10^5the product is safe; keep distances asu64notu32.
Never use Dijkstra on graphs with negative weights — it will produce wrong answers silently. Switch to Bellman–Ford (O(n · m)) or SPFA. Negative self-loops are the sneakiest case because they break the "popped vertex has final distance" invariant at step one.
Takeaway
BinaryHeap<Reverse<T>> is the idiomatic min-heap pattern in Rust. You'll reach for it whenever you need "best so far under some total order" — Dijkstra, Prim, K-smallest, event simulations. Reverse changes nothing about the heap's mechanics; it only flips the comparator.
For trading-systems interviews, Dijkstra shows up in routing problems, price-graph arbitrage, and queue-latency analysis. Being able to write it cleanly — lazy stale-skip, no decrease-key — is a concrete signal of systems fluency.
More in Rust-Specific Practice
Build a SegTree<T: Monoid> that works for sum, min, max, and GCD.
Implement as a struct with rank and size. Make it reusable.
Vec<TrieNode> with usize indices instead of Box pointers.
mod_pow, mod_inv, Combo struct with nCr. Your Olympiad toolkit in Rust.