写在前面
这三道题可以算是一个小系列了,都是DP相关的问题。因为没怎么做过字符串DP的相关问题,在定义数组的时候真的是犯了难,看题解总算是看懂了,希望能融会贯通一点吧
分割回文串Ⅰ
题目
核心思路
这道题并不算是DP问题,因为要枚举所有的分割方案,所以要遍历每一种可能,属于回溯算法,通过栈加递归就可以很容易的解决了。
递归思路
递归想问题就是直接怎么想就怎么写就可以,再加上栈的回溯,先压栈,再操作,再弹栈即可。转换为代码可以变成:
temp.push(s.substring(start, i + 1));
helper(s, temp, ans, i + 1, len);
temp.pop();
代码中用temp
表示深度遍历的路径,当最后传入的字符串为空串""
时,将这个路径temp
保存到ans
里即可。
完整代码
根据上边的简单回溯思路很容易得到下边的代码
public List> partition(String s) {
List> ans = new ArrayList>();
helper(s, new Stack(), ans, 0, s.length());
return ans;
}
public void helper(String s, Stack temp, List> ans, int start, int len) {
if (start == len) {
ans.add(new ArrayList(temp));
return;
}
for (int i = start; i < len; i++) {
if (isRecyle(s, start, i)) {
temp.push(s.substring(start, i + 1));
helper(s, temp, ans, i + 1, len);
temp.pop();
}
}
}
public boolean isRecyle(String s, int start, int end) {
int l = start, h = end;
while (l < h) {
if (s.charAt(l) != s.charAt(h)) {
return false;
}
l++;
h--;
}
return true;
}
优化的可能
根据官方题解给出的优化方法,利用第五题最长回文子串的方法,可以通过预处理提前搞定是否是回文串的判断,具体方法还请看官方题解。
分割回文串Ⅱ
题目
核心思路
根据题目的问题,可以定义dp数组,dp[i]表示从开始位置到下标i位置的最少分割次数
。既然是动态规划问题,那么一定要使用之前计算的结果,也就是找到dp[j]
与dp[i - 1], dp[i - 2], dp[i - 3]....
的关系,而i - 1, i - 2, i - 3... 是i的子串的最小分割次数
,所以想利用上这些结果的话,剩余部分,即i - 1 ~ i, i - 2 ~ i ...
对应部分要满足是回文串,所以只要遍历每一个可能的之前计算结果,对于右边剩余部分子串是回文串的,就保留这个之前的结果,然后加1就是一个可能的结果,再对所有这些可能的结果取最小值既是最后所求的数值。
完整代码
通过上边的分析,可以得到以下的代码
public int minCut(String s) {
int[] dp = new int[s.length()];
for(int i = 1; i < s.length(); i++){
if(isRecyle(s, 0, i)){
continue;
}
int temp = i;
for(int j = 0; j < i; j++){
if(isRecyle(s, j + 1, i)){
temp = Math.min(temp, dp[j] + 1);
}
}
dp[i] = temp;
}
return dp[s.length() - 1];
}
public boolean isRecyle(String s, int start, int end) {
int l = start, h = end;
while (l < h) {
if (s.charAt(l) != s.charAt(h)) {
return false;
}
l++;
h--;
}
return true;
}
这样的代码提交后时间大概为600ms,时间占用很大,显然需要优化,对于更新dp数组的部分,两次遍历基本上算是很极限不可化简了,所以唯一能化简的地方就是isRecyle
函数,对于每次的遍历都要接近O(n)的时间来判断是否是回文串,因此这里是一个可以优化的点。
优化方法
由于判断是否是回文串时如果一个串的子串已经判断过了,在判断它(指前边的一个串)时就会重复很多的步骤,所以可以进行预处理,提前将每个子串是否是回文串进行判断保存,在后边的使用过程中直接使用数组存储的结果就可以大大优化时间。
代码如下:
public int minCut(String s) {
int len = s.length();
boolean[][] checkPalindrome = new boolean[len][len];
for (int right = 0; right < len; right++) {
// 注意:left <= right 取等号表示 1 个字符的时候也需要判断
for (int left = 0; left <= right; left++) {
if (s.charAt(left) == s.charAt(right) && (right - left <= 2 || checkPalindrome[left + 1][right - 1])){
checkPalindrome[left][right] = true;
}
}
}
int[] dp = new int[s.length()];
for(int i = 1; i < s.length(); i++){
if(checkPalindrome[0][i]){
continue;
}
int temp = i;
for(int j = 0; j < i; j++){
if(checkPalindrome[j + 1][i]){
temp = Math.min(temp, dp[j] + 1);
}
}
dp[i] = temp;
}
return dp[s.length() - 1];
}
//用上边的预处理代替函数调用
// public boolean isRecyle(String s, int start, int end) {
// int l = start, h = end;
// while (l < h) {
// if (s.charAt(l) != s.charAt(h)) {
// return false;
// }
// l++;
// h--;
// }
// return true;
// }
}
通过优化预处理,提交的时间得到大大优化14ms,是一个比较理想的结果。
分割回文串Ⅲ
题目
核心思路
对比于第二道题,这道题限制了只能分割为k个子串,同时可以通过修改字母使得结果为回文串,求解的答案为最少的修改次数。虽然三道题都叫分割回文串,但实际上三者的差别还是比较大的。
这道题要求解的是字符串s
分割为k
个回文串所需要修改字母的最少次数。可以想到,s和k
是可以分解的,字符串可以变成一个个子串,而分割k
次可以通过一次一次的分割组成,所以这两个部分可以用来划分子问题。
划分子问题
通过上述两个可分解部分,可以想到最小子问题:只有一个字符,分割为一个回文串
。显然结果是0
,不需要修改字符,分割次数可以一次次叠加即可,但是字符串的划分似乎可以有1. 考虑每个子串,2. 考虑从头开始的每个子串
这两种可能,这里我也没想明白,不过我认为第二种可能已经可以遍历得到最终的结果了,而第一种多计算了一些可能性,所以最终选用第二种,不对的话还请指正。这样就可以定义dp数组
了,dp[i][j]表示字符串s从开头到第i个字符的子串,分割为j个回文串所需要修改的最小次数
,很显然i的范围就是字符串的长度,j的范围就是i, k的最小值(因为i个字符最多划分为i个子串)
,接下来就要考虑状态转移了。
状态转移方程
想要求解dp[i][j]
,即前i个字符,分割为j个子回文串所需要修改的最小次数,那么它的前一个状态就是前i个字符的某个子串,分割为j - 1个子回文串所需要修改的最小次数,所以可以遍历子串结束的位置end,那么dp[i][j] 与 dp[end][j - 1]有相关关系
,不过要想计算出来还需要考虑剩余的部分end到i位置的子串转换为回文串需要修改的次数
。这里可以定义一个cost函数来计算修改次数,也可以提前处理并存储,就像第二题那样,存储结果在一个数组中,就可以直接取来用,节约了处理时间。
预处理
由于上述的end 和 i
都是随意遍历字符串的,所以他们的可能就代表了字符串s
的所有子串,所以定义一个二维数组times[i][j]
表示i到j子串变为回文串所需要修改字符的次数
,有了数组,实际写起来就很简单了,直接用动态规范遍历子串长度和子串开始位置然后更新即可,这里比较简单就不再赘述了。
int len = s.length();
int[][] times = new int[len][len];
for (int l = 2; l <= len; l++) {
for (int i = 0; i <= len - l; i++) {
int end = i + l - 1;
times[i][end] = times[i + 1][end - 1] + (s.charAt(i) == s.charAt(end) ? 0 : 1);
}
}
完整代码
这里要注意一下计算dp数组时,当j等于1,也就是将目的子串分割为一个回文串的最小修改次数,对应的值也就是times[i][j]
class Solution {
public int palindromePartition(String s, int k) {
int len = s.length();
int[][] times = new int[len][len];
for (int l = 2; l <= len; l++) {
for (int i = 0; i <= len - l; i++) {
int end = i + l - 1;
times[i][end] = times[i + 1][end - 1] + (s.charAt(i) == s.charAt(end) ? 0 : 1);
}
}
int[][] dp = new int[len + 1][k + 1];// dp[i][j]表示前i的字符,分割为j个回文串所需的次数
for (int i = 1; i <= len; i++) {
for (int j = 1; j <= i && j <= k; j++) {
if (j == 1) {
dp[i][j] = times[0][i - 1];
} else {
dp[i][j] = Integer.MAX_VALUE;
for (int start = j - 1; start < i; start++) {
dp[i][j] = Math.min(dp[i][j], dp[start][j - 1] + times[start][i - 1]);
}
}
}
}
return dp[len][k];
}
}
总结
DP问题的dp数组定义还真是一大难题,想不出合适的数组定义就做不出来,还只能靠经验去积累,真的是一个慢工啊,慢慢积累,慢慢学习找思路,希望下次遇到同类型可以迎刃而解。如果文章有写的不对的地方还请指正,感恩相遇~
PS:最近在家呆久了真的颓废,就是不想学习,感觉干啥都行就是懒得学,做做题写写博客找找状态。