Graph Traversal with Vec and VecDeque
Representing a graph as a contiguous adjacency list (Vec<Vec<usize>>) and traversing it with Vec (stack) or VecDeque (double-ended queue) powers $O(V + E)$ linear-time graph algorithms including Kahn's topological sort, cycle detection, and 0-1 BFS on binary-weighted graphs.
0-1 BFS on a Graph with Edge Weights in {0, 1} (`VecDeque`):
Current Vertex `u` at distance `d`:
|-- Edge (u -> v) has weight `0` ---> `deque.push_front((v, d))` (Explore immediately!)
+-- Edge (u -> v) has weight `1` ---> `deque.push_back((v, d + 1))` (Explore in next layer!)
Runs in O(V + E) time (faster than Dijkstra's O((V + E) log V))!
1. Kahn's Algorithm for Topological Sorting & Cycle Detection ($O(V + E)$)
On a Directed Acyclic Graph (DAG) (such as a package dependency graph or build task graph), a topological sort orders all vertices so that for every directed edge $u \to v$, vertex $u$ appears before vertex $v$.
Kahn's algorithm computes the in-degree of every vertex, seeds a VecDeque with all vertices of in_degree == 0, and repeatedly pops a vertex while decrementing the in-degrees of its outgoing neighbors. If the resulting order contains fewer than $V$ vertices, the graph contains a directed cycle!
use std::collections::VecDeque;
/// Returns `Ok(topological_order)` if the directed graph is a DAG,
/// or `Err(())` if the graph contains a directed cycle.
pub fn topological_sort(num_vertices: usize, edges: &[(usize, usize)]) -> Result<Vec<usize>, ()> {
let mut adj = vec![Vec::new(); num_vertices];
let mut in_degree = vec![0usize; num_vertices];
for &(u, v) in edges {
adj[u].push(v);
in_degree[v] += 1;
}
let mut queue: VecDeque<usize> = (0..num_vertices)
.filter(|&u| in_degree[u] == 0)
.collect();
let mut order = Vec::with_capacity(num_vertices);
while let Some(u) = queue.pop_front() {
order.push(u);
for &v in &adj[u] {
in_degree[v] -= 1;
if in_degree[v] == 0 {
queue.push_back(v);
}
}
}
if order.len() == num_vertices {
Ok(order)
} else {
Err(()) // Cycle detected!
}
}
2. 0-1 BFS Using VecDeque::push_front and push_back ($O(V + E)$)
When every edge weight in a graph is either 0 or 1 (for example, navigating a grid where moving along arrows costs 0 and flipping an arrow costs 1), VecDeque solves shortest paths in $O(V + E)$ time without a BinaryHeap:
- Relaxing an edge of weight
0pushes the neighbor to the front of the deque (push_front). - Relaxing an edge of weight
1pushes the neighbor to the back of the deque (push_back).
pub fn bfs_zero_one(adj: &[Vec<(usize, u8)>], start: usize) -> Vec<u32> {
let mut dist = vec![u32::MAX; adj.len()];
let mut deque = VecDeque::new();
dist[start] = 0;
deque.push_front(start);
while let Some(u) = deque.pop_front() {
let d = dist[u];
for &(v, weight) in &adj[u] {
let next_d = d + u32::from(weight);
if next_d < dist[v] {
dist[v] = next_d;
if weight == 0 {
deque.push_front(v);
} else {
deque.push_back(v);
}
}
}
}
dist
}
fn main() {
// 1. Topological sort of a 4-stage build graph: 0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3
let dag_edges = [(0, 1), (0, 2), (1, 3), (2, 3)];
let order = topological_sort(4, &dag_edges).unwrap();
assert_eq!(order, vec![0, 1, 2, 3]);
// Cyclic graph: 0 -> 1 -> 0 returns Err(())
assert!(topological_sort(2, &[(0, 1), (1, 0)]).is_err());
// 2. 0-1 BFS: 0 --(w=1)--> 1, 0 --(w=1)--> 2, 2 --(w=0)--> 1, 1 --(w=0)--> 3
let adj_01 = vec![
vec![(1, 1), (2, 1)],
vec![(3, 0)],
vec![(1, 0)],
vec![],
];
let dists = bfs_zero_one(&adj_01, 0);
assert_eq!(dists, vec![0, 1, 1, 1]);
println!("Topological order: {order:?}, 0-1 BFS distances: {dists:?}");
}
Topological order: [0, 1, 2, 3], 0-1 BFS distances: [0, 1, 1, 1]