Skip to content

131. Palindrome Partitioning (Medium)

Problem

Given a string s, partition s such that every substring of the partition is a palindrome. Return all possible palindrome partitionings.

Example

  • s = "aab"[["a","a","b"], ["aa","b"]]
  • s = "a"[["a"]]

LeetCode 131 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Brute force, backtracking with per-cut palindrome check

For each cut point, check whether the left piece is a palindrome; if yes, recurse on the rest.

def partition(s):
result = []
path = []
def is_pal(t):
return t == t[::-1] # L1: O(k) slice + compare
def backtrack(start):
if start == len(s):
result.append(path[:]) # L2: O(n) copy at leaf
return
for end in range(start + 1, len(s) + 1):
piece = s[start:end] # L3: O(k) slice
if is_pal(piece):
path.append(piece) # L4: O(1) push
backtrack(end) # L5: recurse
path.pop() # L6: O(1) pop
backtrack(0)
return result

Where the time goes, line by line

Variables: n = len(s), k = substring length.

LinePer-call costTimes executedContribution
L1/L3 (palindrome check + slice)O(k)O(k) per cutO(n2n)O(n · 2^n) total cutsO(n2n)O(n · 2^n)
L5 (recurse)O(1)O(1) dispatchO(2n)O(2^n) nodesO(2n)O(2^n) ← dominates
L2 (copy)O(n)O(n)partitionsO(npartitions)O(n · partitions)

There are 2^(n-1) possible cut positions, so the search space is O(2n)O(2^n). Each call to is_pal for a substring of length k costs O(k)O(k).

Complexity

  • Time: O(n2n)O(n · 2^n). For each of 2^(n-1) possible partitions, O(n)O(n) palindrome checks.
  • Space: O(n)O(n) recursion.
final class Solution {
func partition(_ s: String) -> [[String]] {
let chars = Array(s); var result: [[String]] = []
func search(_ start: Int, _ current: [String]) { if start == chars.count { result.append(current); return }; for end in start..<chars.count { let piece = String(chars[start...end]); if Array(piece) == Array(piece.reversed()) { search(end + 1, current + [piece]) } } }
search(0, []); return result
}
}

Approach 2: Two-pointer palindrome check (same Big-O, no string slicing)

Avoid slicing by checking the palindrome inline.

def partition(s):
result = []
path = []
n = len(s)
def is_pal(l, r):
while l < r:
if s[l] != s[r]:
return False
l += 1
r -= 1 # L1: O(k) two-pointer check
return True
def backtrack(start):
if start == n:
result.append(path[:]) # L2: O(n) copy
return
for end in range(start, n):
if is_pal(start, end):
path.append(s[start:end + 1]) # L3: O(k) slice for result
backtrack(end + 1) # L4: recurse
path.pop()
backtrack(0)
return result

Where the time goes, line by line

Variables: n = len(s), k = substring length.

LinePer-call costTimes executedContribution
L1 (two-pointer check)O(k)O(k)O(n2)O(n^2) pairsO(n2)O(n^2) setup
L4 (recurse)O(1)O(1) dispatchO(2n)O(2^n) nodesO(n2n)O(n · 2^n) ← dominates
L2 (copy)O(n)O(n)partitionsO(npartitions)O(n · partitions)

Complexity

  • Time: O(n2n)O(n · 2^n) worst case.
  • Space: O(n)O(n) recursion.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).
final class Solution {
func partition(_ s: String) -> [[String]] {
let chars = Array(s); var result: [[String]] = []
func palindrome(_ left: Int, _ right: Int) -> Bool { var left = left, right = right; while left < right { if chars[left] != chars[right] { return false }; left += 1; right -= 1 }; return true }
func search(_ start: Int, _ current: [String]) { if start == chars.count { result.append(current); return }; for end in start..<chars.count where palindrome(start, end) { search(end + 1, current + [String(chars[start...end])]) } }
search(0, []); return result
}
}

Approach 3: Precompute palindrome DP table (optimal constant factor)

