题目链接: https://leetcode.com/problems/palindrome-partitioning-ii/
Given a string s, partition s such that every substring of the partition is a palindrome.
Return the minimum cuts needed for a palindrome partitioning of s.
For example, given s = "aab"
,
Return 1
since the palindrome partitioning ["aa","b"]
could be produced using 1 cut.
思路:本题可以用动态规划来做,也可以用divide and conquer来做.在用divide and conquer 做的时候可以依次枚举每个分割点将字符串不断二分.如果当前字符串为回文的时候需要的切点为0,否则当前字符串需要的切点就为左右两边子串需要的切点数目+1.最后取各个切点的最小值.但是naive的分治包含大量的重复计算,因此需要记忆化.我觉得分治比较好理解.
divide and conquer是自上而下的计算,而动态规划则是自下而上的计算.动态规划是将问题转化为子集问题,缩小问题的规模,也就是说先从最小的子集开始计算,由小的状态得出更大集合的状态.而在本题中,如何将问题分割成子状态呢?将一个字符串切割成两部分,如果右边是回文子串,那么需要的切点就为:左边的子串切点+1,如果右边非回文,则需要的切点就为:左边子串切点+1+右边子串长度-1.这样就将问题分解成了子状态+一个已知状态.我们只需要枚举将字符串分割的切点,取一个最优的解即可计算一个字符串的解,然后这个解又会作为子集解用于解决更大规模的问题,直到将整个问题解决.状态转移方程即为:dp[i] = min(dp[i], 1+dp[j] + val(str[j, i])), 其中j为枚举[0-i]之间的所有点,val[str[j, i]]为右子串的解,如果右子串为回文需要切点为0,否则就是其长度-1.
再来分析一下时间复杂度,有两重循环是很明显的,一个是枚举长度由小到大的字符串,也就是小规模的问题.在解决这个子问题我们还需要一重循环来枚举各个分割点将子串分为一个更小子问题解+已知状态.在判断右子串是否回文的时候还有一个隐藏的一重循环.也就是说我们需要O(n^3)的时间复杂度.但是在判断右子串是否回文的问题上我们可以优化.在子串由小变大的过程中,可以将每次的结果保存起来,这样在字符串不断扩大的时候只需要判断两个端点是否相等和子串是否回文即可,这也是一个空间换时间的方法.这样就可以将时间复杂度降为O(n^2).
还有一个初始状态的问题,如果将字符串左边为空的话需要的切点个数为:右子串切点个数,因此dp[0]=-1,这个-1是为了抵消当前这个分割点的计数.
代码如下:
class Solution {
public:
int minCut(string s) {
if(s.size() ==0) return 0;
int len = s.size();
vector<int> dp(len+1, INT_MAX);
vector<vector<bool>> palin(len, vector<bool>(len, false));
dp[0] = -1;
for(int i = 1; i <= s.size(); i++)
{
for(int j = 0; j < i; j++)
{
if(s[j]==s[i-1] && (i-j<=2 || palin[j+1][i-2]))
palin[j][i-1] = true;
dp[i] = min(dp[i], dp[j]+ (palin[j][i-1]?1:i-j));
}
}
return dp[len];
}
};