【LeetCode】115. 不同的子序列

本文深入探讨了如何使用动态规划解决子序列问题,详细解释了dp数组的定义及其状态转移方程,提供了两种实现方式,一种是二维dp数组,另一种是一维dp数组的优化方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述
规定 :
(1)S(0, i-1) 表示 S 的前 i 个字符组成的字符串,因此基数为 0,所以最后一个字符即第 i-1 个。

(2)S[i-1] 即表示 S 的第 i-1 个字符。


使用动态规划来处理,则有 dp[i][j] 代表 S(0, i-1)T(0, j-1) 对应的解。

对于 dp[i][j] 有:

  1. S[i-1]T[j-1] 不相等时,则 S 的第 i-1 个字符肯定是要删除的,删除了 S 的第 i-1 个字符,就等价于求 S(0, i-2)T(0, j-1) 的解,因此 dp[i][j] 就等价于 dp[i-1][j]

    也可以反过来想,对于 T(j-1) ,当 S[i-1]T[j-1] 不相等时,对于 S 的前 i-1 个字符(即 S(0, i-2)),再加上第 i-1 个字符,是不会影响到关于 T(j-1) 的结果的。

  2. S[i-1]T[j-1] 相等 时,S 的第 i-1 个字符也是要删除的,但是当其删除时,就会有两种情况:

    抵消T[j-1](即抵消掉 T 的第 j-1 个字符,则剩下 T 的前 (j-1) 个字符,此时最后一个字符变为第 T[j-2] 个了),此时就对应于 dp[i-1][j-1]

    或者不抵消(即 T 的第 j-1 个字符还在,则剩下的依然是 T 的前 j 个字符),此时对应于 dp[i-1][j]

    此时 dp[i][j] 为两种情况的和,即 dp[i][j] = dp[i-1][j-1] + dp[i-1][j]

public int numDistinct(String s, String t) {
    if (s == null || s.length() == 0) {
        return 0;
    }
    if (t == null || t.length() == 0) {
        return 1;
    }
    
    int sLen = s.length();
    int tLen = t.length();
    // 注意数组空间为 [sLen + 1][tLen + 1]
    int[][] dp = new int[sLen + 1][tLen + 1];
	
	// 初始值赋值
    for (int i = 0; i <= sLen; i++) {
        dp[i][0] = 1;
    }
    
    // 最终要求得 dp[sLen][tLen]
    for (int i = 1; i <= sLen; i++) {
        for (int j = 1; j <= tLen; 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[sLen][tLen];
}

注意,对于 dp[i][0],即 T 的字串长度为 0 的时候,有 dp[i][0] 为 1,即空字符串为 S 的子串只有一种可能,那就是删除 S 的所有字符。

而对于 dp[0][j] 不包括 dp[0][0](因为它属于 dp[i][0] 对应的情形 ),即 S 为空字符串,此时不管 T 多长,都无法通过删除 S 中的字符来变成 T,因此 dp[0][j] 为 0。

上述实现的空间复杂度为 O(sLen*tLen)


空间复杂度还可以优化,即使用一纬的辅助数组,此时重点就是对于 dp[i-1][j-1] 状态的保存,因为它在遍历的时候会被 dp[i][j-1] 给覆盖。

此时,就可以用一个临时变量,来保存被覆盖之前的 dp[i-1][j-1] 状态。

public int numDistinct(String s, String t) {
    if (s == null || s.length() == 0) {
        return 0;
    }
    if (t == null || t.length() == 0) {
        return 1;
    }
    int sLen = s.length();
    int tLen = t.length();
    // 使用一纬数组,此时 dp[j] 表示 t 的前 j 个字符
    int[] dp = new int[tLen + 1];
    dp[0] = 1;
    for (int i = 1; i <= sLen ; i++) {
        int pre = dp[0];
        dp[0] = 1;
        for (int j = 1; j <= tLen; j++) {
            // 用临时状态保存更新前的 dp[j],此时为 dp[i-1][j-1] 对应的状态
            int tmp = dp[j];
            if (s.charAt(i-1) == t.charAt(j-1)) {
                dp[j] = pre + tmp;
            } else {
                dp[j] = tmp;
            }
            // 将更新前的 dp[i] 赋值给 pre,用于遍历下一个元素
            pre = tmp;
        }
    }
    return dp[tLen];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值