🦀 Functional Rust

841: Backtracking — Generic Recursive Framework with Pruning

Difficulty: 3 Level: Intermediate Enumerate all valid solutions by building candidates incrementally and abandoning dead-end branches early — the algorithmic engine behind N-queens, Sudoku solvers, and combinatorial optimization.

The Problem This Solves

Many problems require finding all (or the best) solution in a combinatorial search space: N-queens placement, Sudoku, graph coloring, subset sum, scheduling with constraints. Brute force generates all candidates then filters — O(n! × n) for permutations. Backtracking does better by pruning: if a partial assignment already violates constraints, abandon it immediately without completing it. This is not just an academic exercise. Sudoku solvers, SAT solvers (DPLL algorithm), constraint satisfaction in AI planning, and automatic theorem provers are all backtracking with increasingly sophisticated pruning strategies. The difference between a 1ms and a 1s Sudoku solve is almost entirely in the quality of the pruning predicate. The generic framework makes the pattern explicit: try each choice, check the constraint predicate after each extension (not just at leaves), recurse, then undo the choice (backtrack). Implemented in Rust with mutable `Vec` state passed by `&mut` reference — no heap allocation per recursive call.

The Intuition

Think of it as tree search: each node is a partial solution, branches are choices at the next position. DFS with pruning: at each node, check if the current partial solution is still consistent. If not, don't explore any of its subtrees — that's the prune. If yes, extend to all children. At leaf nodes (full assignment), collect the solution. The key insight: check constraints at every extension, not just at the end. For N-queens, check whether placing a queen in column `col` at row `row` conflicts with any already-placed queen. This eliminates entire subtrees of size (n-row)! at each prune.

How It Works in Rust

// Constraint check: can we place a queen at (row, col)?
fn is_safe(board: &[usize], row: usize, col: usize) -> bool {
 for r in 0..row {
     let c = board[r];
     // Conflicts: same column, or same diagonal
     if c == col || c.abs_diff(col) == r.abs_diff(row) {
         return false;
     }
 }
 true
}

// N-Queens: board[row] = column of queen in that row
fn n_queens(n: usize) -> Vec<Vec<usize>> {
 let mut solutions = Vec::new();
 let mut board = vec![0usize; n];
 n_queens_rec(n, 0, &mut board, &mut solutions);
 solutions
}

fn n_queens_rec(n: usize, row: usize, board: &mut Vec<usize>, solutions: &mut Vec<Vec<usize>>) {
 if row == n {
     solutions.push(board.clone());  // Complete solution: collect
     return;
 }
 for col in 0..n {
     if is_safe(board, row, col) {
         board[row] = col;                              // Choose
         n_queens_rec(n, row + 1, board, solutions);   // Explore
         // Backtrack: board[row] will be overwritten in next iteration
         // (explicit undo only needed if state isn't overwritten)
     }
 }
}

// Permutations: explicit undo via used[] flag
fn permutations_rec<T: Clone>(xs: &[T], current: &mut Vec<T>,
                            used: &mut Vec<bool>, result: &mut Vec<Vec<T>>) {
 if current.len() == xs.len() { result.push(current.clone()); return; }
 for i in 0..xs.len() {
     if !used[i] {
         used[i] = true;          // Choose
         current.push(xs[i].clone());
         permutations_rec(xs, current, used, result);  // Explore
         current.pop();           // Undo choice
         used[i] = false;         // Undo flag — explicit backtrack
     }
 }
}
The `&mut Vec` pattern — passing mutable collections by reference rather than returning them — is idiomatic Rust for recursive algorithms that accumulate results. It avoids cloning partial solutions on every recursive call.

What This Unlocks

Key Differences

ConceptOCamlRust
Mutable accumulator`ref` list or `Buffer``&mut Vec` passed down — explicit
Board state`int list` (functional, no undo needed)`Vec<usize>` with in-place update
Undo stepFunctional: no undo (immutable state)`current.pop(); used[i] = false;`
Collect solutions`results := sol :: !results``solutions.push(board.clone())`
Constraint checkPure function on listPure function on slice — identical style
/// Backtracking: Generic Recursive Framework with Pruning.
///
/// Demonstrated with N-Queens and permutation generation.
/// Pattern: try choice → check constraint → recurse → undo (backtrack).

/// Check if placing a queen at (row, col) is safe.
fn is_safe(board: &[usize], row: usize, col: usize) -> bool {
    for r in 0..row {
        let c = board[r];
        if c == col || c.abs_diff(col) == r.abs_diff(row) {
            return false;
        }
    }
    true
}

/// N-Queens: find all solutions. board[row] = column of queen.
fn n_queens(n: usize) -> Vec<Vec<usize>> {
    let mut solutions = Vec::new();
    let mut board = vec![0usize; n];
    n_queens_rec(n, 0, &mut board, &mut solutions);
    solutions
}

fn n_queens_rec(n: usize, row: usize, board: &mut Vec<usize>, solutions: &mut Vec<Vec<usize>>) {
    if row == n {
        solutions.push(board.clone());
        return;
    }
    for col in 0..n {
        if is_safe(board, row, col) {
            board[row] = col;                             // choose
            n_queens_rec(n, row + 1, board, solutions);  // explore
            // board[row] = 0; -- undo (implicit, will be overwritten)
        }
    }
}

/// Generate all permutations of a slice.
fn permutations<T: Clone>(xs: &[T]) -> Vec<Vec<T>> {
    let mut result = Vec::new();
    let mut current = Vec::new();
    let mut used = vec![false; xs.len()];
    permutations_rec(xs, &mut current, &mut used, &mut result);
    result
}

