Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

132. Palindrome Partitioning II - Leetcode Solution

Problem Description

Given a string s, partition s such that every substring of the partition is a palindrome. Return the minimum number of cuts needed for a palindrome partitioning of s.

  • A cut is made between two characters in the string. For example, for s = "aab", a cut after the first character results in "a|ab".
  • Each substring after the cuts must be a palindrome (i.e., it reads the same forward and backward).
  • The goal is to minimize the number of cuts needed so that every substring is a palindrome.
  • You must return the minimum number of cuts required.

Constraints:

  • 1 <= s.length <= 2000
  • s consists of lowercase English letters only.

Thought Process

The most straightforward way to solve this problem is to try every possible way to cut the string and check if the resulting substrings are palindromes. However, this approach is highly inefficient because the number of ways to cut a string grows exponentially, and checking each substring for being a palindrome also takes time.

To optimize, we can use dynamic programming by breaking the problem into subproblems: for each position in the string, we can keep track of the minimum number of cuts needed up to that point. We also notice that checking if a substring is a palindrome can be done efficiently if we precompute this information for all possible substrings.

The key insight is that if we know which substrings are palindromes, we can quickly determine the minimum cuts required by looking at previous results. This transforms the problem from a brute-force search into a manageable DP problem.

Solution Approach

We solve the problem using dynamic programming with two main steps:

  1. Precompute Palindromic Substrings:
    • Create a 2D table is_palindrome[i][j] where each entry is True if the substring s[i:j+1] is a palindrome.
    • We fill this table in O(n2) time by checking all possible substring lengths and using the property that a substring is a palindrome if its ends match and its inner substring is a palindrome.
  2. Dynamic Programming for Minimum Cuts:
    • Create a 1D array dp where dp[i] represents the minimum number of cuts needed for the substring s[0:i+1].
    • For each position i in the string:
      • If s[0:i+1] is a palindrome, then dp[i] = 0 (no cuts needed).
      • Otherwise, for each possible j <= i, if s[j:i+1] is a palindrome, update dp[i] as min(dp[i], dp[j-1] + 1).
    • The answer is dp[n-1], where n is the length of s.

Why this works: By precomputing palindromic substrings, we can check each possible cut efficiently. The DP array ensures we always use the minimum number of cuts up to each position.

Example Walkthrough

Let's take s = "aab" as an example.

  1. Precompute palindromes:
    • "a" at (0,0): palindrome
    • "a" at (1,1): palindrome
    • "b" at (2,2): palindrome
    • "aa" at (0,1): palindrome
    • "ab" at (1,2): not palindrome
    • "aab" at (0,2): not palindrome
  2. Dynamic programming:
    • dp[0] = 0 ("a" is a palindrome)
    • dp[1]: "aa" is a palindrome, so dp[1] = 0
    • dp[2]: "aab" is not a palindrome. Check "ab" (not palindrome), "b" (palindrome). So, dp[2] = min(dp[1]+1, dp[0]+1) = min(1,1) = 1
  3. Result:
    • The minimum number of cuts is 1, corresponding to "aa|b".

Time and Space Complexity

  • Brute-force approach:
    • Time complexity: O(2n * n) (since there are 2n-1 ways to cut and each check could take O(n))
    • Space complexity: O(n) for recursion stack, but potentially higher with memoization.
  • Optimized DP approach:
    • Time complexity: O(n2) for both precomputing palindromes and filling the DP table.
    • Space complexity: O(n2) for the palindrome table, O(n) for the DP array.

This is efficient enough for n up to 2000.

Summary

The palindrome partitioning II problem is an excellent example of how dynamic programming can transform an exponential brute-force problem into a tractable one. By precomputing palindromic substrings and using a DP array to store the minimum number of cuts required, we can efficiently solve the problem in O(n2) time. The key insight is to build up solutions for smaller substrings and reuse them, avoiding redundant computations.

Code Implementation

class Solution:
    def minCut(self, s: str) -> int:
        n = len(s)
        is_palindrome = [[False]*n for _ in range(n)]
        for end in range(n):
            for start in range(end+1):
                if s[start] == s[end] and (end-start <= 2 or is_palindrome[start+1][end-1]):
                    is_palindrome[start][end] = True

        dp = [0] * n
        for i in range(n):
            if is_palindrome[0][i]:
                dp[i] = 0
            else:
                dp[i] = i
                for j in range(1, i+1):
                    if is_palindrome[j][i]:
                        dp[i] = min(dp[i], dp[j-1] + 1)
        return dp[-1]
      
class Solution {
public:
    int minCut(string s) {
        int n = s.size();
        vector<vector<bool>> is_palindrome(n, vector<bool>(n, false));
        for (int end = 0; end < n; ++end) {
            for (int start = 0; start <= end; ++start) {
                if (s[start] == s[end] && (end - start <= 2 || is_palindrome[start+1][end-1])) {
                    is_palindrome[start][end] = true;
                }
            }
        }
        vector<int> dp(n, 0);
        for (int i = 0; i < n; ++i) {
            if (is_palindrome[0][i]) {
                dp[i] = 0;
            } else {
                dp[i] = i;
                for (int j = 1; j <= i; ++j) {
                    if (is_palindrome[j][i]) {
                        dp[i] = min(dp[i], dp[j-1] + 1);
                    }
                }
            }
        }
        return dp[n-1];
    }
};
      
class Solution {
    public int minCut(String s) {
        int n = s.length();
        boolean[][] isPalindrome = new boolean[n][n];
        for (int end = 0; end < n; end++) {
            for (int start = 0; start <= end; start++) {
                if (s.charAt(start) == s.charAt(end) && (end - start <= 2 || isPalindrome[start+1][end-1])) {
                    isPalindrome[start][end] = true;
                }
            }
        }
        int[] dp = new int[n];
        for (int i = 0; i < n; i++) {
            if (isPalindrome[0][i]) {
                dp[i] = 0;
            } else {
                dp[i] = i;
                for (int j = 1; j <= i; j++) {
                    if (isPalindrome[j][i]) {
                        dp[i] = Math.min(dp[i], dp[j-1] + 1);
                    }
                }
            }
        }
        return dp[n-1];
    }
}
      
var minCut = function(s) {
    const n = s.length;
    const isPalindrome = Array.from({length: n}, () => Array(n).fill(false));
    for (let end = 0; end < n; end++) {
        for (let start = 0; start <= end; start++) {
            if (s[start] === s[end] && (end - start <= 2 || isPalindrome[start+1][end-1])) {
                isPalindrome[start][end] = true;
            }
        }
    }
    const dp = Array(n).fill(0);
    for (let i = 0; i < n; i++) {
        if (isPalindrome[0][i]) {
            dp[i] = 0;
        } else {
            dp[i] = i;
            for (let j = 1; j <= i; j++) {
                if (isPalindrome[j][i]) {
                    dp[i] = Math.min(dp[i], dp[j-1] + 1);
                }
            }
        }
    }
    return dp[n-1];
};