131(132、1278)-分割回文串Ⅰ、Ⅱ、Ⅲ-字符串DP问题

写在前面

这三道题可以算是一个小系列了,都是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:最近在家呆久了真的颓废,就是不想学习,感觉干啥都行就是懒得学,做做题写写博客找找状态。

你可能感兴趣的:(131(132、1278)-分割回文串Ⅰ、Ⅱ、Ⅲ-字符串DP问题)