fn permutations_rec<T: Clone>(
    xs: &[T],
    current: &mut Vec<T>,
    used: &mut Vec<bool>,
    result: &mut Vec<Vec<T>>,
) {
    if current.len() == xs.len() {
        result.push(current.clone());
        return;
    }
    for i in 0..xs.len() {
        if !used[i] {
            used[i] = true;
            current.push(xs[i].clone());  // choose
            permutations_rec(xs, current, used, result);
            current.pop();                // undo
            used[i] = false;              // undo
        }
    }
}

/// Subset sum: does any subset of nums sum to target?
fn subset_sum(nums: &[i64], target: i64) -> bool {
    fn bt(nums: &[i64], idx: usize, remaining: i64) -> bool {
        if remaining == 0 { return true; }
        if idx == nums.len() || remaining < 0 { return false; }
        // Include nums[idx] or skip it
        bt(nums, idx + 1, remaining - nums[idx]) || bt(nums, idx + 1, remaining)
    }
    bt(nums, 0, target)
}

fn print_board(board: &[usize]) {
    let n = board.len();
    for &col in board {
        let row: String = (0..n).map(|c| if c == col { 'Q' } else { '.' }).collect();
        println!("{row}");
    }
    println!();
}

fn main() {
    let sols = n_queens(4);
    println!("4-Queens: {} solutions", sols.len());
    print_board(&sols[0]);

    println!("8-Queens: {} solutions", n_queens(8).len()); // 92

    let mut perms = permutations(&[1, 2, 3]);
    perms.sort();
    println!("permutations([1,2,3]): {} (expected 6)", perms.len());
    for p in &perms { println!("  {p:?}"); }

    println!("subset_sum([3,1,4,1,5], 9): {}", subset_sum(&[3, 1, 4, 1, 5], 9));
    println!("subset_sum([3,1,4,1,5], 11): {}", subset_sum(&[3, 1, 4, 1, 5], 11));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_4_queens_count() {
        assert_eq!(n_queens(4).len(), 2);
    }

    #[test]
    fn test_8_queens_count() {
        assert_eq!(n_queens(8).len(), 92);
    }

    #[test]
    fn test_queens_valid() {
        for board in n_queens(8) {
            let n = board.len();
            // Every row has exactly one queen
            for row in 0..n {
                for other in 0..n {
                    if row != other {
                        assert_ne!(board[row], board[other], "column clash");
                        assert_ne!(board[row].abs_diff(board[other]),
                                   row.abs_diff(other), "diagonal clash");
                    }
                }
            }
        }
    }

    #[test]
    fn test_permutations_count() {
        assert_eq!(permutations(&[1, 2, 3]).len(), 6);
        assert_eq!(permutations(&[1, 2, 3, 4]).len(), 24);
    }

    #[test]
    fn test_permutations_content() {
        let mut perms = permutations(&[1, 2, 3]);
        perms.sort();
        assert_eq!(perms[0], vec![1, 2, 3]);
        assert_eq!(perms[5], vec![3, 2, 1]);
    }

    #[test]
    fn test_subset_sum() {
        assert!(subset_sum(&[3, 1, 4, 1, 5], 9));    // 3+1+5=9
        assert!(!subset_sum(&[3, 1, 4, 1, 5], 11)); // no subset sums to 11
        assert!(subset_sum(&[3, 1, 4, 1, 5], 5));    // 5 itself
    }
}
(* Backtracking Framework in OCaml — N-Queens and Permutations *)

(* N-Queens: board.(row) = column of queen in that row *)
(* Check if placing a queen at (row, col) is safe given placements so far *)
let is_safe (board : int array) (row : int) (col : int) : bool =
  let ok = ref true in
  for r = 0 to row - 1 do
    let c = board.(r) in
    if c = col || abs (c - col) = abs (r - row) then
      ok := false
  done;
  !ok

(* Solve N-Queens: returns all solutions *)
let n_queens (n : int) : int array list =
  let board = Array.make n 0 in
  let solutions = ref [] in
  let rec solve row =
    if row = n then
      solutions := Array.copy board :: !solutions
    else
      for col = 0 to n - 1 do
        if is_safe board row col then begin
          board.(row) <- col;
          solve (row + 1);
          (* backtrack: no explicit undo needed as we overwrite *)
        end
      done
  in
  solve 0;
  !solutions

(* Print a board *)
let print_board (board : int array) =
  Array.iter (fun col ->
    let row = String.init (Array.length board) (fun c -> if c = col then 'Q' else '.') in
    print_string (row ^ "\n")
  ) board;
  print_newline ()

(* Generic permutation via backtracking *)
let permutations (xs : 'a list) : 'a list list =
  let result = ref [] in
  let n = List.length xs in
  let arr = Array.of_list xs in
  let used = Array.make n false in
  let current = Array.make n (List.hd xs) in
  let rec gen depth =
    if depth = n then
      result := Array.to_list current :: !result
    else
      for i = 0 to n - 1 do
        if not used.(i) then begin
          used.(i) <- true;
          current.(depth) <- arr.(i);
          gen (depth + 1);
          used.(i) <- false  (* backtrack *)
        end
      done
  in
  gen 0;
  !result

let () =
  let sols = n_queens 4 in
  Printf.printf "4-Queens: %d solutions\n" (List.length sols);
  print_board (List.hd sols);
  Printf.printf "8-Queens: %d solutions\n" (List.length (n_queens 8));
  let perms = permutations [1; 2; 3] in
  Printf.printf "permutations([1,2,3]): %d\n" (List.length perms);
  List.iter (fun p ->
    Printf.printf "  [%s]\n" (String.concat "," (List.map string_of_int p))
  ) (List.sort compare perms)