Rust for Algorithmic Problem-Solving
BFS and DFS
Breadth-First Search (BFS) and Depth-First Search (DFS) are the two fundamental $O(V + E)$ graph traversal strategies: BFS explores vertices in concentric layers of increasing edge distance using a FIFO queue (VecDeque), while DFS plunges deep along a path before backtracking using a LIFO stack (Vec or recursion).
(0)
/ \
(1) (2) BFS Visit Order (Layer by Layer): 0 -> 1 -> 2 -> 3 -> 4
/ \ DFS Visit Order (Branch Deep): 0 -> 1 -> 3 -> 4 -> 2
(3) (4)
Comparing BFS and DFS
| Dimension | Breadth-First Search (BFS) | Depth-First Search (DFS) |
|---|---|---|
| Frontier Data Structure | FIFO Queue (VecDeque<usize>) |
Call Stack or Explicit LIFO Stack (Vec<usize>) |
| Time & Space Complexity | $O(V + E)$ time, $O(V)$ memory | $O(V + E)$ time, $O(V)$ memory |
| Shortest Path on Unweighted Graph? | Yes (First time a vertex is reached is guaranteed minimum hops!) | No |
| Primary Algorithmic Applications | Shortest unweighted paths, connected components, bipartite 2-coloring, level-order traversal. | Subtree sizes, cycle detection (3-color DFS), bridges/articulation points, strongly connected components. |
Recursive DFS in Rust: Avoiding Global Mutable State
In C++ or Python, programmers often store visited or adj in global mutable variables. In Rust, you have two clean, idiomatic ways to write recursive DFS without globals:
- Pass
&adjand&mut visited(or aDfsContextstruct) as parameters to an inner helper functionfn dfs(u: usize, adj: &[Vec<usize>], visited: &mut [bool]). - Compute bottom-up tree properties (such as subtree sizes or tree diameter) by passing
parent: usizeto avoid revisiting the parent edge without even needing avisitedarray!
use std::collections::VecDeque;
/// 1. BFS Bipartite Graph Check (2-Coloring) in O(V + E) time:
pub fn is_bipartite(adj: &[Vec<usize>]) -> bool {
let n = adj.len();
// 0 = uncolored, 1 = Red, 2 = Blue
let mut color = vec![0u8; n];
let mut queue = VecDeque::new();
for start in 0..n {
if color[start] != 0 {
continue;
}
color[start] = 1;
queue.push_back(start);
while let Some(u) = queue.pop_front() {
let next_color = 3 - color[u]; // Flips 1 <-> 2
for &v in &adj[u] {
if color[v] == 0 {
color[v] = next_color;
queue.push_back(v);
} else if color[v] == color[u] {
return false; // Odd-length cycle detected!
}
}
}
}
true
}
/// 2. Tree DFS: Computes the number of nodes in every subtree rooted at `root`.
pub fn compute_subtree_sizes(adj: &[Vec<usize>], root: usize) -> Vec<usize> {
let mut sizes = vec![0usize; adj.len()];
fn dfs(u: usize, parent: usize, adj: &[Vec<usize>], sizes: &mut [usize]) -> usize {
let mut count = 1;
for &v in &adj[u] {
if v != parent {
count += dfs(v, u, adj, sizes);
}
}
sizes[u] = count;
count
}
dfs(root, usize::MAX, adj, &mut sizes);
sizes
}
fn main() {
// 4-cycle (0 - 1 - 2 - 3 - 0) is bipartite:
let square = vec![vec![1, 3], vec![0, 2], vec![1, 3], vec![0, 2]];
// 3-cycle (0 - 1 - 2 - 0) is NOT bipartite:
let triangle = vec![vec![1, 2], vec![0, 2], vec![0, 1]];
assert!(is_bipartite(&square));
assert!(!is_bipartite(&triangle));
// Tree: 0 connected to 1 and 2; 1 connected to 3 and 4
let tree = vec![
vec![1, 2],
vec![0, 3, 4],
vec![0],
vec![1],
vec![1],
];
let subtree_sizes = compute_subtree_sizes(&tree, 0);
assert_eq!(subtree_sizes, vec![5, 3, 1, 1, 1]);
println!("Square bipartite: {}, Subtree sizes from root 0: {subtree_sizes:?}", is_bipartite(&square));
}
Square bipartite: true, Subtree sizes from root 0: [5, 3, 1, 1, 1]