Why Backtracking and What Greedy Cannot Solve in DSA Rust - Performance Analysis
We want to understand why some problems need backtracking instead of greedy methods.
How does the time cost grow when we try all possibilities versus making quick choices?
Analyze the time complexity of this backtracking example.
fn backtrack(nums: &Vec<i32>, path: &mut Vec<i32>, used: &mut Vec<bool>) {
if path.len() == nums.len() {
// Found one solution
return;
}
for i in 0..nums.len() {
if used[i] { continue; }
used[i] = true;
path.push(nums[i]);
backtrack(nums, path, used);
path.pop();
used[i] = false;
}
}
This code tries all possible orders of numbers by exploring every choice step-by-step.
Look at what repeats in the code.
- Primary operation: The for-loop tries each unused number at every step.
- How many times: It repeats for every position in the path, exploring all permutations.
Each step tries many options, and this multiplies quickly as input grows.
| Input Size (n) | Approx. Operations |
|---|---|
| 3 | 6 (3x2x1) |
| 5 | 120 (5x4x3x2x1) |
| 10 | 3,628,800 (10!) |
Pattern observation: The number of operations grows very fast, much faster than just doubling.
Time Complexity: O(n!)
This means the time needed grows extremely fast as input size increases, because we try every order.
[X] Wrong: "Greedy choices will always find the best solution quickly."
[OK] Correct: Greedy methods pick the best option now but can miss better overall solutions that backtracking finds by exploring all possibilities.
Understanding when to use backtracking versus greedy shows you can choose the right tool for tricky problems, a skill interviewers value.
"What if we added pruning to stop exploring bad paths early? How would the time complexity change?"
Practice
Solution
Step 1: Understand the problem nature
The N-Queens puzzle requires placing queens so none attack each other, needing exploration of many configurations.Step 2: Compare algorithm approaches
Backtracking tries all possibilities and undoes wrong choices, while greedy picks local best without reconsidering.Final Answer:
Because backtracking explores all possible solutions and can backtrack when a choice leads to failure -> Option AQuick Check:
Backtracking = explores all options [OK]
- Thinking greedy always finds the best solution
- Confusing memory use between algorithms
- Assuming backtracking is always slower
Solution
Step 1: Identify mutable reference usage
Backtracking modifies the vector, so it needs a mutable reference: &mut Vec<i32>.Step 2: Check parameter types
Parameter n is passed by value (i32), not reference, for simplicity.Final Answer:
fn backtrack(nums: &mut Vec<i32>, n: i32) { ... } -> Option DQuick Check:
Mutable reference for vector = fn backtrack(nums: &mut Vec<i32>, n: i32) { ... } [OK]
- Using &Vec<i32> which is immutable
- Passing n as &i32 unnecessarily
- Returning Vec<i32> when not needed
fn backtrack(nums: &[i32], target: i32, start: usize, current: &mut Vec<i32>) {
if target == 0 {
println!("{:?}", current);
return;
}
for i in start..nums.len() {
if nums[i] > target {
continue;
}
current.push(nums[i]);
backtrack(nums, target - nums[i], i + 1, current);
current.pop();
}
}
fn main() {
let nums = [2, 3, 6, 7];
let mut current = Vec::new();
backtrack(&nums, 7, 0, &mut current);
}Solution
Step 1: Analyze the backtracking logic
The function tries subsets of nums that sum to 7, moving forward each time (i + 1), so no repeats.Step 2: Trace possible subsets
Possible subsets: [7] sums to 7; [2,3] sums to 5; [2,3,6] exceeds; no repeats allowed, so only [7] matches exactly.Final Answer:
[[7]] -> Option CQuick Check:
Only subset summing to 7 without repeats = [7] [OK]
- Assuming repeated elements allowed
- Ignoring the target check
- Thinking [2,3,2] is valid subset
fn permute(nums: &mut Vec<i32>, start: usize) {
if start == nums.len() {
println!("{:?}", nums);
return;
}
for i in start..nums.len() {
nums.swap(start, i);
permute(nums, start + 1);
}
}Solution
Step 1: Understand permutation generation
Permutations require swapping elements and then swapping back to restore original order after recursion.Step 2: Identify missing swap back
The code swaps elements before recursion but never swaps back after, causing incorrect permutations.Final Answer:
Missing swap to revert the change after recursive call -> Option AQuick Check:
Swap back after recursion needed [OK]
- Forgetting to swap back
- Changing base case incorrectly
- Using immutable references
Solution
Step 1: Understand greedy limitations
Greedy algorithms pick the best immediate choice, like largest numbers, which can overshoot the target and miss valid subsets.Step 2: Understand backtracking advantage
Backtracking tries all possible subsets by exploring and undoing choices, ensuring it finds an exact match if one exists.Final Answer:
Greedy picks largest numbers first which may overshoot; backtracking tries all combinations to find exact match -> Option BQuick Check:
Greedy overshoot; backtracking explores all [OK]
- Thinking greedy always finds exact subset
- Confusing subset size with sum
- Mixing recursion and iteration definitions
Ready to prove it?
Pass the test and this pattern moves unknown
