【LeetCode】115. 不同的子序列

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

在这里插入图片描述
规定 :
(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];
}
### 解题思路 LeetCode 第 674 题的目标是找到给定数组中的最长连续递增子序列的长度。此问题可以通过一次线性扫描来解决,时间复杂度为 O(n),空间复杂度可以优化到 O(1)[^1]。 #### 关键点分析 - **连续性**:题目强调的是“连续”,因此只需要比较相邻两个元素即可判断是否构成递增关系。 - **动态规划 vs 贪心算法**:虽然可以用动态规划的思想解决问题,但由于只需记录当前的最大值而无需回溯历史状态,贪心策略更为高效[^3]。 --- ### Python 实现 以下是基于贪心算法的 Python 实现: ```python class Solution: def findLengthOfLCIS(self, nums): if not nums: # 如果输入为空,则返回0 return 0 max_len = 1 # 至少有一个元素时,最小长度为1 current_len = 1 # 当前连续递增序列的长度初始化为1 for i in range(1, len(nums)): # 从第二个元素开始遍历 if nums[i] > nums[i - 1]: # 判断当前元素是否大于前一个元素 current_len += 1 # 是则增加当前长度 max_len = max(max_len, current_len) # 更新全局最大长度 else: current_len = 1 # 否则重置当前长度 return max_len # 返回最终结果 ``` 上述代码通过维护 `current_len` 和 `max_len` 来跟踪当前连续递增序列的长度以及整体的最大长度。 --- ### Java 实现 下面是等效的 Java 版本实现: ```java public class Solution { public int findLengthOfLCIS(int[] nums) { if (nums.length == 0) { // 处理边界情况 return 0; } int maxLength = 1; // 初始化最大长度 int currentLength = 1; // 初始化当前长度 for (int i = 1; i < nums.length; i++) { if (nums[i] > nums[i - 1]) { // 若满足递增条件 currentLength++; // 增加当前长度 maxLength = Math.max(maxLength, currentLength); // 更新最大长度 } else { currentLength = 1; // 不满足递增条件时重新计数 } } return maxLength; // 返回结果 } } ``` 该版本逻辑与 Python 类似,但在语法上更贴近 Java 的特性[^4]。 --- ### C++ 实现 对于 C++ 用户,下面是一个高效的解决方案: ```cpp #include <vector> #include <algorithm> // 使用 std::max 函数 using namespace std; class Solution { public: int findLengthOfLCIS(vector<int>& nums) { if (nums.empty()) { // 边界处理 return 0; } int result = 1; // 结果变量 int count = 1; // 当前连续递增序列长度 for (size_t i = 1; i < nums.size(); ++i) { if (nums[i] > nums[i - 1]) { // 检查递增条件 count++; result = max(result, count); } else { count = 1; // 重置计数器 } } return result; // 返回最终结果 } }; ``` 这段代码同样遵循了单次遍历的原则,并利用标准库函数简化了一些操作。 --- ### 小结 三种语言的核心思想一致,均采用了一种简单的线性扫描方式完成任务。这种方法不仅易于理解,而且性能优越,在实际应用中非常实用[^2]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值