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
.
s = "aab"
, a cut after the first character results in "a|ab"
.
Constraints:
1 <= s.length <= 2000
s
consists of lowercase English letters only.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.
We solve the problem using dynamic programming with two main steps:
is_palindrome[i][j]
where each entry is True
if the substring s[i:j+1]
is a palindrome.
dp
where dp[i]
represents the minimum number of cuts needed for the substring s[0:i+1]
.
i
in the string:
s[0:i+1]
is a palindrome, then dp[i] = 0
(no cuts needed).
j <= i
, if s[j:i+1]
is a palindrome, update dp[i]
as min(dp[i], dp[j-1] + 1)
.
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.
Let's take s = "aab"
as an example.
"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 palindromedp[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
1
, corresponding to "aa|b".
This is efficient enough for n
up to 2000.
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.
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];
};