210. Palindrome Partitioning
π LeetCode Problem: 131. Palindrome Partitioning
π Difficulty: Medium
π·οΈ Topics: Dynamic Programming, Backtracking, String, 1D DP
Problem Statement
Given a string s, partition s such that every substring of the partition is a palindrome. Return all possible palindrome partitioning of s.
Note: The same word in the dictionary may be reused multiple times in the segmentation.
Example 1:
Input: s = "aab"
Output: [["a","a","b"],["aa","b"]]
Example 2:
Input: s = "a"
Output: [["a"]]
Constraints:
- 1 <= s.length <= 16
- s contains only lowercase English letters.
π³ Visual Understanding - Using "aabb"
The Problem We're Solving:
Given: String "aabb"
Goal: Find ALL ways to partition into palindromes
Palindrome: Reads same forwards and backwards
"a" β palindrome β
"aa" β palindrome β
"aab" β NOT palindrome β
"b" β palindrome β
"bb" β palindrome β
"aabb" β NOT palindrome β
Question: How many different ways can we split "aabb"? π€
Finding All Valid Partitions for "aabb":
String: "aabb"
Partition 1: ["a", "a", "b", "b"]
β "a" is palindrome
β "a" is palindrome
β "b" is palindrome
β "b" is palindrome
VALID! β
Partition 2: ["a", "a", "bb"]
β "a" is palindrome
β "a" is palindrome
β "bb" is palindrome
VALID! β
Partition 3: ["aa", "b", "b"]
β "aa" is palindrome
β "b" is palindrome
β "b" is palindrome
VALID! β
Partition 4: ["aa", "bb"]
β "aa" is palindrome
β "bb" is palindrome
VALID! β
Invalid Partition: ["aab", "b"]
β "aab" is NOT palindrome
INVALID! β
Invalid Partition: ["aabb"]
β "aabb" is NOT palindrome
INVALID! β
Total: 4 valid partitions
Answer: [["a","a","b","b"], ["a","a","bb"], ["aa","b","b"], ["aa","bb"]]
Complete Decision Tree for "aabb":
"aabb"
|
+-----------------+------------------+
| | |
Take "a" Take "aa" Take "aab"
(palin β) (palin β) (palin β)
| | DEAD END
v v
"abb" "bb"
| |
+----+----+ +----+----+
| | | |
"a" "ab" "b" "bb"
(palinβ) (palinβ) (palinβ) (palinβ)
| DEAD | |
v v v
"bb" "b" ""
| (palinβ) [Path 4]
| | ["aa","bb"] β
| v
| ""
| [Path 3]
| ["aa","b","b"] β
|
+--+--+
| |
"b" "bb"
(β) (β)
| |
v v
"b" ""
(β) [Path 2]
| ["a","a","bb"] β
v
""
[Path 1]
["a","a","b","b"] β
Total: 4 valid partitions found!
Why This Is Interesting:
Same string, MULTIPLE valid ways to partition!
"aabb" can be split as:
1. Four single chars
2. Two singles + one double
3. One double + two singles
4. Two doubles
Each partition has ALL palindromic substrings!
This is what makes the problem challenging! π―
π§ The AHA Moment - Why This Problem Needs Special Approach
What Makes This Hard?
CHALLENGE 1: We need to find ALL valid partitions
- Not just count them
- Not just check if possible
- Actually GENERATE every valid partition!
CHALLENGE 2: Checking palindromes is expensive
- For "aabb", we check:
- "a" (index 0-0)
- "aa" (index 0-1)
- "aab" (index 0-2)
- "aabb" (index 0-3)
- "a" (index 1-1) β REPEATED!
- "ab" (index 1-2)
- "abb" (index 1-3)
- ... many more!
Same substrings checked multiple times! β οΈ
CHALLENGE 3: Backtracking is necessary
- Must try every possible cut position
- Must validate each choice
- Must collect all valid paths
This needs BOTH Backtracking AND DP! π―
The Key Insight:
TWO-PHASE APPROACH:
PHASE 1 (DP): Precompute which substrings are palindromes
Build a table: dp[i][j] = is s[i..j] a palindrome?
Do this ONCE, use many times!
Time: O(nΒ²)
PHASE 2 (Backtracking): Generate all valid partitions
At each position, try all palindromic cuts
Use precomputed table for O(1) palindrome check
Collect all complete valid partitions
Time: O(n Γ 2^n)
Why this works:
- DP eliminates repeated palindrome checks
- Backtracking systematically explores all possibilities
- Combined = optimal solution! π
π΄ Approach 1: Backtracking Only (No DP Optimization)
π Function Definition
Function Signature:
List<List<String>> partition(String s)
void backtrack(String s, int start, List<String> current, List<List<String>> result)
boolean isPalindrome(String s, int left, int right)
What it represents:
Input Parameter s:
- The string to partition
- Example: s = "aabb"
Input Parameter start:
- Current position where we're making partition decision
- Example: start=2 means "decide how to partition from index 2"
Input Parameter current:
- Current partition being built (list of palindromic substrings found so far)
- Example: ["a", "a"] means we've partitioned first two characters
Input Parameter result:
- Collection of all complete valid partitions
Return Value: - List of all valid palindrome partitions
Purpose: - Try every possible way to cut the string - Check if each piece is a palindrome - Collect all valid complete partitions
Key Understanding:
backtrack("aabb", 0, [], result) means:
"Find all ways to partition 'aabb' starting from index 0"
At index 0 with string "aabb":
Try: cut after 1 char β "a" + recurse on "abb"
Try: cut after 2 chars β "aa" + recurse on "bb"
Try: cut after 3 chars β "aab" + recurse on "b"
Try: cut after 4 chars β "aabb" + done
For each cut that creates a palindrome, explore further
π‘ Intuition
The intuition is simple: Try every possible way to make the first cut, then recursively solve the rest.
Think of it like cutting a rope:
"aabb" - where should I make the first cut?
Option 1: Cut after "a"
β Got "a" (palindrome β)
β Now solve "abb"
Option 2: Cut after "aa"
β Got "aa" (palindrome β)
β Now solve "bb"
Option 3: Cut after "aab"
β Got "aab" (NOT palindrome β)
β Don't explore this path
Option 4: Cut after "aabb"
β Got "aabb" (NOT palindrome β)
β Don't explore this path
For each valid first cut, recursively find all ways
to partition what's left!
This naturally explores ALL possible partition combinations!
The Backtracking Pattern:
At each position:
1. TRY: all possible cuts from this position
2. CHECK: is the substring a palindrome?
3. CHOOSE: if YES, add it to current partition
4. EXPLORE: recursively partition the rest
5. UNCHOOSE: remove what we added (backtrack)
6. COLLECT: when we reach the end, save the partition
This is the classic "Choose-Explore-Unchoose" pattern!
π Implementation
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> result = new ArrayList<>();
backtrack(s, 0, new ArrayList<>(), result);
return result;
}
private void backtrack(String s, int start, List<String> current,
List<List<String>> result) {
// Base case: reached end of string
if (start == s.length()) {
result.add(new ArrayList<>(current)); // Save this partition
return;
}
// Try all possible cuts from current position
for (int end = start; end < s.length(); end++) {
// Check if substring from start to end is palindrome
if (isPalindrome(s, start, end)) {
// CHOOSE: Add this palindrome to current partition
current.add(s.substring(start, end + 1));
// EXPLORE: Recursively partition the rest
backtrack(s, end + 1, current, result);
// UNCHOOSE: Remove last added (backtrack)
current.remove(current.size() - 1);
}
}
}
private boolean isPalindrome(String s, int left, int right) {
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
}
π Detailed Dry Run: s = "aabb"
backtrack("aabb", 0, [], result)
β
ββ Try cut at position 0 (substring "a")
β ββ isPalindrome("aabb", 0, 0) β "a" β TRUE β
β ββ current = ["a"]
β ββ backtrack("aabb", 1, ["a"], result)
β β β
β β ββ Try cut at position 1 (substring "a")
β β β ββ isPalindrome("aabb", 1, 1) β "a" β TRUE β
β β β ββ current = ["a", "a"]
β β β ββ backtrack("aabb", 2, ["a","a"], result)
β β β β β
β β β β ββ Try cut at position 2 (substring "b")
β β β β β ββ isPalindrome("aabb", 2, 2) β "b" β TRUE β
β β β β β ββ current = ["a", "a", "b"]
β β β β β ββ backtrack("aabb", 3, ["a","a","b"], result)
β β β β β β β
β β β β β β ββ Try cut at position 3 (substring "b")
β β β β β β β ββ isPalindrome("aabb", 3, 3) β "b" β TRUE β
β β β β β β β ββ current = ["a", "a", "b", "b"]
β β β β β β β ββ backtrack("aabb", 4, ["a","a","b","b"], result)
β β β β β β β β ββ start == length β FOUND PARTITION!
β β β β β β β β result.add(["a","a","b","b"]) β
β β β β β β β ββ UNCHOOSE: current = ["a", "a", "b"]
β β β β β β ββ return
β β β β β ββ UNCHOOSE: current = ["a", "a"]
β β β β β
β β β β ββ Try cut at position 2-3 (substring "bb")
β β β β β ββ isPalindrome("aabb", 2, 3) β "bb" β TRUE β
β β β β β ββ current = ["a", "a", "bb"]
β β β β β ββ backtrack("aabb", 4, ["a","a","bb"], result)
β β β β β β ββ start == length β FOUND PARTITION!
β β β β β β result.add(["a","a","bb"]) β
β β β β β ββ UNCHOOSE: current = ["a", "a"]
β β β β ββ return
β β β ββ UNCHOOSE: current = ["a"]
β β β
β β ββ Try cut at position 1-2 (substring "ab")
β β β ββ isPalindrome("aabb", 1, 2) β "ab" β FALSE β
β β β ββ Skip this cut
β β β
β β ββ Try cut at position 1-3 (substring "abb")
β β β ββ isPalindrome("aabb", 1, 3) β "abb" β FALSE β
β β β ββ Skip this cut
β β ββ return
β ββ UNCHOOSE: current = []
β
ββ Try cut at position 0-1 (substring "aa")
β ββ isPalindrome("aabb", 0, 1) β "aa" β TRUE β
β ββ current = ["aa"]
β ββ backtrack("aabb", 2, ["aa"], result)
β β β
β β ββ Try cut at position 2 (substring "b")
β β β ββ isPalindrome("aabb", 2, 2) β "b" β TRUE β
β β β ββ current = ["aa", "b"]
β β β ββ backtrack("aabb", 3, ["aa","b"], result)
β β β β β
β β β β ββ Try cut at position 3 (substring "b")
β β β β β ββ isPalindrome("aabb", 3, 3) β "b" β TRUE β
β β β β β ββ current = ["aa", "b", "b"]
β β β β β ββ backtrack("aabb", 4, ["aa","b","b"], result)
β β β β β β ββ start == length β FOUND PARTITION!
β β β β β β result.add(["aa","b","b"]) β
β β β β β ββ UNCHOOSE: current = ["aa", "b"]
β β β β ββ return
β β β ββ UNCHOOSE: current = ["aa"]
β β β
β β ββ Try cut at position 2-3 (substring "bb")
β β β ββ isPalindrome("aabb", 2, 3) β "bb" β TRUE β
β β β ββ current = ["aa", "bb"]
β β β ββ backtrack("aabb", 4, ["aa","bb"], result)
β β β β ββ start == length β FOUND PARTITION!
β β β β result.add(["aa","bb"]) β
β β β ββ UNCHOOSE: current = ["aa"]
β β ββ return
β ββ UNCHOOSE: current = []
β
ββ Try cut at position 0-2 (substring "aab")
β ββ isPalindrome("aabb", 0, 2) β "aab" β FALSE β
β ββ Skip this cut
β
ββ Try cut at position 0-3 (substring "aabb")
β ββ isPalindrome("aabb", 0, 3) β "aabb" β FALSE β
β ββ Skip this cut
β
ββ Final result = [
["a","a","b","b"],
["a","a","bb"],
["aa","b","b"],
["aa","bb"]
]
Total: 4 valid partitions found! β
Counting Palindrome Checks:
Notice how many times we check palindromes:
"a" (index 0-0): checked once
"a" (index 1-1): checked once
"aa" (index 0-1): checked once
"ab" (index 1-2): checked once
"aab" (index 0-2): checked once
"abb" (index 1-3): checked once
"aabb" (index 0-3): checked once
"b" (index 2-2): checked TWICE! β οΈ
"b" (index 3-3): checked TWICE! β οΈ
"bb" (index 2-3): checked TWICE! β οΈ
Some substrings checked multiple times!
This is inefficient but works correctly.
π Complexity Analysis
Time Complexity: O(n Γ 2^n)
At each position, we decide: cut here or don't cut
This creates 2^n possible partition combinations
For each partition:
- Extract substrings: O(n)
- Check palindromes: O(n) per check, multiple checks
Total: O(n Γ 2^n) for exploring all partitions
Plus: Repeated palindrome checks add overhead
Space Complexity: O(n)
Recursion depth: O(n) worst case
Current partition storage: O(n)
Total: O(n)
The Problem:
β
Finds all valid partitions correctly
β
Logic is straightforward
β Checks same substrings for palindrome multiple times
β Inefficient for larger strings
π Can optimize with DP precomputation!
π‘ Approach 2: Backtracking + Top-Down DP (Memoized Palindrome Checks)
π Function Definition
Function Signature:
void backtrack(String s, int start, List<String> current,
List<List<String>> result, Boolean[][] dp)
boolean isPalindrome(String s, int left, int right, Boolean[][] dp)
What it represents:
Input Parameter dp (Boolean[][]):
- Memoization table for palindrome results
- dp[i][j] = is substring s[i..j] a palindrome?
- null = not yet checked
- true = is palindrome
- false = not palindrome
Purpose: - Same backtracking as Approach 1 - BUT cache palindrome check results - Avoid checking same substring twice
Key Understanding:
First time checking s[2..3] = "bb":
- dp[2][3] = null (not checked yet)
- Calculate: 'b' == 'b' β true
- Store: dp[2][3] = true
- Return: true
Second time checking s[2..3] = "bb":
- dp[2][3] = true (already know!)
- Return immediately: true
- No recalculation needed! β¨
π‘ Intuition
The idea is simple: Don't check the same thing twice!
OBSERVATION:
In Approach 1, we checked "bb" twice
We checked "b" twice at different positions
This is wasteful!
SOLUTION:
First time we check a substring:
- Calculate if it's palindrome
- Remember the result in dp[i][j]
Next time we need same substring:
- Look up dp[i][j]
- Return immediately
- No recalculation! π
This is "memoization" - remember results to avoid recomputation!
π Implementation
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> result = new ArrayList<>();
Boolean[][] dp = new Boolean[s.length()][s.length()];
backtrack(s, 0, new ArrayList<>(), result, dp);
return result;
}
private void backtrack(String s, int start, List<String> current,
List<List<String>> result, Boolean[][] dp) {
if (start == s.length()) {
result.add(new ArrayList<>(current));
return;
}
for (int end = start; end < s.length(); end++) {
// Check palindrome with memoization
if (isPalindrome(s, start, end, dp)) {
current.add(s.substring(start, end + 1));
backtrack(s, end + 1, current, result, dp);
current.remove(current.size() - 1);
}
}
}
private boolean isPalindrome(String s, int left, int right, Boolean[][] dp) {
// Check if already calculated
if (dp[left][right] != null) {
return dp[left][right]; // Return cached result!
}
// Base cases
if (left >= right) {
dp[left][right] = true;
return true;
}
// Calculate and store
if (s.charAt(left) == s.charAt(right)) {
dp[left][right] = isPalindrome(s, left + 1, right - 1, dp);
} else {
dp[left][right] = false;
}
return dp[left][right];
}
}
π Complexity Analysis
Time Complexity: O(n Γ 2^n)
Same backtracking complexity
But palindrome checks are now O(1) after first check!
Much faster in practice!
Space Complexity: O(nΒ²)
DP table: O(nΒ²) to store all substring results
Recursion: O(n)
Total: O(nΒ²)
π’ Approach 3: Backtracking + Bottom-Up DP (Precomputed Palindrome Table)
π Function Definition
DP Array Definition:
boolean[][] dp = new boolean[n][n];
What dp[i][j] represents:
Indices i and j:
- Substring boundaries in the string
- dp[i][j] represents s[i..j]
- Example: dp[0][1] represents s[0..1] = "aa" in "aabb"
Value dp[i][j] (boolean):
- true = substring s[i..j] IS a palindrome
- false = substring s[i..j] is NOT a palindrome
Purpose: - Build COMPLETE palindrome table BEFORE backtracking - Then use O(1) lookups during backtracking - No recursive palindrome checking needed!
Key Understanding:
For "aabb", the complete table tells us:
dp[0][0] = true β "a" is palindrome
dp[0][1] = true β "aa" is palindrome
dp[0][2] = false β "aab" is NOT palindrome
dp[0][3] = false β "aabb" is NOT palindrome
dp[1][1] = true β "a" is palindrome
dp[1][2] = false β "ab" is NOT palindrome
dp[1][3] = false β "abb" is NOT palindrome
dp[2][2] = true β "b" is palindrome
dp[2][3] = true β "bb" is palindrome
dp[3][3] = true β "b" is palindrome
Build this once, use many times!
π‘ Intuition
The most intuitive approach: Know all palindromes upfront, then explore partitions!
Think of it like solving a puzzle:
PREPARATION PHASE:
Before starting the puzzle, organize all pieces
Check which substrings are palindromes
Store this info in a table
SOLUTION PHASE:
Now solve the puzzle using the organized info
At each step, instantly know if a piece fits (palindrome check)
No need to verify each time!
This is the CLEANEST approach:
1. Build palindrome table (DP)
2. Use table for backtracking
Clear separation of concerns! π―
How to Build the Palindrome Table:
Key insight: Build by increasing length!
Length 1: All single characters are palindromes
dp[0][0] = true ("a")
dp[1][1] = true ("a")
dp[2][2] = true ("b")
dp[3][3] = true ("b")
Length 2: Two chars palindrome if they're equal
dp[0][1] = (s[0] == s[1]) = ('a' == 'a') = true ("aa")
dp[1][2] = (s[1] == s[2]) = ('a' == 'b') = false ("ab")
dp[2][3] = (s[2] == s[3]) = ('b' == 'b') = true ("bb")
Length 3: Check ends equal AND inner is palindrome
dp[0][2] = (s[0] == s[2]) AND dp[1][1]
= ('a' == 'b') = false ("aab")
dp[1][3] = (s[1] == s[3]) AND dp[2][2]
= ('a' == 'b') = false ("abb")
Length 4: Check ends equal AND inner is palindrome
dp[0][3] = (s[0] == s[3]) AND dp[1][2]
= ('a' == 'b') = false ("aabb")
Building by length ensures inner palindromes are ready!
π― Base Cases Transformation
Why build by length?
For dp[i][j], we need to know dp[i+1][j-1]
(Inner substring must be checked first!)
Example: To know if "aabb" is palindrome
Need to know: s[0] == s[3]? ('a' == 'b'? NO)
Also need: Is inner "ab" palindrome? (dp[1][2])
So dp[1][2] must be computed BEFORE dp[0][3]!
Building by increasing length guarantees this!
Length 1 β Length 2 β Length 3 β Length 4
This way, all dependencies are satisfied! π―
π Implementation
class Solution {
public List<List<String>> partition(String s) {
int n = s.length();
// PHASE 1: Build palindrome table
boolean[][] dp = new boolean[n][n];
for (int length = 1; length <= n; length++) {
for (int i = 0; i <= n - length; i++) {
int j = i + length - 1;
if (length == 1) {
// Single character - always palindrome
dp[i][j] = true;
} else if (length == 2) {
// Two characters - palindrome if equal
dp[i][j] = (s.charAt(i) == s.charAt(j));
} else {
// Longer substring - check ends AND inner
dp[i][j] = (s.charAt(i) == s.charAt(j)) && dp[i + 1][j - 1];
}
}
}
// PHASE 2: Backtrack using precomputed table
List<List<String>> result = new ArrayList<>();
backtrack(s, 0, new ArrayList<>(), result, dp);
return result;
}
private void backtrack(String s, int start, List<String> current,
List<List<String>> result, boolean[][] dp) {
if (start == s.length()) {
result.add(new ArrayList<>(current));
return;
}
for (int end = start; end < s.length(); end++) {
// O(1) palindrome lookup!
if (dp[start][end]) {
current.add(s.substring(start, end + 1));
backtrack(s, end + 1, current, result, dp);
current.remove(current.size() - 1);
}
}
}
}
π Detailed Dry Run: s = "aabb"
Input: s = "aabb", n = 4
ββββββββββββββββββββββββββββββββββββββββββββββββ
PHASE 1: BUILD PALINDROME TABLE
ββββββββββββββββββββββββββββββββββββββββββββββββ
Initial empty table:
0 1 2 3
0 [ ? ][ ? ][ ? ][ ? ]
1 [ ? ][ ? ][ ? ]
2 [ ? ][ ? ]
3 [ ? ]
Length 1 (single characters):
βββββββββββββββββββββββββββ
i=0, j=0: s[0]='a' β dp[0][0] = true
i=1, j=1: s[1]='a' β dp[1][1] = true
i=2, j=2: s[2]='b' β dp[2][2] = true
i=3, j=3: s[3]='b' β dp[3][3] = true
0 1 2 3
0 [ T ][ ? ][ ? ][ ? ]
1 [ T ][ ? ][ ? ]
2 [ T ][ ? ]
3 [ T ]
Length 2 (two characters):
βββββββββββββββββββββββββββ
i=0, j=1: s[0]='a', s[1]='a' β equal β dp[0][1] = true β
i=1, j=2: s[1]='a', s[2]='b' β NOT equal β dp[1][2] = false β
i=2, j=3: s[2]='b', s[3]='b' β equal β dp[2][3] = true β
0 1 2 3
0 [ T ][ T ][ ? ][ ? ]
1 [ T ][ F ][ ? ]
2 [ T ][ T ]
3 [ T ]
Length 3 (three characters):
βββββββββββββββββββββββββββ
i=0, j=2: s[0]='a', s[2]='b' β NOT equal β dp[0][2] = false β
i=1, j=3: s[1]='a', s[3]='b' β NOT equal β dp[1][3] = false β
0 1 2 3
0 [ T ][ T ][ F ][ ? ]
1 [ T ][ F ][ F ]
2 [ T ][ T ]
3 [ T ]
Length 4 (four characters):
βββββββββββββββββββββββββββ
i=0, j=3: s[0]='a', s[3]='b' β NOT equal β dp[0][3] = false β
Final palindrome table:
0 1 2 3
0 [ T ][ T ][ F ][ F ] β "a", "aa", "aab", "aabb"
1 [ T ][ F ][ F ] β "a", "ab", "abb"
2 [ T ][ T ] β "b", "bb"
3 [ T ] β "b"
Reading the table:
dp[0][0] = true β "a" is palindrome β
dp[0][1] = true β "aa" is palindrome β
dp[0][2] = false β "aab" is NOT palindrome β
dp[0][3] = false β "aabb" is NOT palindrome β
dp[1][1] = true β "a" is palindrome β
dp[1][2] = false β "ab" is NOT palindrome β
dp[1][3] = false β "abb" is NOT palindrome β
dp[2][2] = true β "b" is palindrome β
dp[2][3] = true β "bb" is palindrome β
dp[3][3] = true β "b" is palindrome β
ββββββββββββββββββββββββββββββββββββββββββββββββ
PHASE 2: BACKTRACKING (Using Precomputed Table)
ββββββββββββββββββββββββββββββββββββββββββββββββ
backtrack(0, [])
Check dp[0][0]=true β Add "a"
backtrack(1, ["a"])
Check dp[1][1]=true β Add "a"
backtrack(2, ["a","a"])
Check dp[2][2]=true β Add "b"
backtrack(3, ["a","a","b"])
Check dp[3][3]=true β Add "b"
backtrack(4, ["a","a","b","b"])
Reached end β Add ["a","a","b","b"] to result β
Check dp[2][3]=true β Add "bb"
backtrack(4, ["a","a","bb"])
Reached end β Add ["a","a","bb"] to result β
Check dp[0][1]=true β Add "aa"
backtrack(2, ["aa"])
Check dp[2][2]=true β Add "b"
backtrack(3, ["aa","b"])
Check dp[3][3]=true β Add "b"
backtrack(4, ["aa","b","b"])
Reached end β Add ["aa","b","b"] to result β
Check dp[2][3]=true β Add "bb"
backtrack(4, ["aa","bb"])
Reached end β Add ["aa","bb"] to result β
Check dp[0][2]=false β Skip "aab"
Check dp[0][3]=false β Skip "aabb"
Final result: [
["a","a","b","b"],
["a","a","bb"],
["aa","b","b"],
["aa","bb"]
]
Total: 4 valid partitions found! β
Why This Is The Best Approach:
ADVANTAGES:
β
Clear two-phase structure
β
All palindrome checks are O(1)
β
No repeated palindrome calculations
β
Easy to understand and debug
β
Optimal time complexity
EFFICIENCY:
Palindrome table: Built once in O(nΒ²)
Backtracking: Uses O(1) lookups
Total: O(nΒ²) prep + O(n Γ 2^n) backtracking
This is THE standard solution! π
π Complexity Analysis
Time Complexity: O(nΒ² + n Γ 2^n)
Build palindrome table: O(nΒ²)
- All substrings: n(n+1)/2 β nΒ²
- Each cell: O(1) using previous results
Backtracking: O(n Γ 2^n)
- Number of partitions: O(2^n)
- Work per partition: O(n)
Total: O(nΒ²) + O(n Γ 2^n) = O(n Γ 2^n)
Space Complexity: O(nΒ²)
DP table: O(nΒ²) - all substring palindrome info
Recursion stack: O(n)
Current partition: O(n)
Total: O(nΒ²)
π΅ Approach 4: Space-Optimized (No DP Table)
π‘ Intuition
For small strings (n β€ 16), the O(nΒ²) table might be overkill:
Trade-off decision:
- DP table saves time but uses O(nΒ²) space
- For n=16: 16Β² = 256 cells
- For small n, repeated checks are fast enough
Space-optimized approach:
- Check palindromes on-demand (like Approach 1)
- No memoization table
- Save O(nΒ²) space
- Good for memory-constrained environments
π Implementation
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> result = new ArrayList<>();
backtrack(s, 0, new ArrayList<>(), result);
return result;
}
private void backtrack(String s, int start, List<String> current,
List<List<String>> result) {
if (start == s.length()) {
result.add(new ArrayList<>(current));
return;
}
for (int end = start; end < s.length(); end++) {
if (isPalindrome(s, start, end)) {
current.add(s.substring(start, end + 1));
backtrack(s, end + 1, current, result);
current.remove(current.size() - 1);
}
}
}
private boolean isPalindrome(String s, int left, int right) {
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
}
π Complexity Analysis
Time Complexity: O(n Γ 2^n)
Slightly slower due to repeated palindrome checks
But for n β€ 16, difference is negligible
Space Complexity: O(n)
No DP table: Best space complexity!
Only recursion stack: O(n)
π― Pattern Recognition
Core Pattern: Generate All Valid Combinations
When you need to find ALL valid ways to do something:
1. Use BACKTRACKING to explore all possibilities
2. Use DP to optimize expensive checks
3. Use Choose-Explore-Unchoose pattern
For Palindrome Partitioning:
- BACKTRACKING: Try all partition points
- DP: Precompute palindrome information
- PATTERN: Choose cut β Explore rest β Unchoose
β οΈ Important Edge Cases to Test
s = "a" // Expected: [["a"]]
s = "aaa" // Expected: [["a","a","a"],["a","aa"],["aa","a"],["aaa"]]
s = "ab" // Expected: [["a","b"]]
s = "aba" // Expected: [["a","b","a"],["aba"]]
s = "aab" // Expected: [["a","a","b"],["aa","b"]]
s = "aabb" // Expected: [["a","a","b","b"],["a","a","bb"],["aa","b","b"],["aa","bb"]]
s = "abba" // Expected: [["a","b","b","a"],["a","bb","a"],["abba"]]
s = "racecar" // Expected: Multiple including ["racecar"]
π Quick Revision Notes
π― Core Concept
Problem: Partition string so every piece is palindrome. Return ALL valid partitions.
Key Insight: Two-phase approach = Build palindrome table (DP), then backtrack with O(1) lookups.
Four Approaches: 1. Backtracking Only β O(n Γ 2^n), O(n) space, repeated checks 2. Memoized Checks β O(n Γ 2^n), O(nΒ²) space, cached checks 3. Precomputed Table β O(n Γ 2^n), O(nΒ²) space, best! β 4. Space-Optimized β O(n Γ 2^n), O(n) space, for small n
β‘ Quick Implementation
https://www.youtube.com/watch?v=WBgsABoClE0
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
public List<List<String>> partition(String s) {
int n = s.length();
List<List<String>> res = new ArrayList<>();
// // Approach 1 - recursive
// recursive(s, n, 0, new ArrayList<>(), res);
// // Approach 2 - top down
// // GOTCHA: Optimization will happen while checking if string
// // is palindrome or not. Nowhere else. That is the main
// // confusion of this problem.
// Boolean[][] memo = new Boolean[n][n];
// topDown(s, n, 0, new ArrayList<>(), res, memo);
bottomUp(s, res);
return res;
}
private void bottomUp(String s, List<List<String>> res) {
int n = s.length();
// Build the palindrome DP table first.
// 3 cases - 1 length, 2 length and remaining lengths
boolean[][] dp = new boolean[n][n];
// GOTCHA: see the reverse of i loop.
// Since its dp[i+1][j-1], i should come from end j should start from 0
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
if (i == j) {
dp[i][j] = true;
} else if (j == i + 1) {
dp[i][j] = s.charAt(i) == s.charAt(j);
} else {
dp[i][j] = (s.charAt(i) == s.charAt(j)) && dp[i + 1][j - 1];
}
}
}
recursive(s, n, 0, new ArrayList<>(), res, dp);
}
private void recursive(String s, int n, int start, ArrayList<String> curr, List<List<String>> res,
boolean[][] dp) {
// step 2: base case
if (start == n) {
res.add(new ArrayList<>(curr));
return;
}
for (int end = start; end < n; end++) {
if (dp[start][end]) {
curr.add(s.substring(start, end + 1));
recursive(s, n, end + 1, curr, res, dp);
curr.remove(curr.size() - 1);
}
}
}
private void topDown(String s, int n, int start, ArrayList<String> curr, List<List<String>> res, Boolean[][] memo) {
if (start == n) {
res.add(new ArrayList<>(curr));
}
for (int end = start; end < n; end++) {
if (isPalindrome(s, start, end, memo)) {
curr.add(s.substring(start, end + 1));
// for a|abb => we now proceed for abb
// for aa|bb => we now proceed for bb
topDown(s, n, end + 1, curr, res, memo);
// why remove? above call will build the result like aa, bb
curr.remove(curr.size() - 1);
}
}
}
private boolean isPalindrome(String s, int start, int end, Boolean[][] memo) {
if (memo[start][end] != null) {
return memo[start][end];
}
if (start >= end) {
return true;
}
while (start <= end) {
if (s.charAt(start) != s.charAt(end)) {
return memo[start][end] = false;
}
start++;
end--;
}
return memo[start][end] = true;
}
private void recursive(String s, int n, int start, ArrayList<String> curr, List<List<String>> res) {
// step 2: base case
if (start == n) {
res.add(new ArrayList<>(curr));
return;
}
// step 1: cut at every index start from index
// for start = 0, end traverses from end = 0 to end = n - 1
// aabb => a|abb, aa|bb, aab|b, aabb
// check if every cut is a palindrome. If yes, then only proceed to next.
// for example, a|abb => a is a palindrome. Now, repeat the cycle for abb
// For aa|bb => aa is a palindrome. Now, repeat the cycle for bb
// aab is not a palindrome. Do not proceed
// aabb is a palindome. Add it to the result.
for (int end = start; end < n; end++) {
if (isPalindrome(s, start, end)) {
curr.add(s.substring(start, end + 1));
// for a|abb => we now proceed for abb
// for aa|bb => we now proceed for bb
recursive(s, n, end + 1, curr, res);
// why remove? above call will build the result like aa, bb
curr.remove(curr.size() - 1);
}
}
}
private boolean isPalindrome(String s, int start, int end) {
while (start <= end) {
if (s.charAt(start) != s.charAt(end)) {
return false;
}
start++;
end--;
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
// [[a, a, b, b], [a, a, bb], [aa, b, b], [aa, bb]]
System.out.println(s.partition("aabb"));
System.out.println(s.partition("a")); // [[a]]
// [["a","a","a"],["a","aa"],["aa","a"],["aaa"]]
System.out.println(s.partition("aaa"));
System.out.println(s.partition("ab")); // [["a","b"]]
System.out.println(s.partition("aba")); // [["a","b","a"],["aba"]]
System.out.println(s.partition("aab")); // [["a","a","b"],["aa","b"]]
// [["a","b","b","a"],["a","bb","a"],["abba"]]
System.out.println(s.partition("abba"));
}
}
π Key Insights
Why Build by Length:
dp[i][j] needs dp[i+1][j-1] (inner substring)
Building by length ensures inner is ready:
Length 1 β Length 2 β Length 3 β ...
Each length uses only previous lengths!
Choose-Explore-Unchoose:
current.add(substring) // Choose
backtrack(next_position) // Explore
current.remove(current.size()-1) // Unchoose
This pattern naturally explores all combinations!
Why Create Copy:
result.add(new ArrayList<>(current)) // CORRECT β
result.add(current) // WRONG β
Must create copy! Otherwise all results
point to same list that keeps changing!
πͺ Memory Aid
"Build table first, backtrack second!"
"Choose-Explore-Unchoose pattern!"
"Build by length, inner first!"
"Always copy when adding to result!" β¨
β οΈ Common Mistakes
- β Not creating copy when adding to result
- β Forgetting to backtrack (remove last element)
- β Building palindrome table in wrong order
- β Checking palindrome without any optimization
- β Off-by-one errors in substring indices
π Congratulations!
You've mastered Palindrome Partitioning with the "aabb" example!
What You Learned:
β
Generation Problems - Finding ALL solutions
β
Two-Phase Approach - DP + Backtracking
β
Palindrome Table - Build by length
β
Backtracking Pattern - Choose-Explore-Unchoose
β
Space-Time Tradeoff - When to use DP table
You've mastered combining DP with Backtracking! πβ¨