131. Palindrome Partitioning - Explanation
Description
Given a string s, split s into substrings where every substring is a palindrome. Return all possible lists of palindromic substrings.
You may return the solution in any order.
Example 1:
Input: s = "aab"
Output: [["a","a","b"],["aa","b"]]Example 2:
Input: s = "a"
Output: [["a"]]Constraints:
1 <= s.length <= 20scontains only lowercase English letters.
Topics
Recommended Time & Space Complexity
You should aim for a solution with O(n * (2^n)) time and O(n) space, where n is the length of the input string.
Hint 1
For a given string there are 2^n possible partitions because at each index we have two decisions: we can either partition and start a new string, or continue without partitioning. Can you think of an algorithm to recursively traverse all combinations?
Hint 2
We can use backtracking to recursively traverse the string with indices j (start of the current substring) and i (current iterating index). At each step, we either skip partitioning at the current index or, if the substring from j to i is a palindrome, make a partition, update j = i + 1, and start a new substring. The base condition to stop the recursion is when j reaches the end of the string. How do you implement this?
Hint 3
We start with j = 0, i = 0 and a temporary list which stores the substrings from the partitions. Then we recursively iterate the string with the index i. At each step we apply the 2 decisions accordingly. At the base condition of the recursive path, we make a copy of the current partition list and add it to the result.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Backtracking - Exploring all possible partitions and undoing choices to try alternatives
- Recursion - Breaking down the problem into smaller subproblems
- Palindrome Checking - Using two pointers to verify if a substring is a palindrome
- Dynamic Programming - Precomputing palindrome substrings for O(1) lookup
1. Backtracking - I
Intuition
We want to split the string into pieces, but we only keep a split if every piece is a palindrome.
Think of placing “cuts” in the string:
- Start at some index
j(start of the next piece). - Try to extend the end index
ito form a substrings[j..i]. - If
s[j..i]is a palindrome, we choose it (put it intopart) and then restart from the next position (i+1) to build the next piece. - Whether or not it was a palindrome, we can also extend further by moving
itoi+1(trying a longer substring from the same startj).
Backtracking means:
- When we choose a palindrome piece, we go deeper.
- After returning, we remove that piece and try other possibilities.
Algorithm
- Keep:
part: current list of chosen palindrome substrings.res: all valid partitions.
- Use DFS with two pointers:
j= start index of the current substring we're trying to form.i= end index we are expanding.
- If
ireaches the end of the string:- If
jis also at the end, it means we perfectly partitioned the whole string → add a copy ofparttores. - Return.
- If
- If substring
s[j..i]is a palindrome:- Add it to
part. - Recurse with next start/end:
dfs(i+1, i+1). - Backtrack: remove the last added substring.
- Add it to
- Also try making the substring longer without cutting yet:
dfs(j, i+1).
- Palindrome check (
isPali(l,r)): two pointers moving inward; if mismatch → returnfalse.
class Solution:
def partition(self, s: str) -> List[List[str]]:
res, part = [], []
def dfs(j, i):
if i >= len(s):
if i == j:
res.append(part.copy())
return
if self.isPali(s, j, i):
part.append(s[j : i + 1])
dfs(i + 1, i + 1)
part.pop()
dfs(j, i + 1)
dfs(0, 0)
return res
def isPali(self, s, l, r):
while l < r:
if s[l] != s[r]:
return False
l, r = l + 1, r - 1
return TrueTime & Space Complexity
- Time complexity:
- Space complexity:
- extra space.
- space for the output list.
2. Backtracking - II
Intuition
We build the partition from left to right.
At any starting index i, we have a simple question:
“Where should I cut next?”
So we try every possible end index j from i to end:
- If
s[i..j]is a palindrome, it can be the next piece. - Choose it (add to
part), then recursively solve the rest starting atj + 1. - After coming back, undo the choice (pop) and try a different
j.
This guarantees:
- We only add palindrome pieces.
- We explore all valid ways to cut the string.
Algorithm
- Maintain:
part: current list of chosen substrings.res: all palindrome partitions.
- Define DFS
dfs(i)whereiis the start index of the next substring. - Base case:
- If
i == len(s), the whole string has been partitioned → add a copy ofparttores.
- If
- For each
jfromitolen(s)-1:- If
s[i..j]is palindrome:- Add
s[i..j]topart. - Recurse
dfs(j + 1). - Backtrack: remove last substring.
- Add
- If
- Palindrome check:
- Two pointers
l, rmove inward; if mismatch → returnfalse.
- Two pointers
class Solution:
def partition(self, s: str) -> List[List[str]]:
res, part = [], []
def dfs(i):
if i >= len(s):
res.append(part.copy())
return
for j in range(i, len(s)):
if self.isPali(s, i, j):
part.append(s[i : j + 1])
dfs(j + 1)
part.pop()
dfs(0)
return res
def isPali(self, s, l, r):
while l < r:
if s[l] != s[r]:
return False
l, r = l + 1, r - 1
return TrueTime & Space Complexity
- Time complexity:
- Space complexity:
- extra space.
- space for the output list.
3. Backtracking (DP)
Intuition
In plain backtracking, we repeatedly check whether substrings are palindromes, which costs extra time.
To optimize this, we precompute all palindrome substrings once using Dynamic Programming (DP).
Idea:
- First, build a DP table
dp[i][j]that tells whethers[i..j]is a palindrome. - Then use backtracking just like before, but instead of checking palindromes on the fly, we directly look up
dp[i][j].
This makes backtracking faster because palindrome checks become O(1).
Algorithm
Step 1: Precompute Palindromes (DP)
- Create a 2D table
dpwhere:dp[i][j] = Trueif substrings[i..j]is a palindrome.
- Fill it by increasing substring length:
- Single characters are palindromes.
- For longer substrings:
s[i] == s[j]and inner substring is palindrome (or length ≤ 2).
Step 2: Backtracking
- Maintain:
part: current partition.res: all valid palindrome partitions.
- Define
dfs(i):i= starting index for next substring.
- Base case:
- If
i == len(s), add a copy ofparttores.
- If
- Try all
jfromito end:- If
dp[i][j]istrue:- Choose substring
s[i..j]. - Recurse on
dfs(j + 1). - Backtrack (remove last choice).
- Choose substring
- If
class Solution:
def partition(self, s: str) -> List[List[str]]:
n = len(s)
dp = [[False] * n for _ in range(n)]
for l in range(1, n + 1):
for i in range(n - l + 1):
dp[i][i + l - 1] = (s[i] == s[i + l - 1] and
(i + 1 > (i + l - 2) or
dp[i + 1][i + l - 2]))
res, part = [], []
def dfs(i):
if i >= len(s):
res.append(part.copy())
return
for j in range(i, len(s)):
if dp[i][j]:
part.append(s[i : j + 1])
dfs(j + 1)
part.pop()
dfs(0)
return resTime & Space Complexity
- Time complexity:
- Space complexity:
- extra space.
- space for the output list.
4. Recursion
Intuition
This approach combines Dynamic Programming and pure recursion (return-based).
- We first precompute all palindromic substrings using DP.
- Then we use recursion where each recursive call:
- returns all valid palindrome partitions starting from a given index.
- Instead of maintaining a global result or path, each recursive call builds and returns its own list of partitions, which makes the logic clean and declarative.
Think of it as:
“All partitions starting at index
i=
choose a palindromes[i..j]+ all partitions starting atj + 1”
Algorithm
Step 1: Precompute Palindromes (DP)
- Create a 2D table
dp[i][j]. dp[i][j] = Trueif substrings[i..j]is a palindrome.- Fill the table by increasing substring length:
- Characters at ends must match.
- Inner substring must already be a palindrome (or be empty).
Step 2: Recursive Construction
- Define
dfs(i):- Returns all palindrome partitions starting from index
i.
- Returns all palindrome partitions starting from index
- Base case:
- If
i == len(s), return[[]](one empty partition).
- If
- Recursive case:
- For every
jfromito end:- If
dp[i][j]istrue:- Recursively get partitions from
dfs(j + 1). - Prepend
s[i..j]to each returned partition.
- Recursively get partitions from
- If
- For every
- Return the collected partitions.
class Solution:
def partition(self, s: str) -> List[List[str]]:
n = len(s)
dp = [[False] * n for _ in range(n)]
for l in range(1, n + 1):
for i in range(n - l + 1):
dp[i][i + l - 1] = (s[i] == s[i + l - 1] and
(i + 1 > (i + l - 2) or
dp[i + 1][i + l - 2]))
def dfs(i):
if i >= n:
return [[]]
ret = []
for j in range(i, n):
if dp[i][j]:
nxt = dfs(j + 1)
for part in nxt:
cur = [s[i : j + 1]] + part
ret.append(cur)
return ret
return dfs(0)Time & Space Complexity
- Time complexity:
- Space complexity:
- extra space.
- space for the output list.
Common Pitfalls
Forgetting to Copy the Partition Before Adding to Results
When adding the current partition to the results list, you must add a copy of the list, not a reference to it. Since backtracking modifies the same partition list, adding the reference directly means all entries in the result will point to the same (eventually empty) list.
Incorrect Base Case in Backtracking
The base case should trigger when the starting index reaches the end of the string, indicating a complete valid partition. A common mistake is to check i > len(s) instead of i >= len(s) or i == len(s), causing missed partitions or index errors.
Redundant Palindrome Checks Without Memoization
Repeatedly checking whether the same substring is a palindrome across different recursion branches wastes time. Without precomputing palindrome information using DP, the same substring may be checked O(2^n) times, significantly slowing down the solution for longer strings.
Sign in to join the discussion