Backtracking Framework
Tutorial Video
Text description (accessibility)
This video demonstrates the "Backtracking Framework" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Many combinatorial optimization and enumeration problems — N-queens, Sudoku solving, permutation generation, subset enumeration, graph coloring, constraint satisfaction — require exploring a search space that grows exponentially with input size. Key difference from OCaml: | Aspect | Rust | OCaml |
Tutorial
The Problem
Many combinatorial optimization and enumeration problems — N-queens, Sudoku solving, permutation generation, subset enumeration, graph coloring, constraint satisfaction — require exploring a search space that grows exponentially with input size. Backtracking systematically explores candidates and abandons (backtracks from) any partial solution as soon as it determines it cannot lead to a valid complete solution. This pruning makes backtracking vastly more efficient than brute force for well-constrained problems, though still exponential in the worst case. Understanding backtracking as a reusable framework — not just specific puzzles — enables applying it to new combinatorial problems.
🎯 Learning Outcomes
choose → explore → unchooseused flag array to avoid revisiting elements in permutation generationCode Example
#![allow(clippy::all)]
//! # Backtracking Framework
pub fn generate_permutations<T: Clone>(items: &[T]) -> Vec<Vec<T>> {
let mut result = vec![];
let mut current = vec![];
let mut used = vec![false; items.len()];
fn backtrack<T: Clone>(
items: &[T],
curr: &mut Vec<T>,
used: &mut [bool],
res: &mut Vec<Vec<T>>,
) {
if curr.len() == items.len() {
res.push(curr.clone());
return;
}
for i in 0..items.len() {
if !used[i] {
used[i] = true;
curr.push(items[i].clone());
backtrack(items, curr, used, res);
curr.pop();
used[i] = false;
}
}
}
backtrack(items, &mut current, &mut used, &mut result);
result
}
pub fn generate_subsets<T: Clone>(items: &[T]) -> Vec<Vec<T>> {
let mut result = vec![];
let mut current = vec![];
fn backtrack<T: Clone>(items: &[T], start: usize, curr: &mut Vec<T>, res: &mut Vec<Vec<T>>) {
res.push(curr.clone());
for i in start..items.len() {
curr.push(items[i].clone());
backtrack(items, i + 1, curr, res);
curr.pop();
}
}
backtrack(items, 0, &mut current, &mut result);
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_perms() {
assert_eq!(generate_permutations(&[1, 2, 3]).len(), 6);
}
#[test]
fn test_subsets() {
assert_eq!(generate_subsets(&[1, 2, 3]).len(), 8);
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Used flags | Vec<bool> | bool array |
| Result accumulation | &mut Vec<Vec<T>> | acc ref or continuation |
| Choose/unchoose | push/pop on Vec | Array.set true/false |
| Generic type | <T: Clone> | Parametric 'a |
| N-queens variant | Separate fn queens | Separate or same framework |
| Iterative variant | Iterator via generator | Sequence with Seq.t |
OCaml Approach
OCaml backtracking uses a used array as bool array mutated in-place. The recursive function returns unit and accumulates results via an acc ref. OCaml's functional style can also express this with continuation-passing: let rec backtrack cont current used = .... The Array.make n false initializes the used flags. OCaml's pattern matching for N-queens constraint checking reads naturally. The List.rev current at leaf nodes (if building the list from head prepending) recovers the correct order.
Full Source
#![allow(clippy::all)]
//! # Backtracking Framework
pub fn generate_permutations<T: Clone>(items: &[T]) -> Vec<Vec<T>> {
let mut result = vec![];
let mut current = vec![];
let mut used = vec![false; items.len()];
fn backtrack<T: Clone>(
items: &[T],
curr: &mut Vec<T>,
used: &mut [bool],
res: &mut Vec<Vec<T>>,
) {
if curr.len() == items.len() {
res.push(curr.clone());
return;
}
for i in 0..items.len() {
if !used[i] {
used[i] = true;
curr.push(items[i].clone());
backtrack(items, curr, used, res);
curr.pop();
used[i] = false;
}
}
}
backtrack(items, &mut current, &mut used, &mut result);
result
}
pub fn generate_subsets<T: Clone>(items: &[T]) -> Vec<Vec<T>> {
let mut result = vec![];
let mut current = vec![];
fn backtrack<T: Clone>(items: &[T], start: usize, curr: &mut Vec<T>, res: &mut Vec<Vec<T>>) {
res.push(curr.clone());
for i in start..items.len() {
curr.push(items[i].clone());
backtrack(items, i + 1, curr, res);
curr.pop();
}
}
backtrack(items, 0, &mut current, &mut result);
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_perms() {
assert_eq!(generate_permutations(&[1, 2, 3]).len(), 6);
}
#[test]
fn test_subsets() {
assert_eq!(generate_subsets(&[1, 2, 3]).len(), 8);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_perms() {
assert_eq!(generate_permutations(&[1, 2, 3]).len(), 6);
}
#[test]
fn test_subsets() {
assert_eq!(generate_subsets(&[1, 2, 3]).len(), 8);
}
}
Deep Comparison
OCaml vs Rust: Backtracking Framework
Overview
See the example.rs and example.ml files for detailed implementations.
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Type system | Hindley-Milner | Ownership + traits |
| Memory | GC | Zero-cost abstractions |
| Mutability | Explicit ref | mut keyword |
| Error handling | Option/Result | Result<T, E> |
See README.md for detailed comparison.
Exercises
Iterator interface to the backtracking framework so permutations can be consumed lazily.