Palindrome Partitioning Using Backtracking in DSA Rust - Time & Space Complexity
We want to understand how the time needed to find all palindrome partitions grows as the input string gets longer.
Specifically, how does the backtracking process scale with the string size?
Analyze the time complexity of the following code snippet.
fn is_palindrome(s: &str) -> bool {
s.chars().eq(s.chars().rev())
}
fn backtrack(s: &str, start: usize, path: &mut Vec, result: &mut Vec>) {
if start == s.len() {
result.push(path.clone());
return;
}
for end in start + 1..=s.len() {
let substring = &s[start..end];
if is_palindrome(substring) {
path.push(substring.to_string());
backtrack(s, end, path, result);
path.pop();
}
}
}
This code finds all ways to split a string into palindrome parts using backtracking.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: The for-loop tries every possible substring starting at each position.
- How many times: For each character, it tries all substrings to the end, and recursively explores further splits.
As the string length grows, the number of possible palindrome partitions grows very fast.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | Thousands of checks and recursive calls |
| 100 | Millions to billions of operations |
| 1000 | Extremely large, often impractical to compute fully |
Pattern observation: The number of partitions grows exponentially, so execution time grows very quickly as n increases.
Time Complexity: O(n * 2^n)
This means the time grows very fast, roughly doubling with each added character, multiplied by the cost to check palindromes.
[X] Wrong: "The algorithm runs in polynomial time because it just uses loops and recursion."
[OK] Correct: The number of palindrome partitions can be exponential, so the recursion explores many combinations, making the time grow very fast, not just polynomially.
Understanding this complexity helps you explain why some problems need careful pruning or memoization to run efficiently in real situations.
"What if we added memoization to store palindrome checks? How would the time complexity change?"
Practice
Solution
Step 1: Understand palindrome partitioning using backtracking
Palindrome partitioning means dividing a string into parts where each part is a palindrome. Backtracking helps explore all possible ways to split the string into palindrome parts.Final Answer:
To split a string into all possible palindrome substrings -> Option AQuick Check:
Palindrome partitioning = all palindrome splits [OK]
- Confusing palindrome partitioning with longest palindrome substring
- Thinking it only counts palindromes
- Assuming it just reverses the string
Solution
Step 1: Check function signature and verify palindrome logic
Using &str is efficient for string slices without ownership transfer. Comparing characters iterator with its reverse checks palindrome correctly.Final Answer:
fn is_palindrome(s: &str) -> bool { s.chars().eq(s.chars().rev()) } -> Option AQuick Check:
Correct Rust palindrome check syntax = fn is_palindrome(s: &str) -> bool { s.chars().eq(s.chars().rev()) } [OK]
- Using String instead of &str unnecessarily
- Calling reverse() method which does not exist on String
- Comparing string to itself without reversing
fn main() {
let s = "aab";
let mut result = vec![];
fn backtrack(s: &str, start: usize, path: &mut Vec<String>, result: &mut Vec) {
if start == s.len() {
result.push(path.clone());
return;
}
for end in start+1..=s.len() {
let substr = &s[start..end];
if substr.chars().eq(substr.chars().rev()) {
path.push(substr.to_string());
backtrack(s, end, path, result);
path.pop();
}
}
}
backtrack(s, 0, &mut vec![], &mut result);
println!("{:?}", result);
}Solution
Step 1: Trace backtracking on "aab"
Partitions are checked for palindrome substrings starting from index 0. "a" is palindrome, then "a" again, then "b"; also "aa" is palindrome, then "b". Output: [["a", "a", "b"], ["aa", "b"]].Final Answer:
[["a", "a", "b"], ["aa", "b"]] -> Option CQuick Check:
All palindrome splits of "aab" = [["a", "a", "b"], ["aa", "b"]] [OK]
- Missing the "aa" palindrome substring
- Including non-palindrome substrings like "ab"
- Confusing output format
fn backtrack(s: &str, start: usize, path: &mut Vec<String>, result: &mut Vec) {
if start > s.len() {
result.push(path.clone());
return;
}
for end in start+1..=s.len() {
let substr = &s[start..end];
if substr.chars().eq(substr.chars().rev()) {
path.push(substr.to_string());
backtrack(s, end, path, result);
path.pop();
}
}
}Solution
Step 1: Analyze base case and why it fails
Backtracking ends when start reaches string length exactly, not greater. If start > s.len(), it means out of bounds, so base case never triggers correctly.Final Answer:
The base case condition should be start == s.len(), not start > s.len() -> Option DQuick Check:
Base case must match string length exactly [OK]
- Using start > s.len() causes missed base case
- Changing loop range incorrectly
- Unnecessary cloning or wrong substring checks
"aaaaa", how many palindrome partitions will the backtracking algorithm find if it returns all possible palindrome partitions?Solution
Step 1: Analyze all-'a' string partitions
Since all characters are identical 'a', every substring is a palindrome. The number of partitions equals the ways to place cuts in the 4 gaps between 5 characters: 2^4 = 16.Final Answer:
16 -> Option BQuick Check:
2^{n-1} = 16 for n=5 [OK]
- Counting only large palindrome substrings
- Ignoring smaller palindrome partitions
- Underestimating total partition count
Ready to prove it?
Pass the test and this pattern moves unknown
