Palindrome Partitioning
Difficulty: Medium Source: NeetCode
Problem
Given a string
s, partitionssuch that every substring of the partition is a palindrome. Return all possible palindrome partitionings.Example 1: Input:
s = "aab"Output:[["a","a","b"],["aa","b"]]Example 2: Input:
s = "a"Output:[["a"]]Constraints:
1 <= s.length <= 16sconsists only of lowercase English letters.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Palindrome checking — a string equals its reverse; or check with two pointers
- Backtracking — at each position, try all possible next substrings
- Subsets/combinations — this problem is about choosing “cut points” in the string
1. Brute Force
Intuition
At each index in the string, try every possible prefix ending at some index end. If the prefix is a palindrome, add it to the current partition and recurse on the remainder of the string. When the start index reaches the end of the string, we’ve partitioned the whole string into palindromes — record it. This is already basically the optimal backtracking approach; there’s no fundamentally simpler way to enumerate all partitions.
Algorithm
- Define
backtrack(start, current). - If
start == len(s): append a copy ofcurrentto results and return. - For
endfromstart + 1tolen(s):- Extract
substr = s[start:end]. - If
substris a palindrome:- Append
substrtocurrent. - Recurse with
start = end. - Pop
substr.
- Append
- Extract
Solution
def partition_brute(s):
result = []
def is_palindrome(sub):
return sub == sub[::-1]
def backtrack(start, current):
if start == len(s):
result.append(list(current))
return
for end in range(start + 1, len(s) + 1):
substr = s[start:end]
if is_palindrome(substr):
current.append(substr)
backtrack(end, current)
current.pop()
backtrack(0, [])
return result
print(partition_brute("aab")) # [['a', 'a', 'b'], ['aa', 'b']]
print(partition_brute("a")) # [['a']]
Complexity
- Time:
O(n * 2^n)— up to 2^(n-1) partitions, each palindrome check takes O(n) - Space:
O(n)— recursion depth
2. Backtracking with DP Palindrome Precomputation
Intuition
The approach is the same as brute force, but we precompute a 2D table dp[i][j] that stores whether s[i:j+1] is a palindrome. This turns every palindrome check from O(n) into O(1), which helps when the string is longer. dp[i][j] is a palindrome if s[i] == s[j] and either the substring is length 1 or 2, or dp[i+1][j-1] is also a palindrome.
Algorithm
- Precompute
dp[i][j] = True if s[i:j+1] is a palindrome. - Run the same backtracking, but use
dp[start][end-1]instead of callingis_palindrome.
Solution
def partition(s):
n = len(s)
# precompute palindrome table
dp = [[False] * n for _ in range(n)]
for i in range(n - 1, -1, -1):
for j in range(i, n):
if s[i] == s[j] and (j - i <= 1 or dp[i + 1][j - 1]):
dp[i][j] = True
result = []
def backtrack(start, current):
if start == n:
result.append(list(current))
return
for end in range(start, n):
if dp[start][end]: # O(1) palindrome check
current.append(s[start:end + 1])
backtrack(end + 1, current)
current.pop()
backtrack(0, [])
return result
print(partition("aab")) # [['a', 'a', 'b'], ['aa', 'b']]
print(partition("a")) # [['a']]
print(partition("aba")) # [['a', 'b', 'a'], ['aba']]
print(partition("aabaa")) # multiple valid partitions
Complexity
- Time:
O(n^2)for DP precomputation +O(n * 2^n)for backtracking — overallO(n * 2^n) - Space:
O(n^2)for the DP table +O(n)recursion depth
Common Pitfalls
Off-by-one in the end range. The substring s[start:end] uses Python’s exclusive upper bound. If you want to include s[end], you need s[start:end+1]. When using the DP table, dp[start][end] checks if s[start..end] (inclusive) is a palindrome, so you pass the end index inclusively.
Appending current without copying. Always result.append(list(current)). If you append the reference, all stored partitions will point to the same list and end up empty after the recursion finishes.
Forgetting to try single-character substrings. Every single character is trivially a palindrome. Make sure your loop starts at end = start (for the DP version) or end = start + 1 (for the slice version where s[start:start+1] is one character). A common mistake is starting at end = start + 2, which would skip single characters.