动态规划解最长子序列子串等一类问题之让字符串成为回文及其Follow Up[Sika Deer]

Posted 阿飞算法

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划解最长子序列子串等一类问题之让字符串成为回文及其Follow Up[Sika Deer]相关的知识,希望对你有一定的参考价值。

走过路过不要错过

点击蓝字关注我吧

这是个好题目

0.源起

1.Step1

源问题,如上图,让字符串成为回文串的最少插入次数

定义状态

  • nstr的长度,dp[n][n]是一张二维表

  • 定义:dp[i][j]表示的是子串str[i][j]最少添加多少字符可以使其整体变成一个回文,注意,是最少

  • 要求的结果是dp[0][n-1]表示的是str[0…n-1]也就是这个字符从开头到结尾,要想形成回文,最少的添加字符的个数

问题来了,如何填写这张dp table呢?

  • 分三大类讨论

  • 1.str[i…j]只有一个字符,即i==j,如图,str[i…j]=A,一个字符要形成回文,很简单,不要向其增加字符,即其本身可以形成回文,即dp[i][j]=0

  • 2.str[i…j]有两个字符

  • 2.1,当str[i]==str[j],即两个字符相同时,其与场景1的情况一样,例如,AA不需要添加字符,即可使其成为回文,即dp[i][j]=0

  • 2.2,当str[i]!=str[j],即两个字符不同时,例如,AB,可以在前面加一个B,变成BAB,也可以在后面加一个A,变成ABA,总之是只要添加一个字符,即可使其变成回文,即dp[i][j]=1

  • 3.str[i…j]有三个或者三个以上字符,这就属于一般的情况了

  • 3.1.当str[i]==str[j]时,例如,ABDCA,只需要考虑BDC,也即要求dp[i][j],转化为求dp[i+1][j-1],只要搞定str[i+1…j-1]部分,使其变成回文,再加上str[i]str[j],整体就变成回文了,即转移方程:dp[i][j]=dp[i+1][j-1]

  • 3.2.当str[i]!=str[j]时,有两种方法使其变成回文,一种是先让str[i…j-1]变成回文,在i的左边添加str[j],整体就会变成回文,例如ABECD,可以先考虑ABEC这部分,一个方案是ABECEBA,再在ABECEBA左边加上str[j],即D,带上源字符的D,变成了DABECEBAD;另外一种方案,同理,先让str[i+1…j]变成回文,最后在右边加上str[i],最终变成了整体回文,那么,动态转移方程式什么呢?

  • 要想使str[i…j-1]变成回文,其最少的添加次数是dp[i][j-1],再加上往左侧补的字符,最终应该是dp[i][j-1]+1

  • 同理可得,dp[i+1][j]+1

  • dp[i][j]=min(dp[i][j-1],dp[i+1][j])+1

核心代码

  • 生成dp table的代码,注意遍历时,综合考虑了以上分析的三种情况

  • 下文中需要使用到这个buildDP,做个一个抽离

public int[][] buildDP(char[] chas) {
        // `dp[i][j]`表示子串`str[i...j]`范围内的最少添加多少个字符后,可以形成回文子串
        int n = chas.length;
        int[][] dp = new int[n][n];
        for (int j = 1; j < n; j++) {
            dp[j - 1][j] = (chas[j - 1] == chas[j]) ? 0 : 1;
            for (int i = j - 2; i >= 0; i--) {
                if (chas[i] == chas[j]) dp[i][j] = dp[i + 1][j - 1];
                else dp[i][j] = Math.min(dp[i + 1][j], dp[i][j - 1]) + 1;
            }
        }
        return dp;
    }

整体代码

    public int minInsertions(String s) {
        int[][] dp = buildDP(s.toCharArray());
        return dp[0][s.length() - 1];
    }

复杂度分析

  • 时间复杂度:O(N^2)N是字符的长度

  • 空间复杂度:O(N^2)dp表的空间

2.Step2

题目

给定一个字符串str,如果可以在str的任意位置添加字符,请返回在添加字符最少的情况下,让str整体都是回文字符串的结果。

  • 这个问题比Step1的问题多了一步,要求返回一种使其变成整体回文的结果,例如,源字符AB,返回一种添加最少字符,使其变成回文的一种方案,返回ABA或者BAB均可

解题思路

  • 准备一个res数组,长度是字符s本身的长度+dp[0][n-1]

  • 运用双指针的想法,给res填充字符

  • str[i]==str[j]时,res直接追加即可

  • str[i]!=str[j]时,需要找到一个最少添加字符的方案

    • 当`dp[i][j - 1] < dp[i + 1][j]`时,即最终在生成字符的时候,在左侧填充一个`str[j]`

    • 当`dp[i][j - 1] >= dp[i + 1][j]`时,即最终在生成字符的时候,在右侧填充一个`str[i]`

整体代码

public String getPalindromeI(String s) {
        char[] chas = s.toCharArray();
        int n = chas.length;
        int[][] dp = buildDP(chas);
        int appendLen = dp[0][n - 1];
        char[] res = new char[n + appendLen];
        int i = 0, j = n - 1;
        int left = 0, right = res.length - 1;
        while (i <= j) {
            if (chas[i] == chas[j]) {
                res[left++] = chas[i++];
                res[right--] = chas[j--];
            } else {
                if (dp[i][j - 1] < dp[i + 1][j]) {
                    res[left++] = chas[j];
                    res[right--] = chas[j--];
                } else if (dp[i][j - 1] >= dp[i + 1][j]) {
                    res[left++] = chas[i];
                    res[right--] = chas[i++];
                }
            }
        }
        return String.valueOf(res);
    }

