Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 if s[i] == s[j] AND the inner substring s[i+1...j-1] is also a palindrome
  • We iterate backwards on i to ensure g[i+1][j-1] is computed before g[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] is True (substring s[j...i] is a palindrome), we have two cases:
    • If j = 0: The entire s[0...i] is a palindrome, so f[i] = 0 (no cuts needed)
    • If j > 0: We need f[j-1] + 1 cuts (cuts for s[0...j-1] plus one cut at position j-1)
  • We take the minimum among all valid options

The state transition equation is: f[i]=min0ji{f[j1]+1,if g[j][i]=True0,if g[0][i]=Truef[i] = \min_{0 \leq j \leq i} \begin{cases} f[j-1] + 1, & \text{if } g[j][i] = \text{True} \\ 0, & \text{if } g[0][i] = \text{True} \end{cases}

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 Evaluator

Example 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, and g[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: Is g[0][1] (substring "aa") a palindrome? Yes!
    • Since j = 0, the entire "aa" is a palindrome → f[1] = 0
  • Check j = 1: Is g[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: Is g[0][2] (substring "aab") a palindrome? No, skip.
  • Check j = 1: Is g[1][2] (substring "ab") a palindrome? No, skip.
  • Check j = 2: Is g[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) where i goes from n-1 to 0 and j goes from i+1 to n, resulting in O(n^2) operations.
  • The second nested loop (computing minimum cuts in array f) has an outer loop from 1 to n and an inner loop from 0 to i+1, which also gives us O(n^2) operations in total.
  • Since both parts are O(n^2) and run sequentially, the overall time complexity remains O(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 size n × n is used to store whether each substring s[i:j+1] is a palindrome, requiring O(n^2) space.
  • The 1D array f of size n is used to store the minimum number of cuts needed for each prefix, requiring O(n) space.
  • Since O(n^2) + O(n) = O(n^2), the overall space complexity is O(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))
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More