132. Palindrome Partitioning II
Problem Description
Given a string s
, you need to partition it into substrings where every substring is a palindrome. The goal is to find the minimum number of cuts needed to achieve such a partitioning.
A palindrome is a string that reads the same forwards and backwards (like "aba" or "racecar"). A partition means dividing the string into consecutive non-overlapping substrings.
For example:
- If
s = "aab"
, you can partition it as["aa", "b"]
with 1 cut (between the second 'a' and 'b') - If
s = "aba"
, the entire string is already a palindrome, so 0 cuts are needed
The problem asks you to return the minimum number of cuts required to ensure every resulting substring is a palindrome.
Intuition
To find the minimum cuts, we need to think about this problem systematically. Every cut we make creates a new partition point, and we want to minimize these cuts while ensuring all resulting substrings are palindromes.
The key insight is that this is an optimization problem that can be solved using dynamic programming. We need to break it down into smaller subproblems.
First, we realize that to efficiently determine where to make cuts, we need to know which substrings of s
are palindromes. If we keep checking whether each substring is a palindrome repeatedly, it would be inefficient. So, we can precompute this information for all possible substrings using a 2D table g[i][j]
, where g[i][j]
tells us if the substring from index i
to j
is a palindrome.
Next, we think about how to find the minimum cuts. For any position i
in the string, we ask: "What's the minimum number of cuts needed to partition s[0...i]
into palindromes?"
To answer this, we consider all possible positions j
where we could make the last cut before position i
. If the substring s[j...i]
is a palindrome, then we can make a cut at position j-1
and the minimum cuts for s[0...i]
would be the minimum cuts for s[0...j-1]
plus one additional cut.
However, there's a special case: if the entire substring s[0...i]
is itself a palindrome, then we don't need any cuts at all for this portion.
By building up the solution from smaller substrings to larger ones, we can efficiently compute the minimum cuts needed for the entire string. The answer will be the minimum cuts needed for the complete string s[0...n-1]
.
Solution Approach
The implementation consists of two main phases: preprocessing to identify palindromes and dynamic programming to find minimum cuts.
Phase 1: Palindrome Preprocessing
We create a 2D boolean array g[i][j]
where g[i][j] = True
if substring s[i...j]
is a palindrome. We fill this table from bottom-right to top-left:
g = [[True] * n for _ in range(n)]
for i in range(n - 1, -1, -1):
for j in range(i + 1, n):
g[i][j] = s[i] == s[j] and g[i + 1][j - 1]
The logic here is:
- All single characters are palindromes (diagonal elements are
True
by default) - For substring
s[i...j]
, it's a palindrome ifs[i] == s[j]
AND the inner substrings[i+1...j-1]
is also a palindrome - We iterate backwards on
i
to ensureg[i+1][j-1]
is computed beforeg[i][j]
Phase 2: Dynamic Programming for Minimum Cuts
We define f[i]
as the minimum cuts needed for substring s[0...i]
. Initially, we set f[i] = i
(worst case: cut between every character).
f = list(range(n))
for i in range(1, n):
for j in range(i + 1):
if g[j][i]:
f[i] = min(f[i], 1 + f[j - 1] if j else 0)
For each position i
, we check all possible last palindrome segments s[j...i]
:
- If
g[j][i]
isTrue
(substrings[j...i]
is a palindrome), we have two cases:- If
j = 0
: The entires[0...i]
is a palindrome, sof[i] = 0
(no cuts needed) - If
j > 0
: We needf[j-1] + 1
cuts (cuts fors[0...j-1]
plus one cut at positionj-1
)
- If
- We take the minimum among all valid options
The state transition equation is:
The final answer is f[n-1]
, representing the minimum cuts for the entire string.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with s = "aab"
.
Step 1: Palindrome Preprocessing
We create a 3×3 table g[i][j]
to identify all palindromic substrings:
Initial state (all single characters are palindromes):
0 1 2 0 [True, ?, ?] 1 [ -,True, ?] 2 [ -, -,True]
Now we fill from bottom-right to top-left:
g[1][2]
: Is "ab" a palindrome?s[1] == s[2]
? ('a' == 'b'?) No →g[1][2] = False
g[0][1]
: Is "aa" a palindrome?s[0] == s[1]
? ('a' == 'a'?) Yes, andg[1][0]
(empty substring) is True →g[0][1] = True
g[0][2]
: Is "aab" a palindrome?s[0] == s[2]
? ('a' == 'b'?) No →g[0][2] = False
Final palindrome table:
0 1 2 0 [True, True, False] // "a", "aa", "aab" 1 [ -, True, False] // "a", "ab" 2 [ -, -, True] // "b"
Step 2: Dynamic Programming for Minimum Cuts
Initialize f = [0, 1, 2]
(worst case: cut after each character).
For i = 1
(substring "aa"):
- Check
j = 0
: Isg[0][1]
(substring "aa") a palindrome? Yes!- Since
j = 0
, the entire "aa" is a palindrome →f[1] = 0
- Since
- Check
j = 1
: Isg[1][1]
(substring "a") a palindrome? Yes!f[1] = min(0, f[0] + 1) = min(0, 1) = 0
- Result:
f[1] = 0
For i = 2
(substring "aab"):
- Check
j = 0
: Isg[0][2]
(substring "aab") a palindrome? No, skip. - Check
j = 1
: Isg[1][2]
(substring "ab") a palindrome? No, skip. - Check
j = 2
: Isg[2][2]
(substring "b") a palindrome? Yes!f[2] = min(2, f[1] + 1) = min(2, 0 + 1) = 1
- Result:
f[2] = 1
Final f = [0, 0, 1]
Answer: 1 cut is needed (partition "aab" into ["aa", "b"] with a cut between position 1 and 2).
Solution Implementation
1class Solution:
2 def minCut(self, s: str) -> int:
3 n = len(s)
4
5 # Build a 2D table to store palindrome information
6 # is_palindrome[i][j] = True if s[i:j+1] is a palindrome
7 is_palindrome = [[True] * n for _ in range(n)]
8
9 # Fill the palindrome table using dynamic programming
10 # Start from the end of string and work backwards
11 for start in range(n - 1, -1, -1):
12 for end in range(start + 1, n):
13 # A substring is palindrome if:
14 # 1. First and last characters match
15 # 2. The inner substring is also a palindrome (or length <= 2)
16 is_palindrome[start][end] = (s[start] == s[end] and
17 is_palindrome[start + 1][end - 1])
18
19 # min_cuts[i] represents minimum cuts needed for s[0:i+1]
20 # Initialize with worst case: i cuts for string of length i+1
21 min_cuts = list(range(n))
22
23 # Calculate minimum cuts for each position
24 for i in range(1, n):
25 # Check all possible last palindrome ending at position i
26 for j in range(i + 1):
27 # If s[j:i+1] is a palindrome
28 if is_palindrome[j][i]:
29 if j == 0:
30 # If entire s[0:i+1] is palindrome, no cuts needed
31 min_cuts[i] = 0
32 else:
33 # Otherwise, it's cuts for s[0:j] plus one more cut
34 min_cuts[i] = min(min_cuts[i], 1 + min_cuts[j - 1])
35
36 # Return minimum cuts for entire string
37 return min_cuts[-1]
38
1class Solution {
2 public int minCut(String s) {
3 int n = s.length();
4
5 // isPalindrome[i][j] indicates whether substring s[i...j] is a palindrome
6 boolean[][] isPalindrome = new boolean[n][n];
7
8 // Initialize all entries as true (single characters and empty strings are palindromes)
9 for (boolean[] row : isPalindrome) {
10 Arrays.fill(row, true);
11 }
12
13 // Build palindrome table using dynamic programming
14 // Start from the end and work backwards to ensure smaller subproblems are solved first
15 for (int start = n - 1; start >= 0; start--) {
16 for (int end = start + 1; end < n; end++) {
17 // A substring is a palindrome if:
18 // 1. First and last characters match
19 // 2. The substring between them is also a palindrome
20 isPalindrome[start][end] = (s.charAt(start) == s.charAt(end))
21 && isPalindrome[start + 1][end - 1];
22 }
23 }
24
25 // minCuts[i] represents the minimum cuts needed for substring s[0...i]
26 int[] minCuts = new int[n];
27
28 // Initialize with worst case: cut after every character
29 for (int i = 0; i < n; i++) {
30 minCuts[i] = i;
31 }
32
33 // Calculate minimum cuts for each position
34 for (int end = 1; end < n; end++) {
35 // Check all possible starting positions for the last palindrome partition
36 for (int start = 0; start <= end; start++) {
37 // If s[start...end] is a palindrome
38 if (isPalindrome[start][end]) {
39 // If the palindrome starts at index 0, no cuts needed for this substring
40 // Otherwise, we need 1 cut plus the minimum cuts for s[0...start-1]
41 minCuts[end] = Math.min(minCuts[end],
42 start > 0 ? 1 + minCuts[start - 1] : 0);
43 }
44 }
45 }
46
47 // Return minimum cuts needed for the entire string
48 return minCuts[n - 1];
49 }
50}
51
1class Solution {
2public:
3 int minCut(string s) {
4 int n = s.size();
5
6 // isPalindrome[i][j] indicates whether substring s[i...j] is a palindrome
7 vector<vector<bool>> isPalindrome(n, vector<bool>(n, true));
8
9 // Build the palindrome lookup table using dynamic programming
10 // Start from the end of string and work backwards
11 for (int start = n - 1; start >= 0; --start) {
12 for (int end = start + 1; end < n; ++end) {
13 // A substring is palindrome if:
14 // 1. First and last characters match
15 // 2. Inner substring is also a palindrome (or length <= 2)
16 isPalindrome[start][end] = (s[start] == s[end]) && isPalindrome[start + 1][end - 1];
17 }
18 }
19
20 // minCuts[i] represents minimum cuts needed for substring s[0...i]
21 vector<int> minCuts(n);
22
23 // Initialize: worst case is to cut between every character
24 for (int i = 0; i < n; ++i) {
25 minCuts[i] = i; // Maximum i cuts needed for string of length i+1
26 }
27
28 // Calculate minimum cuts for each position
29 for (int end = 1; end < n; ++end) {
30 for (int start = 0; start <= end; ++start) {
31 // If s[start...end] is a palindrome
32 if (isPalindrome[start][end]) {
33 if (start == 0) {
34 // Entire substring from beginning is palindrome, no cuts needed
35 minCuts[end] = 0;
36 } else {
37 // Add one cut after position (start-1)
38 minCuts[end] = min(minCuts[end], minCuts[start - 1] + 1);
39 }
40 }
41 }
42 }
43
44 // Return minimum cuts for entire string
45 return minCuts[n - 1];
46 }
47};
48
1/**
2 * Finds the minimum number of cuts needed to partition a string into palindromes
3 * @param s - The input string to partition
4 * @returns The minimum number of cuts needed
5 */
6function minCut(s: string): number {
7 const length: number = s.length;
8
9 // isPalindrome[i][j] indicates whether substring s[i...j] is a palindrome
10 const isPalindrome: boolean[][] = Array.from(
11 { length: length },
12 () => Array(length).fill(true)
13 );
14
15 // Build palindrome lookup table using dynamic programming
16 // Start from the end of string and work backwards
17 for (let startIndex = length - 1; startIndex >= 0; startIndex--) {
18 for (let endIndex = startIndex + 1; endIndex < length; endIndex++) {
19 // A substring is palindrome if:
20 // 1. Characters at both ends match
21 // 2. Inner substring is also a palindrome (or length <= 2)
22 isPalindrome[startIndex][endIndex] =
23 s[startIndex] === s[endIndex] &&
24 isPalindrome[startIndex + 1][endIndex - 1];
25 }
26 }
27
28 // minCuts[i] represents minimum cuts needed for substring s[0...i]
29 // Initialize with worst case: i cuts (each character as separate palindrome)
30 const minCuts: number[] = Array.from(
31 { length: length },
32 (_, index) => index
33 );
34
35 // Calculate minimum cuts for each position
36 for (let endPos = 1; endPos < length; endPos++) {
37 for (let startPos = 0; startPos <= endPos; startPos++) {
38 // If s[startPos...endPos] is a palindrome
39 if (isPalindrome[startPos][endPos]) {
40 if (startPos === 0) {
41 // Entire substring from beginning is palindrome, no cuts needed
42 minCuts[endPos] = 0;
43 } else {
44 // One cut after position (startPos - 1) plus previous minimum cuts
45 minCuts[endPos] = Math.min(
46 minCuts[endPos],
47 1 + minCuts[startPos - 1]
48 );
49 }
50 }
51 }
52 }
53
54 // Return minimum cuts for entire string
55 return minCuts[length - 1];
56}
57
Time and Space Complexity
The time complexity is O(n^2)
, where n
is the length of the string s
. This is because:
- The first nested loop (building the palindrome table
g
) iterates through all pairs(i, j)
wherei
goes fromn-1
to0
andj
goes fromi+1
ton
, resulting inO(n^2)
operations. - The second nested loop (computing minimum cuts in array
f
) has an outer loop from1
ton
and an inner loop from0
toi+1
, which also gives usO(n^2)
operations in total. - Since both parts are
O(n^2)
and run sequentially, the overall time complexity remainsO(n^2)
.
The space complexity is O(n^2)
, where n
is the length of the string s
. This is because:
- The 2D boolean array
g
of sizen × n
is used to store whether each substrings[i:j+1]
is a palindrome, requiringO(n^2)
space. - The 1D array
f
of sizen
is used to store the minimum number of cuts needed for each prefix, requiringO(n)
space. - Since
O(n^2) + O(n) = O(n^2)
, the overall space complexity isO(n^2)
.
Common Pitfalls
1. Incorrect Palindrome Table Initialization for Length-2 Substrings
The Pitfall:
When checking if a substring is a palindrome, the condition is_palindrome[start][end] = (s[start] == s[end] and is_palindrome[start + 1][end - 1])
can fail for length-2 substrings. When end = start + 1
, accessing is_palindrome[start + 1][end - 1]
means accessing is_palindrome[start + 1][start]
, which represents an invalid range where the end index is before the start index.
While the code initializes all values to True
, relying on this for invalid ranges is implicit and can lead to confusion or bugs if the initialization changes.
Example of the issue: For string "ab" where start=0, end=1:
- We check
is_palindrome[1][0]
which doesn't represent a valid substring - It works only because we initialized everything to
True
Solution: Explicitly handle base cases for substrings of length 1 and 2:
for start in range(n - 1, -1, -1):
for end in range(start + 1, n):
if end - start == 1:
# Length-2 substring: only check if characters match
is_palindrome[start][end] = (s[start] == s[end])
else:
# Length > 2: check both ends and inner substring
is_palindrome[start][end] = (s[start] == s[end] and
is_palindrome[start + 1][end - 1])
2. Optimization Opportunity: Early Termination
The Pitfall:
The current implementation always checks all possible partitions even when we've already found that no cuts are needed (when min_cuts[i] = 0
). This wastes computational cycles.
Solution: Add a break statement when we find the entire substring from start is a palindrome:
for i in range(1, n):
for j in range(i + 1):
if is_palindrome[j][i]:
if j == 0:
min_cuts[i] = 0
break # No need to check other partitions
else:
min_cuts[i] = min(min_cuts[i], 1 + min_cuts[j - 1])
3. Memory Optimization Consideration
The Pitfall:
The palindrome table uses O(n²) space even though we only check upper triangle values (where end >= start
). This can cause memory issues for very long strings.
Solution: Only store the upper triangle of the palindrome table or use a different data structure:
# Alternative: Use a dictionary to store only valid palindrome ranges
is_palindrome = {}
for length in range(1, n + 1):
for start in range(n - length + 1):
end = start + length - 1
if length == 1:
is_palindrome[(start, end)] = True
elif length == 2:
is_palindrome[(start, end)] = (s[start] == s[end])
else:
is_palindrome[(start, end)] = (s[start] == s[end] and
is_palindrome.get((start + 1, end - 1), False))
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!