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 <= 20s contains only lowercase English letters.
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.
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?
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?
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.
Before attempting this problem, you should be comfortable with:
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:
j (start of the next piece).i to form a substring s[j..i].s[j..i] is a palindrome, we choose it (put it into part) and then restart from the next position (i+1) to build the next piece.i to i+1 (trying a longer substring from the same start j).Backtracking means:
part: current list of chosen palindrome substrings.res: all valid partitions.j = start index of the current substring we're trying to form.i = end index we are expanding.i reaches the end of the string:j is also at the end, it means we perfectly partitioned the whole string → add a copy of part to res.s[j..i] is a palindrome:part.dfs(i+1, i+1).dfs(j, i+1).isPali(l,r)): two pointers moving inward; if mismatch → return false.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 TrueWe 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:
s[i..j] is a palindrome, it can be the next piece.part), then recursively solve the rest starting at j + 1.j.This guarantees:
part: current list of chosen substrings.res: all palindrome partitions.dfs(i) where i is the start index of the next substring.i == len(s), the whole string has been partitioned → add a copy of part to res.j from i to len(s)-1:s[i..j] is palindrome:s[i..j] to part.dfs(j + 1).l, r move inward; if mismatch → return false.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 TrueIn 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:
dp[i][j] that tells whether s[i..j] is a palindrome.dp[i][j].This makes backtracking faster because palindrome checks become O(1).
Step 1: Precompute Palindromes (DP)
dp where:dp[i][j] = True if substring s[i..j] is a palindrome.s[i] == s[j] and inner substring is palindrome (or length ≤ 2).Step 2: Backtracking
part: current partition.res: all valid palindrome partitions.dfs(i):i = starting index for next substring.i == len(s), add a copy of part to res.j from i to end:dp[i][j] is true:s[i..j].dfs(j + 1).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 resThis approach combines Dynamic Programming and pure recursion (return-based).
Think of it as:
“All partitions starting at index
i=
choose a palindromes[i..j]+ all partitions starting atj + 1”
Step 1: Precompute Palindromes (DP)
dp[i][j].dp[i][j] = True if substring s[i..j] is a palindrome.Step 2: Recursive Construction
dfs(i):i.i == len(s), return [[]] (one empty partition).j from i to end:dp[i][j] is true:dfs(j + 1).s[i..j] to each returned partition.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)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.
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.
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.