Sudoku Solver Using Backtracking in DSA Rust - Time & Space Complexity
We want to understand how the time needed to solve a Sudoku puzzle changes as the puzzle size or difficulty changes.
Specifically, how does the backtracking method's work grow with the number of empty cells?
Analyze the time complexity of the following code snippet.
fn solve_sudoku(board: &mut [[u8; 9]; 9]) -> bool {
for row in 0..9 {
for col in 0..9 {
if board[row][col] == 0 {
for num in 1..=9 {
if is_valid(board, row, col, num) {
board[row][col] = num;
if solve_sudoku(board) {
return true;
}
board[row][col] = 0;
}
}
return false;
}
}
}
true
}
This code tries to fill empty cells (marked 0) with numbers 1 to 9, checking if the number fits rules, and uses backtracking to find a solution.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Trying numbers 1 to 9 in each empty cell and recursively solving the rest.
- How many times: For each empty cell, up to 9 attempts; recursion explores many combinations depending on puzzle difficulty.
As the number of empty cells increases, the number of possible number placements grows very fast.
| Input Size (empty cells) | Approx. Operations |
|---|---|
| 10 | Up to 9^10 = 3,486,784,401 tries |
| 20 | Up to 9^20 ≈ 1.21e19 tries |
| 30 | Up to 9^30 ≈ 1.21e28 tries |
Pattern observation: The work grows exponentially with the number of empty cells, making the problem much harder as empties increase.
Time Complexity: O(9^m) where m is the number of empty cells
This means the time needed can grow very fast as more cells are empty, because each empty cell can try up to 9 numbers recursively.
[X] Wrong: "The solver always tries exactly 9 options per empty cell, so it's just 9 times the number of empty cells."
[OK] Correct: The solver tries many combinations recursively, so the total tries multiply, not just add, leading to exponential growth.
Understanding this exponential growth helps you explain why backtracking can be slow and why pruning or smarter methods matter.
It shows your grasp of how recursive search explores many possibilities and why complexity matters in real problems.
"What if we added a heuristic to pick the next empty cell with the fewest valid options? How would that affect the time complexity?"
Practice
Solution
Step 1: Understand backtracking role
Backtracking tries placing numbers and removes them if they cause conflicts.Step 2: Compare options
Only To try possible numbers and undo choices if they lead to conflicts describes this trial and undo process correctly.Final Answer:
To try possible numbers and undo choices if they lead to conflicts -> Option DQuick Check:
Backtracking = Try and undo [OK]
- Thinking backtracking fills randomly
- Confusing backtracking with sorting
- Ignoring the undo step in backtracking
Solution
Step 1: Identify Rust function syntax
Rust functions start with fn, specify parameter types and return type.Step 2: Match options to Rust syntax
Only fn can_place(board: &[[u8; 9]; 9], row: usize, col: usize, num: u8) -> bool { /* code */ } uses fn and types correctly.Final Answer:
fn can_place(board: &[[u8; 9]; 9], row: usize, col: usize, num: u8) -> bool { /* code */ } -> Option AQuick Check:
Rust fn syntax = fn can_place(board: &[[u8; 9]; 9], row: usize, col: usize, num: u8) -> bool { /* code */ } [OK]
- Using JavaScript or Python syntax in Rust
- Omitting parameter types
- Not specifying return type
let mut board = [[0u8; 9]; 9];
board[0][0] = 5;
println!("{}", board[0][0]);Solution
Step 1: Understand board initialization
The board is a 9x9 grid initialized with zeros.Step 2: Check assignment and print
board[0][0] is set to 5, so printing it outputs 5.Final Answer:
5 -> Option BQuick Check:
board[0][0] = 5 prints 5 [OK]
- Assuming default zero prints after assignment
- Confusing index order
- Expecting error due to mutability
fn solve(board: &mut [[u8; 9]; 9]) -> bool {
for row in 0..9 {
for col in 0..9 {
if board[row][col] == 0 {
for num in 1..9 {
if can_place(board, row, col, num) {
board[row][col] = num;
if solve(board) {
return true;
}
board[row][col] = 0;
}
}
return false;
}
}
}
true
}Solution
Step 1: Check number range in loop
Rust range 1..9 iterates from 1 to 8 (exclusive end), missing 9 which is required for Sudoku.Step 2: Compare options
Only The range 1..9 excludes 9, should be 1..10 correctly identifies the range bug and suggests 1..10 (1 through 9).Step 3: Verify other options
A: Incorrect; returns true after loops if no empty cells. C: Wrong; mutable needed for changes. D: Unnecessary; order is fine.Final Answer:
The range 1..9 excludes 9, should be 1..10 -> Option CQuick Check:
Rust 1..9 = 1-8, use 1..10 for 1-9 [OK]
- Thinking 1..10 includes 10
- Not marking board mutable
- Swapping loops unnecessarily
Solution
Step 1: Understand backtracking validation
The algorithm tries numbers only if they don't break Sudoku rules (row, column, box).Step 2: Understand backtracking completeness
If a number leads to conflict later, it removes it (backtracks) and tries another number until the board is full.Step 3: Eliminate wrong options
Options B, C, and D describe incorrect or unrelated methods.Final Answer:
By placing numbers only if they don't violate Sudoku rules and backtracking on conflicts until the board is full -> Option AQuick Check:
Backtracking = Try-valid-undo until complete [OK]
- Thinking random fill works
- Confusing sorting with validation
- Using queues instead of recursion
Ready to prove it?
Pass the test and this pattern moves unknown
