Loading content...
Consider the string "aab". How many ways can you split it so that every piece reads the same forwards and backwards? The answer might surprise you: there are two ways—["a","a","b"] and ["aa","b"]. Each partition consists entirely of palindromes.
The Palindrome Partitioning problem combines string processing with backtracking in an elegant dance. It asks us to find all ways to partition a string such that every resulting substring is a palindrome. This problem appears in text processing, data compression, and even DNA sequence analysis, where finding symmetric patterns within sequences has biological significance.
What makes this problem fascinating is how it transforms a seemingly linear string operation into a tree exploration problem. At each position, we face choices: where should the next cut be? The backtracking framework lets us systematically explore every possibility.
By the end of this page, you will understand the Palindrome Partitioning problem completely, implement both naive and optimized solutions using dynamic programming for palindrome checking, analyze the time and space complexity, and recognize how this pattern applies to other string partitioning problems.
The Palindrome Partitioning Problem (LeetCode 131):
Given a string s, partition s such that every substring of the partition is a palindrome. Return all possible palindrome partitioning of s.
Definitions:
Example 1:
s = "aab"[["a","a","b"], ["aa","b"]]Example 2:
s = "a"[["a"]]Constraints:
The small constraint on string length (≤ 16) hints that an exponential algorithm is acceptable—there can be up to 2^(n-1) partitions of a string of length n.
s = "aab"[["a","a","b"], ["aa","b"]]Partition ["a","a","b"]: three cuts resulting in single-character substrings (all single chars are palindromes). Partition ["aa","b"]: "aa" is a palindrome, "b" is a palindrome. Invalid partition ["aab"]: the entire string "aab" is NOT a palindrome, so this doesn't count. Invalid partition ["a","ab"]: "ab" is not a palindrome.
A partition is defined by where we make cuts in the string. For a string of length n, there are n-1 possible cut positions (between each pair of adjacent characters). Each cut position is either used or not, giving 2^(n-1) possible partitions. Our job is to filter those where every piece is a palindrome.
The Palindrome Partitioning problem naturally maps to backtracking because we're exploring a decision tree where each node represents a choice.
The Decision at Each Position:
Imagine we're at position i in the string. We need to decide: where does the current palindrome end? We could take:
s[i] (length 1) as the next palindrome, then continue from i+1s[i:i+2] (length 2) if it's a palindrome, then continue from i+2s[i:i+3] (length 3) if it's a palindrome, then continue from i+3This creates a tree:
For s = "aab":
Start at index 0
├── Take "a" (palindrome ✓), continue from 1
│ ├── Take "a" (palindrome ✓), continue from 2
│ │ └── Take "b" (palindrome ✓), continue from 3 → DONE: ["a","a","b"]
│ └── Take "ab" (palindrome ✗) → Prune
└── Take "aa" (palindrome ✓), continue from 2
└── Take "b" (palindrome ✓), continue from 3 → DONE: ["aa","b"]
└── Take "aab" (palindrome ✗) → Prune
The Pruning Condition:
We prune a branch whenever the current substring is not a palindrome. This avoids exploring subtrees that can never lead to valid partitions. The earlier we detect non-palindromes, the more computation we save.
index in the string, and the current partition path built so far.index == len(s)), meaning we've successfully partitioned the entire string.index: s[index:index+1], s[index:index+2], ..., s[index:n].Let's implement the straightforward backtracking solution with an inline palindrome check.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
def partition(s: str) -> list[list[str]]: """ Find all palindrome partitions of string s. Basic approach: backtracking with inline palindrome check. Time Complexity: O(n * 2^n) where n = len(s) Space Complexity: O(n) for recursion depth and current path """ result = [] def is_palindrome(substring: str) -> bool: """Check if a string is a palindrome.""" return substring == substring[::-1] def backtrack(index: int, path: list[str]) -> None: # Base case: reached end of string, valid partition found if index == len(s): result.append(path[:]) # Append a copy return # Try all possible substrings starting at 'index' for end in range(index + 1, len(s) + 1): substring = s[index:end] # Only proceed if the substring is a palindrome if is_palindrome(substring): # CHOOSE: Add this palindrome to our partition path.append(substring) # EXPLORE: Continue partitioning the rest of the string backtrack(end, path) # UNCHOOSE: Remove the palindrome (backtrack) path.pop() backtrack(0, []) return result # Example usageprint(partition("aab"))# Output: [['a', 'a', 'b'], ['aa', 'b']] print(partition("aaba"))# Output: [['a', 'a', 'b', 'a'], ['a', 'aba'], ['aa', 'b', 'a']] print(partition("aaaa"))# Output: Many partitions including ['aaaa'], ['aa', 'aa'], ['a', 'a', 'a', 'a'], etc.We iterate 'end' from index+1 to len(s)+1 because Python/TypeScript slicing s[index:end] excludes the end index. So s[0:1] gives us s[0], s[0:2] gives us s[0:1], etc. This is a common source of off-by-one errors—always trace through with a small example!
The basic solution rechecks palindrome status for overlapping substrings. For example, when exploring different paths, we might check if "aba" is a palindrome multiple times.
The Redundancy Problem:
Consider s = "ababa". During backtracking:
The substring "aba" is checked multiple times across different paths. For longer strings, this redundancy becomes significant.
The DP Solution:
We precompute a 2D table is_palindrome[i][j] that tells us whether s[i:j+1] is a palindrome. The recurrence:
is_palindrome[i][j] = (s[i] == s[j]) AND is_palindrome[i+1][j-1]
Base cases:
is_palindrome[i][i] = Trueis_palindrome[i][i+1] = (s[i] == s[i+1])We fill the table by increasing substring length, ensuring that when we compute is_palindrome[i][j], we've already computed is_palindrome[i+1][j-1].
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
def partition_optimized(s: str) -> list[list[str]]: """ Find all palindrome partitions with DP-precomputed palindrome checking. Optimization: Precompute is_palindrome[i][j] for all substrings. Time Complexity: O(n^2) for precomputation + O(n * 2^n) for backtracking Space Complexity: O(n^2) for the DP table + O(n) for recursion """ n = len(s) # Step 1: Precompute palindrome table # is_pal[i][j] = True if s[i:j+1] is a palindrome is_pal = [[False] * n for _ in range(n)] # All single characters are palindromes for i in range(n): is_pal[i][i] = True # Check two-character substrings for i in range(n - 1): is_pal[i][i + 1] = (s[i] == s[i + 1]) # Fill for lengths 3 and above for length in range(3, n + 1): for i in range(n - length + 1): j = i + length - 1 # s[i:j+1] is palindrome if s[i]==s[j] and inner part is palindrome is_pal[i][j] = (s[i] == s[j]) and is_pal[i + 1][j - 1] # Step 2: Backtracking with O(1) palindrome lookup result = [] def backtrack(index: int, path: list[str]) -> None: if index == n: result.append(path[:]) return for end in range(index, n): # O(1) palindrome check using precomputed table if is_pal[index][end]: path.append(s[index:end + 1]) backtrack(end + 1, path) path.pop() backtrack(0, []) return result # Example with timing comparisonimport time s = "aaaaaaaaaaaaa" # 13 a's - many palindrome partitions start = time.time()result_basic = partition(s) # Basic versionbasic_time = time.time() - start start = time.time()result_optimized = partition_optimized(s)optimized_time = time.time() - start print(f"Basic: {len(result_basic)} partitions in {basic_time:.4f}s")print(f"Optimized: {len(result_optimized)} partitions in {optimized_time:.4f}s")# Optimized version is noticeably faster for repeated palindrome checksNotice the subtle index change in the optimized version: we iterate 'end' from index to n-1 (inclusive), and use s[index:end+1] for the substring. The DP table uses inclusive indices is_pal[i][j] for s[i..j], matching how we typically think about substrings mathematically. Be careful to stay consistent!
Let's rigorously analyze both the basic and optimized solutions.
Basic Solution Complexity:
Time Complexity: O(n × 2^n)
Space Complexity: O(n)
Optimized Solution Complexity:
Time Complexity: O(n^2) + O(n × 2^n) = O(n × 2^n)
Space Complexity: O(n^2)
| Aspect | Basic Solution | Optimized Solution |
|---|---|---|
| Time – Precomputation | O(1) | O(n²) |
| Time – Per palindrome check | O(n) | O(1) |
| Time – Total | O(n × 2ⁿ) | O(n × 2ⁿ) but faster constants |
| Space – Auxiliary | O(n) | O(n²) |
| Best for | Small n or few palindromes | Large n or many repeated checks |
The DP precomputation shines when the same substring is checked multiple times. For strings like "aaaa...a" where almost every substring is a palindrome, optimization provides substantial speedup. For strings with few palindromes (like "abcdefghij"), most branches are pruned early, and the basic solution may suffice.
The Palindrome Partitioning pattern extends to several related problems that test your understanding more deeply.
Palindrome Partitioning II (LeetCode 132):
Instead of finding all partitions, find the minimum number of cuts needed to partition the string into palindromes.
This transforms from backtracking to pure DP:
dp[i] = minimum cuts needed for s[0:i+1]j ≤ i, if s[j:i+1] is a palindrome: dp[i] = min(dp[i], dp[j-1] + 1)Palindrome Partitioning III (LeetCode 1278):
Partition the string into exactly k non-empty substrings and make each substring a palindrome by changing at most some characters. Find the minimum changes needed.
This requires tracking two dimensions: position and number of partitions.
Palindrome Partitioning IV (LeetCode 1745):
Can the string be partitioned into exactly three non-empty palindromic substrings?
This can be solved in O(n²) by precomputing palindrome status and checking all possible split points.
123456789101112131415161718192021222324252627282930313233343536373839
def minCut(s: str) -> int: """ Palindrome Partitioning II: Find minimum cuts. This is a DP problem, not backtracking. dp[i] = minimum cuts for s[0:i+1] Time: O(n^2), Space: O(n^2) """ n = len(s) # Precompute palindrome table is_pal = [[False] * n for _ in range(n)] for i in range(n): is_pal[i][i] = True for i in range(n - 1): is_pal[i][i + 1] = s[i] == s[i + 1] for length in range(3, n + 1): for i in range(n - length + 1): j = i + length - 1 is_pal[i][j] = s[i] == s[j] and is_pal[i + 1][j - 1] # DP for minimum cuts # dp[i] = min cuts for s[0:i+1] dp = list(range(n)) # Worst case: cut before each char for i in range(n): if is_pal[0][i]: dp[i] = 0 # The whole prefix is a palindrome else: for j in range(1, i + 1): if is_pal[j][i]: dp[i] = min(dp[i], dp[j - 1] + 1) return dp[n - 1] print(minCut("aab")) # 1 (cut after "aa")print(minCut("a")) # 0 (already a palindrome)print(minCut("ab")) # 1 (must cut to get "a" and "b")Implementing Palindrome Partitioning correctly requires attention to several subtle details.
s[i:j] gives characters i to j-1. If your DP table uses inclusive indices is_pal[i][j] for positions i to j, you need s[i:j+1] to get the corresponding substring.result.append(path[:]) not result.append(path). The path list is reused.is_pal[i][j], you need is_pal[i+1][j-1] already computed. Fill by increasing length, not by row or column alone.When debugging, trace through s="ab" first (simple, 2 partitions: ["a","b"]). Then try s="aa" (should give [["a","a"], ["aa"]]). Finally test s="aab" which has more variation. Build up complexity gradually.
Interview Approach:
Clarify: Ask about input constraints, expected output format, and whether we need all partitions or just count/minimum.
Start simple: Begin with the basic backtracking solution. Mention that palindrome checking happens at each step.
Optimize if prompted: Introduce DP precomputation for O(1) palindrome checks. Explain the time-space tradeoff.
Discuss complexity: Show you understand both the exponential nature of the problem (2^n partitions) and how optimization reduces constant factors.
Edge cases: Single character (one partition), all same characters (many partitions), no valid multi-char palindromes (trivial partition into singles).
Palindrome Partitioning elegantly combines string processing with backtracking, demonstrating how seemingly complex problems yield to systematic exploration.
What's next:
With string partitioning mastered, we'll explore Valid Parentheses Generation—a classic backtracking problem that generates all syntactically valid combinations of n pairs of parentheses. This problem introduces the concept of balance constraints and demonstrates backtracking with more abstract validity conditions.
You now have comprehensive understanding of Palindrome Partitioning. This pattern—partitioning a string based on substring properties—applies broadly to word segmentation, text parsing, and sequence analysis. Practice implementing both versions and tracing through the DP table construction to solidify your understanding.