复杂度分析

  • 时间复杂度:O(N^2)N是字符的长度,求res的过程是O(N),总体是O(N^2)

  • 空间复杂度:O(N^2)dp表的空间

3.Step3

给定一个字符串str,再给定str的最长回文子序列字符串strlps,请返回在添加字符最少的情况下,让str整体都是回文字符串的一种结果。进阶问题比原问题多了一个参数,请做到时间复杂度比原问题的实现低

举例:

str="A1B21C",strlps="121",返回"AC1B2B1CA"或者"CA1B2B1AC",总之,只要是添加的字符数最少,只返回其中一种结果即可

题目分析

  • 这个问题是给出了一个seed字符,生成最少添加字符形成回文后的其中一种结果

解题思路

运用剥洋葱的方式去解决

  • str的长度为nstrlps的长度为m,形成回文res的结果是2*n-m ,因为m部分已经是回文了,不需要额外添加

  • str=A1BC22DE1F,strlps=1221为例

  • 洋葱的第0层是由strlps[0]strlps[m-1],组成,即1…1,从str的最左侧开始找,一直找到strlps[0]=1的字符为止,记为leftPart,为A,接着从str的最右侧开始找,一直找到strlps[m-1]=1的字符为止,记为rightPart,为F

  • leftPart+rightPart的逆序,复制到res的左侧

  • rightPart+leftPart的逆序,复制到res的右侧

  • 结果变成了AF……FA

  • 在加上strlps,变成了AF1……1FA

  • 洋葱进入第1层,即2…2,接着第一层的位置找str,leftPartBCrightPartDE

  • leftPart+rightPart的逆序,复制到res的左侧

  • rightPart+leftPart的逆序,复制到res的右侧

  • 结果变成了BCED……DECB

  • 在加上strlps,变成了BCED2……2DECB

  • 加上上一层,变成了AF1BCED22DECB1FA

  • 洋葱进入第n层。。。

  • 代码部分注释掉了一部分,将resLeft = 0, resRight = 0;定义为全局变量,好理解些

  • 整体实现逻辑是双指针,游走,不过左右指针有很多对,嗯,只能是,细节是魔鬼啊

整体代码

//res array 的左右指针,定义为全局变量
    int resLeft = 0, resRight = 0;

    public String getPalindromeII(String str, String strlps) {

        char[] chas = str.toCharArray();//str 目标字符串
        char[] lps = strlps.toCharArray();//base 字符串
        int n = chas.length, m = lps.length;//len
        char[] res = new char[2 * n - m];//res字符串 arr
        int chasLeft = 0, chasRight = n - 1;//str 的左右指针
        int lpsLeft = 0, lpsRight = m - 1;//base 的左右指针
        resRight = res.length - 1;
        int tmpLeft = 0, tmpRight = 0;//tmp 左右指针
        while (lpsLeft <= lpsRight) {//循环遍历lps字符
            tmpLeft = chasLeft;
            tmpRight = chasRight;
            while (chas[chasLeft] != lps[lpsLeft]) chasLeft++;//如果lpsLeft在chas未出现,一直chasLeft++
            while (chas[chasRight] != lps[lpsRight]) chasRight--;//如果lpsRight在chas未出现,一直chasRight--
            build(res, chas, chasLeft, chasRight, tmpLeft, tmpRight);//组装
//            int len = chasLeft - tmpLeft + tmpRight - chasRight;
//            resLeft += len;
//            resRight -= len;
            res[resLeft++] = chas[chasLeft++];//添加当前的lpsLeft ==lpsRight对于的字符串
            res[resRight--] = chas[chasRight--];//添加当前的lpsLeft ==lpsRight对于的字符串
            lpsLeft++;//进入下一轮
            lpsRight--;
        }
        return String.valueOf(res);
    }

    /**
     * @param res       结果arr
     *                  //     * @param resLeft
     *                  //     * @param resRight
     * @param chas      str arr
     * @param chasLeft  str arr left pointer
     * @param chasRight str arr right pointer
     * @param tmpLeft   tmp left pointer
     * @param tmpRight  tmp right pointer
     */

    private void build(char[] res,
                       char[] chas, int chasLeft, int chasRight,
                       int tmpLeft, int tmpRight)
 
{

        for (int i = tmpLeft; i < chasLeft; i++) {
            res[resLeft++] = chas[i];
            res[resRight--] = chas[i];
        }
        for (int j = tmpRight; j > chasRight; j--) {
            res[resLeft++] = chas[j];
            res[resRight--] = chas[j];
        }
    }

Reference

本文完

关联阅读


以上是关于动态规划解最长子序列子串等一类问题之让字符串成为回文及其Follow Up[Sika Deer]的主要内容,如果未能解决你的问题,请参考以下文章

最长公共子序列(LCS)动态规划解题笔记

动态规划 最长公共子序列 过程图解

动态规划——最长公共子序列与最长公共子串 (含Python实现代码)

算法图解:动态规划之最长公共子串,最长公共子序列

动态规划-最长公共子序列

动态规划-最长公共子序列