Palindrome Partitioning: Minimum Cuts DP
Given a string, partition it such that every substring in the partition is a palindrome. Return the minimum number of cuts needed to achieve this. This classic dynamic programming problem appears...
Key Insights
- The minimum palindrome partitioning problem requires two interconnected DP tables: one for O(1) palindrome lookups and another for tracking minimum cuts, reducing time complexity from exponential to O(n²).
- Preprocessing palindrome checks into a lookup table is essential—without it, repeated substring validation dominates runtime and defeats the purpose of dynamic programming.
- The recurrence relation
minCuts[i] = min(minCuts[j-1] + 1)for all valid palindrome endings captures the optimal substructure, where each cut decision builds on previously solved subproblems.
Introduction & Problem Statement
Given a string, partition it such that every substring in the partition is a palindrome. Return the minimum number of cuts needed to achieve this. This classic dynamic programming problem appears frequently in technical interviews and has practical applications beyond algorithmic puzzles.
In text processing, palindrome partitioning helps with pattern recognition and data compression. DNA sequence analysis uses similar techniques to identify symmetric genetic patterns. Compilers use related algorithms for parsing and tokenization where symmetric structures matter.
Here’s what the problem looks like in practice:
# Input: "aab"
# Output: 1
# Explanation: ["aa", "b"] requires 1 cut
# Input: "a"
# Output: 0
# Explanation: Already a palindrome, no cuts needed
# Input: "ab"
# Output: 1
# Explanation: ["a", "b"] requires 1 cut
# Input: "aabbc"
# Output: 2
# Explanation: ["aa", "bb", "c"] requires 2 cuts
The key insight is that a string of length n requires at most n-1 cuts (splitting into individual characters), but we want the minimum. When the entire string is already a palindrome, we need zero cuts.
Brute Force Approach
The naive approach tries every possible partition point recursively. For each position, we check if the prefix is a palindrome, and if so, recursively solve for the remaining suffix.
def min_cuts_brute_force(s: str) -> int:
def is_palindrome(string: str) -> bool:
return string == string[::-1]
def helper(string: str) -> int:
# Base case: empty string or already palindrome
if len(string) == 0 or is_palindrome(string):
return 0
min_cuts = len(string) - 1 # Worst case: cut every character
# Try every possible first cut position
for i in range(1, len(string)):
if is_palindrome(string[:i]):
# If prefix is palindrome, solve for suffix
cuts = 1 + helper(string[i:])
min_cuts = min(min_cuts, cuts)
return min_cuts
return helper(s)
This solution has exponential time complexity—O(2^n) in the worst case. Each position presents a binary choice (cut or don’t cut), and we explore all combinations. The palindrome check at each step adds O(n) overhead, making this approach impractical for strings longer than 20-25 characters.
The inefficiency stems from two sources: overlapping subproblems (we solve the same suffix multiple times) and repeated palindrome checks (we verify the same substrings repeatedly). Dynamic programming addresses both issues.
Understanding the DP Substructure
The problem exhibits optimal substructure: the minimum cuts for a string depends on the minimum cuts for its substrings. If we know the minimum cuts for all prefixes of our string, we can compute the answer for the full string by considering where to place the last cut.
Define minCuts[i] as the minimum number of cuts needed for the substring s[0..i] (inclusive). The recurrence relation is:
minCuts[i] = 0 if s[0..i] is a palindrome
minCuts[i] = min(minCuts[j-1] + 1) for all j where s[j..i] is a palindrome
The intuition: if s[j..i] is a palindrome, we can make one cut before position j and add it to the optimal solution for s[0..j-1].
# Pseudocode for state transitions
# For string "aab" (indices 0, 1, 2):
# minCuts[0] = 0 (single char 'a' is palindrome)
# minCuts[1] = 0 (s[0..1] = "aa" is palindrome)
# minCuts[2] = ?
# - Check if s[0..2] = "aab" is palindrome? No
# - Check if s[1..2] = "ab" is palindrome? No
# - Check if s[2..2] = "b" is palindrome? Yes
# So minCuts[2] = minCuts[1] + 1 = 0 + 1 = 1
This approach reduces our problem to O(n²) subproblems, but we still need efficient palindrome checks.
Preprocessing: Palindrome Lookup Table
Checking if a substring is a palindrome in O(1) requires preprocessing. We build a 2D boolean table where isPalin[i][j] indicates whether s[i..j] is a palindrome.
The key observation: s[i..j] is a palindrome if and only if s[i] == s[j] and s[i+1..j-1] is a palindrome (or j - i < 2).
def build_palindrome_table(s: str) -> list[list[bool]]:
n = len(s)
# isPalin[i][j] = True if s[i:j+1] is a palindrome
isPalin = [[False] * n for _ in range(n)]
# Every single character is a palindrome
for i in range(n):
isPalin[i][i] = True
# Check substrings of length 2
for i in range(n - 1):
isPalin[i][i + 1] = (s[i] == s[i + 1])
# Check substrings of length 3 and above
# We iterate by length to ensure subproblems are solved first
for length in range(3, n + 1):
for i in range(n - length + 1):
j = i + length - 1
# Outer characters match AND inner substring is palindrome
isPalin[i][j] = (s[i] == s[j]) and isPalin[i + 1][j - 1]
return isPalin
This preprocessing takes O(n²) time and space. Now every palindrome check is O(1), which is critical for the main DP solution’s efficiency.
Complete DP Solution
With the palindrome table ready, we implement the full bottom-up solution:
def min_cut(s: str) -> int:
if not s or len(s) <= 1:
return 0
n = len(s)
# Step 1: Build palindrome lookup table
isPalin = [[False] * n for _ in range(n)]
for i in range(n):
isPalin[i][i] = True
for i in range(n - 1):
isPalin[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
isPalin[i][j] = (s[i] == s[j]) and isPalin[i + 1][j - 1]
# Step 2: Compute minimum cuts
# minCuts[i] = minimum cuts for s[0..i]
minCuts = [0] * n
for i in range(n):
if isPalin[0][i]:
# Entire prefix is palindrome, no cuts needed
minCuts[i] = 0
else:
# Worst case: cut after every character
minCuts[i] = i
# Try every possible last palindrome
for j in range(1, i + 1):
if isPalin[j][i]:
# s[j..i] is palindrome, consider cutting before j
minCuts[i] = min(minCuts[i], minCuts[j - 1] + 1)
return minCuts[n - 1]
Let’s trace through with “aabbc”:
Palindrome table (True positions):
- Single chars: [0,0], [1,1], [2,2], [3,3], [4,4]
- Length 2: [0,1] ("aa"), [2,3] ("bb")
- Length 3+: None
minCuts computation:
- i=0: isPalin[0][0]=True → minCuts[0]=0
- i=1: isPalin[0][1]=True → minCuts[1]=0
- i=2: isPalin[0][2]=False, check j=1,2
j=2: isPalin[2][2]=True → minCuts[1]+1=1
minCuts[2]=1
- i=3: isPalin[0][3]=False, check j=1,2,3
j=2: isPalin[2][3]=True → minCuts[1]+1=1
minCuts[3]=1
- i=4: isPalin[0][4]=False, check j=1,2,3,4
j=4: isPalin[4][4]=True → minCuts[3]+1=2
minCuts[4]=2
Result: 2 cuts → ["aa", "bb", "c"]
Time complexity is O(n²) for both the palindrome table and the minimum cuts computation. Space complexity is O(n²) for the palindrome table plus O(n) for the cuts array.
Optimizations & Variations
We can reduce space by building the palindrome table on-the-fly while computing cuts. This approach expands around each center rather than precomputing all palindromes:
def min_cut_optimized(s: str) -> int:
if not s or len(s) <= 1:
return 0
n = len(s)
# minCuts[i] = minimum cuts for s[0..i-1]
# We use i-1 indexing to simplify boundary handling
minCuts = list(range(-1, n)) # [-1, 0, 1, 2, ..., n-1]
for center in range(n):
# Odd-length palindromes (single center)
left, right = center, center
while left >= 0 and right < n and s[left] == s[right]:
# s[left..right] is a palindrome
minCuts[right + 1] = min(minCuts[right + 1], minCuts[left] + 1)
left -= 1
right += 1
# Even-length palindromes (two centers)
left, right = center, center + 1
while left >= 0 and right < n and s[left] == s[right]:
minCuts[right + 1] = min(minCuts[right + 1], minCuts[left] + 1)
left -= 1
right += 1
return minCuts[n]
This version uses O(n) space while maintaining O(n²) worst-case time. The key insight is that we only need to consider palindromes that actually exist in the string, not all possible substrings.
For even faster palindrome detection, Manacher’s algorithm finds all palindromic substrings in O(n) time. However, the overall complexity remains O(n²) because we still need to consider all possible cut positions.
Related problems worth exploring include counting all possible palindrome partitions (use backtracking with memoization) and finding the longest palindromic subsequence (different DP formulation).
Conclusion & Practice Problems
The minimum palindrome partitioning problem demonstrates how combining multiple DP tables solves complex optimization problems efficiently. The critical insights are recognizing the need for O(1) palindrome lookups and formulating the correct recurrence relation.
Common pitfalls include forgetting to handle the base case when the entire string is already a palindrome, off-by-one errors in index boundaries, and attempting to optimize prematurely before understanding the basic solution.
Practice these related problems to solidify your understanding:
- LeetCode 132: Palindrome Partitioning II (this exact problem)
- LeetCode 131: Palindrome Partitioning (return all partitions)
- LeetCode 5: Longest Palindromic Substring
- LeetCode 647: Palindromic Substrings (count all)
Master this pattern, and you’ll handle most substring DP problems with confidence.