动态规划10:判断子序列、不同的子序列、两个字符串的删除操作、编辑距离、编辑距离总结、回文子串、最长回文子序列、动态规划总结

47. 判断子序列

例题392:
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

进阶:

如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
在这里插入图片描述
暴力法:遍历字符串t,在t中依次找到是否存在s中的每一个元素的子序列。注意:使用indexOf(String str,int fromIndex),每次从上一个找到位置之后的一个开始查找,避免重复和相对位置改变。

class Solution {
    public boolean isSubsequence(String s, String t) {
        if(s.length()==0) return true;
        if(s.length()>t.length()) return false;
        int i=0,j=0;
        while(i<t.length() && j<s.length()){
            if(t.indexOf(s.charAt(j),i)!=-1){
                i=t.indexOf(s.charAt(j),i)+1;
                j++;
            }
            else{
                break;
            }
        }
        if(j==s.length())
        return true;
        else
        return false;
    }
}

动态规划

只涉及减法

  1. 确定dp数组及下标含义
    dp[i][j]表示从0到i-1的字符串s和从0到j-1的字符串t的子序列长度。
  2. 确定递推公式
    如果s[i-1]与t[j-1]相同,那么dp[i][j]=dp[i-1][j-1]+1。
    如果不相同,就意味着t要删字符,dp[i][j]=dp[i][j-1]。
    这里与最长公共子序列不同的是,只有t可以回退,s不能回退。
  3. 初始化
    i=0,j=0时为0.

代码如下:

class Solution {
    public boolean isSubsequence(String s, String t) {
        int n1=s.length();
        int n2=t.length();
        int[][] dp=new int[n1+1][n2+1];
        for(int i=0;i<=n1;i++){
            dp[i][0]=0;
        }
        for(int j=0;j<=n2;j++){
            dp[0][j]=0;
        }
        for(int i=1;i<=n1;i++){
            for(int j=1;j<=n2;j++){
                if(s.charAt(i-1)==t.charAt(j-1))
                dp[i][j]=dp[i-1][j-1]+1;
                else
                dp[i][j]=dp[i][j-1];
            }
        }
        return (dp[n1][n2]==n1)?true:false;
    }
}

48. 不同的子序列

例题115:
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109 + 7 取模。

在这里插入图片描述
就是找到t在s中可以通过几种不同的子序列得到。
题解如下:
需要搞清楚当s[i-1]=t[i-1]的时候,可以有两种情况。
dp[i][j]=dp[i-1][j-1]
dp[i][j]=dp[i-1][j],第二种情况是因为也可能删除s的一个字符,前面的也相等。

class Solution {
    public int numDistinct(String s, String t) {
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        for (int i = 0; i < s.length() + 1; i++) {
            dp[i][0] = 1;
        }
        
        for (int i = 1; i < s.length() + 1; i++) {
            for (int j = 1; j < t.length() + 1; j++) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                }else{
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        
        return dp[s.length()][t.length()];
    }
}

49. 两个字符串的删除操作

例题583:
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。

每步 可以删除任意一个字符串中的一个字符。

在这里插入图片描述
先找到最长公共子序列?然后两个字符串减去子数组的长度。

class Solution {
    public int minDistance(String word1, String word2) {
        int n1=word1.length();
        int n2=word2.length();
        int res=0;
        int[][] dp=new int[n1+1][n2+1];
        for(int i=1;i<=n1;i++){
            for(int j=1;j<=n2;j++){
                if(word1.charAt(i-1)==word2.charAt(j-1)){
                    dp[i][j]=dp[i-1][j-1]+1;
                }
                else{
                    dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
                }
                 res=Math.max(res,dp[i][j]);
            }
        }
        return n1+n2-2*res;
    }
}

50. 编辑距离

例题72:
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符
    在这里插入图片描述

动态规划

  1. 确定dp数组及下标含义
    dp[i][j]表示从0到i-1的数组s和从0到j-1的数组t之间的最短编辑距离。
  2. 确定递推公式
    要确定有几种操作情况
if(s[i-1]==t[i-1])
不操作;
dp[i][j]=dp[i-1][j-1];
else{
增:word1删除一个就相当于word2添加一个:dp[i][j]=dp[i-1][j]+1;
删:word2删除一个就相当于word1增加一个:dp[i][j]=dp[i][j-1]+1;
改:在word1中修改一个等于word2:dp[i][j]=dp[i-1][j-1]+1;
}

所以dp[i][j]取以下四种情况的最小值。
3. 初始化
dp[i][0]表示到i-1的字符串word1,和空字符串的编辑距离为word1删去i个字符,所以dp[i][0]=i;
dp[0][j]表示到j-1的字符串word2,和空字符串的编辑距离为word2删去j个字符,所以dp[0][j]=j。

代码如下:

class Solution {
    public int minDistance(String word1, String word2) {
int m = word1.length();
    int n = word2.length();
    int[][] dp = new int[m + 1][n + 1];
    // 初始化
    for (int i = 1; i <= m; i++) {
        dp[i][0] =  i;
    }
    for (int j = 1; j <= n; j++) {
        dp[0][j] = j;
    }
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            // 因为dp数组有效位从1开始
            // 所以当前遍历到的字符串的位置为i-1 | j-1
            if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
            }
        }
    }
    return dp[m][n];
    }
}