Precompute is_pal[i][j] for all substrings in O(n2)O(n^2). Then the palindrome check during backtracking is O(1)O(1).

def partition(s):
n = len(s)
# is_pal[i][j] = True iff s[i:j+1] is a palindrome
is_pal = [[False] * n for _ in range(n)]
for i in range(n):
is_pal[i][i] = True # L1: single chars are palindromes
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j] and (length == 2 or is_pal[i + 1][j - 1]):
is_pal[i][j] = True # L2: O(1) DP recurrence
result = []
path = []
def backtrack(start):
if start == n:
result.append(path[:]) # L3: O(n) copy
return
for end in range(start, n):
if is_pal[start][end]: # L4: O(1) table lookup
path.append(s[start:end + 1]) # L5: O(k) slice for result
backtrack(end + 1) # L6: recurse
path.pop()
backtrack(0)
return result

Where the time goes, line by line

Variables: n = len(s).

LinePer-call costTimes executedContribution
L1/L2 (DP table build)O(1)O(1)n^2 cellsO(n2)O(n^2)
L4 (O(1)O(1) lookup)O(1)O(1)O(n2n)O(n · 2^n)O(n2n)O(n · 2^n)
L6 (recurse)O(1)O(1) dispatchO(2n)O(2^n) nodesO(n2n)O(n · 2^n) ← dominates

The O(n2)O(n^2) DP preprocessing pays off when there are many palindrome checks; each one drops from O(k)O(k) to O(1)O(1).

Complexity

  • Time: O(n2n)O(n · 2^n). The DP table is O(n2)O(n^2) once; the backtracking work dominates asymptotically but is constant-factor faster.
  • Space: O(n2)O(n^2) DP + O(n)O(n) recursion.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).
final class Solution {
func partition(_ s: String) -> [[String]] {
let chars = Array(s), count = s.count; var table = Array(repeating: Array(repeating: false, count: count), count: count)
for left in stride(from: count - 1, through: 0, by: -1) { for right in left..<count where chars[left] == chars[right] && (right - left < 2 || table[left + 1][right - 1]) { table[left][right] = true } }
var result: [[String]] = []; func search(_ start: Int, _ current: [String]) { if start == count { result.append(current); return }; for end in start..<count where table[start][end] { search(end + 1, current + [String(chars[start...end])]) } }; search(0, []); return result
}
}

Summary

ApproachTimeSpace
Backtrack + slice palindrome checkO(n2n)O(n · 2^n)O(n)O(n)
Backtrack + two-pointer checkO(n2n)O(n · 2^n)O(n)O(n)
Backtrack + precomputed DPO(n2n)O(n · 2^n)O(n2)O(n^2)

All three have the same dominant term (exponential number of partitions). DP precomputation is the constant-factor win; it’s also how problem 132. Palindrome Partitioning II (min cuts) becomes tractable.

Test cases

def partition(s):
n = len(s)
is_pal = [[False] * n for _ in range(n)]
for i in range(n):
is_pal[i][i] = True
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j] and (length == 2 or is_pal[i + 1][j - 1]):
is_pal[i][j] = True
result = []; path = []
def backtrack(start):
if start == n: result.append(path[:]); return
for end in range(start, n):
if is_pal[start][end]:
path.append(s[start:end + 1]); backtrack(end + 1); path.pop()
backtrack(0)
return result
def _run_tests():
r = partition("aab")
assert sorted(map(tuple, r)) == sorted([("a","a","b"), ("aa","b")])
assert partition("a") == [["a"]]
# all same characters: every prefix is a palindrome
r3 = partition("aaa")
assert sorted(map(tuple, r3)) == sorted([("a","a","a"), ("a","aa"), ("aa","a"), ("aaa",)])
# single partition only (no internal palindromes)
r4 = partition("abc")
assert sorted(map(tuple, r4)) == sorted([("a","b","c")])
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Strings, partitioning at indices
  • Arrays, the 2D DP palindrome table
  • Backtracking, the explore, undo, and prune pattern for building candidates.
  • Sequence DP, the prefix or position state pattern used for strings and ordered arrays.