不同的子序列——动态规划集大成版

给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。

测试用例保证结果在 32 位有符号整数范围内。

示例 1:

输入:s = "rabbbit", t = "rabbit"
输出3

解释:
如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit示例 2:
输入:s = "babgbag", t = "bag"
输出5
解释:如下所示, 有 5 种可以从 s 中得到 "bag" 的方案。 
babgbagbabgbagbabgbagbabgbagbabgbag

以下给出解法套路和五种实现。

【五种实现】

  1. 常规二维dp
  2. 交替滚动一维dp
  3. 原地滚动一维dp(借助变量正序版)
  4. 原地滚动一维dp(逆序版)
  5. 原地滚动一维dp(逆序版,最后一行只更新最终答案)

【解法套路四步走】

1)根据问题给出二维dp数组定义。

要求s子序列中t的个数。立刻定义dp[i][j]: s的前i个字符中的t的前j个字符的子序列个数

2)分别令两个维度为0,推测边界。

只有当j=0时,才有dp[0][j] = 1,否则dp[0][j] = 0。

dp[i][0] = 1。

3)寻找转移方程。

若s[i] ≠ t[j] 转移方程:

dp[i][j] = dp[i-1][j]

若s[i] = t[j]:


dp[i][j] = dp[i-1][j-1] + dp[i-1][j]

4)空间压缩。

也是固定套路,先从二维数组转两个一维数组(交替滚动),再从两个一维数组转一个一维数组(原地滚动)。可借助变量来实现正序一维滚动dp,也可以不借助变量直接一维逆序滚动dp。

下面给出代码。

==== 二维dp ====

class Solution {
    public int numDistinct(String s, String t) {
        if(s.length() < t.length()) return 0; // s长度小于t时,s中不会出现t
        int ns = s.length(), nt = t.length();
        char[] chss = s.toCharArray(), chst = t.toCharArray();
        int[][] dp = new int[ns + 1][nt + 1];
        for(int i = 0; i <= ns; i++) dp[i][0] = 1; 
        for(int i = 1; i <= ns; i++){
            for(int j = 1; j <= nt; j++){
                dp[i][j] = dp[i - 1][j] + (chss[i - 1] == chst[j - 1] ? dp[i - 1][j - 1] : 0);
            }
        }
        return dp[ns][nt];
    }
}

==== 交替滚动一维dp ====

class Solution {
    public int numDistinct(String s, String t) {
        if(t.length() > s.length()) return 0; // s长度小于t时间直接返回0
        int ns = s.length(), nt = t.length();
        int[] pre = new int[nt + 1], cur = new int[nt + 1];
        char[] chss = s.toCharArray(), chst = t.toCharArray();
        pre[0] = 1;
        cur[0] = 1; // 当t为空串时,此行是必须的
        for(int i = 1; i <= ns; i++){
            for(int j = 1; j <= nt; j++){
                if(chss[i - 1] == chst[j - 1]) cur[j] = pre[j - 1] + pre[j];
                else cur[j] = pre[j];
            }
            pre = Arrays.copyOf(cur, nt + 1); // for(int k = 0; k <= nt; k++) pre[k] = cur[k];
        }
        return cur[nt];
    }
}

==== 原地滚动一维dp 【借助变量正序】====

class Solution {
    public int numDistinct(String s, String t) {
        if(s.length() < t.length()) return 0; // s长度小于t时,s中不会出现t
        int ns = s.length(), nt = t.length();
        char[] chss = s.toCharArray(), chst = t.toCharArray();
        int[] dp = new int[nt + 1];
        dp[0] = 1;
        int pre = dp[0];
        for(int i = 1; i <= ns; i++){
            for(int j = 1; j <= nt; j++){
                int nextPre = dp[j]; // 相当于二维矩阵中待更新的dp[i][j]的dp[i - 1][j - 1]
                if(chss[i - 1] == chst[j - 1] ) dp[j] += pre;
                pre = nextPre;
            }
            pre = 1; // 每完成一行要重置该值为1
        }
        return dp[nt];
    }
}

==== 原地滚动一维dp 【逆序(无需变量辅助)】====

class Solution {
    public int numDistinct(String s, String t) {
        if(s.length() < t.length()) return 0; // s长度小于t时,s中不会出现t
        int ns = s.length(), nt = t.length();
        char[] chss = s.toCharArray(), chst = t.toCharArray();
        int[] dp = new int[nt + 1];
        dp[0] = 1;
        for(int i = 1; i <= ns; i++){ 
            for(int j = nt; j > 0; j--){
                if(chss[i - 1] == chst[j - 1] ) dp[j] += dp[j - 1];
            }
        }
        return dp[nt];
    }
}

==== 原地滚动一维dp 【最后一行只更新dp[nt]】====

class Solution {
    public int numDistinct(String s, String t) {
        if(s.length() < t.length()) return 0; // s长度小于t时,s中不会出现t
        int ns = s.length(), nt = t.length();
        char[] chss = s.toCharArray(), chst = t.toCharArray();
        int[] dp = new int[nt + 1];
        dp[0] = 1;
        for(int i = 1; i < ns; i++){ // 省一行
            for(int j = nt; j > 0; j--){
                if(chss[i - 1] == chst[j - 1] ) dp[j] += dp[j - 1];
            }
        }
        return chss[ns - 1] == chst[nt - 1] ? dp[nt] + dp[nt - 1] : dp[nt]; // 最后一行只需处理dp[nt]
    }
}

需注意:

本题将 s 放置于 行的位置还是 列的位置 非常重要,这决定了转移方程怎么写,也决定了用一维原地滚动的时候,是按行遍历 还是按列遍历。

上面的一维原地滚动 是按行遍历的,如果将s换个位置放,原地滚动 就需要按列遍历了。

【按行遍历指的是:一行一行遍历;按列遍历指的是:一列一列遍历】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值