51. 编辑距离总结

  1. 不同的子序列:在字符串s中找到有几种子序列可以包含字符串t?
    如果s[i-1]==t[j-1],那么有两种情况dp[i][j]=dp[i-1][j-1]+dp[i-1][j]。也就是s减去当前一个还相等。
    关键在于当s[i-1]!=t[j-1]的时候,需要对s进行删除操作。也就是dp[i][j]=dp[i-1][j]。
  2. 两个字符串的删除操作:可以找到两个字符串的最长公共子序列长度,然后分别减去求删除步骤。
  3. 编辑距离:需要搞清楚相等的不操作和不等时的增删改操作。
    增和删其实是一个操作。dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1;

52. 回文子串

例题647:
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

在这里插入图片描述

动态规划

  1. 确定dp数组及下标含义
    布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。

  2. 确定递推公式
    在确定递推公式时,就要分析如下几种情况。

整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。

当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。

当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况

情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
情况二:下标i 与 j相差为1,例如aa,也是回文子串
情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
3. dp数组如何初始化
dp[i][j]可以初始化为true么? 当然不行,怎能刚开始就全都匹配上了。

所以dp[i][j]初始化为false。

  1. 确定遍历顺序
    遍历顺序可有有点讲究了。

首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。

dp[i + 1][j - 1] 在 dp[i][j]的左下角,如图:
在这里插入图片描述
如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。

所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。

有的代码实现是优先遍历列,然后遍历行,其实也是一个道理,都是为了保证dp[i + 1][j - 1]都是经过计算的。

代码如下:

class Solution {
    public int countSubstrings(String s) {
        char[] chars = s.toCharArray();
        int len = chars.length;
        boolean[][] dp = new boolean[len][len];
        int result = 0;
        for (int i = len - 1; i >= 0; i--) {
            for (int j = i; j < len; j++) {
                if (chars[i] == chars[j]) {
                    if (j - i <= 1) { // 情况一 和 情况二
                        result++;
                        dp[i][j] = true;
                    } else if (dp[i + 1][j - 1]) { //情况三
                        result++;
                        dp[i][j] = true;
                    }
                }
            }
        }
        return result;
    }
}

53. 最长回文子序列

例题516:
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
在这里插入图片描述
与回文子串不同的是,子串是连续的,子序列不是连续的。

  1. 确定dp数据及下标含义
    dp[i][j]表示[i,j]范围内最长的回文子序列长度。
  2. 确定递推公式
    如果s[i]=s[j],那么dp[i][j]=dp[i+1][j-1]+2;
    如果s[i]!=s[j],表示回文子序列不能同时加入s[i],s[j],分别加入两个元素看谁组成的回文子序列更长。
    所以,dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1]);
  3. 初始化
    当i和j相等时,dp[i][j]=1,其余时候初始化为0
  4. 遍历顺序
    从递推公式可以看出必须从下至上,从左到右遍历

代码如下:

class Solution {
    public int longestPalindromeSubseq(String s) {
        int n=s.length();
        int[][] dp=new int[n+1][n+1];
        for(int i=0;i<=n;i++){
            for(int j=0;j<=n;j++){
                if(i==j) dp[i][j]=1;
            }
        }
        for(int i=n-1;i>=0;i--){
            for(int j=i+1;j<n;j++){
                if(s.charAt(i)==s.charAt(j)){
                    dp[i][j]=dp[i+1][j-1]+2;
                }
                else{
                    dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1]);
                }
            }
        }
        return dp[0][n-1];
    }
}

需要注意:遍历的顺序是根据递推公式,从下至上
最后的结果是在dp[0][n-1],因为从下至上从左到右的遍历,这是最后的结果。

54. 动态规划总结

五步骤
01背包:背包容量有限,每种物品0只能一个,选或不选
二维dp:从物品到背包、从背包到物品遍历方向都可以。背包与物品都从头开始增长
一维dp:从物品到背包遍历。背包倒序遍历,保证物品i只被放入一次
完全背包:每种物品有无限个,可以选多个或不选
先物品再背包是组合数,先背包再物品是排列数。都从正序开始遍历
多重背包:与01背包类似,只不过每件物品有多个。如果把每个相同种类的物品摊开就是01背包。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值