动态规划经典问题解析:最长公共子序列与最大子数组和
1. 动态规划经典问题实战解析
今天我想和大家分享LeetCode上四个经典的动态规划问题:1143.最长公共子序列、1035.不相交的线、53.最大子数组和以及392.判断子序列。这几个问题看似不同,实则都体现了动态规划的核心思想,非常适合用来训练DP思维。
2. LeetCode 1143. 最长公共子序列
2.1 问题描述与理解
给定两个字符串text1和text2,返回这两个字符串的最长公共子序列的长度。子序列是指在不改变字符相对顺序的情况下删除某些字符(也可以不删除)后组成的新字符串。
例如: text1 = "abcde", text2 = "ace" 输出:3 解释:最长公共子序列是 "ace",长度为3。
2.2 DP状态定义与转移方程
我们定义dp[i][j]表示text1前i个字符和text2前j个字符的最长公共子序列长度。
状态转移方程:
- 当text1[i-1] == text2[j-1]时:dp[i][j] = dp[i-1][j-1] + 1
- 否则:dp[i][j] = max(dp[i-1][j], dp[i][j-1])
2.3 代码实现
def longestCommonSubsequence(text1: str, text2: str) -> int: m, n = len(text1), len(text2) dp = [[0]*(n+1) for _ in range(m+1)] for i in range(1, m+1): for j in range(1, n+1): if text1[i-1] == text2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) return dp[m][n]2.4 空间优化技巧
可以将二维DP优化为一维数组,空间复杂度从O(mn)降为O(n):
def longestCommonSubsequence(text1: str, text2: str) -> int: m, n = len(text1), len(text2) dp = [0]*(n+1) for i in range(1, m+1): prev = 0 for j in range(1, n+1): temp = dp[j] if text1[i-1] == text2[j-1]: dp[j] = prev + 1 else: dp[j] = max(dp[j], dp[j-1]) prev = temp return dp[n]3. LeetCode 1035. 不相交的线
3.1 问题转化
这道题看似是几何问题,实际上可以转化为最长公共子序列问题。在数组A和B中绘制不相交的连线,等价于找到A和B的最长公共子序列。
3.2 解法分析
因为连线不能相交,所以必须保持相对顺序,这与LCS的定义完全一致。因此可以直接复用1143题的解法。
3.3 代码实现
def maxUncrossedLines(nums1: List[int], nums2: List[int]) -> int: m, n = len(nums1), len(nums2) dp = [[0]*(n+1) for _ in range(m+1)] for i in range(1, m+1): for j in range(1, n+1): if nums1[i-1] == nums2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) return dp[m][n]4. LeetCode 53. 最大子数组和
4.1 问题描述
给定一个整数数组nums,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例: 输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为6。
4.2 动态规划解法
定义dp[i]为以nums[i]结尾的最大子数组和:
- dp[i] = max(nums[i], dp[i-1]+nums[i])
- 最终结果是max(dp)
4.3 代码实现
def maxSubArray(nums: List[int]) -> int: n = len(nums) dp = [0]*n dp[0] = nums[0] for i in range(1, n): dp[i] = max(nums[i], dp[i-1]+nums[i]) return max(dp)4.4 空间优化
可以只用一个变量记录前一个状态:
def maxSubArray(nums: List[int]) -> int: max_sum = curr_sum = nums[0] for num in nums[1:]: curr_sum = max(num, curr_sum + num) max_sum = max(max_sum, curr_sum) return max_sum5. LeetCode 392. 判断子序列
5.1 问题描述
给定字符串s和t,判断s是否为t的子序列。子序列的定义与1143题相同。
示例: 输入:s = "abc", t = "ahbgdc" 输出:true
5.2 双指针解法
最简单的解法是使用双指针:
def isSubsequence(s: str, t: str) -> bool: i = j = 0 while i < len(s) and j < len(t): if s[i] == t[j]: i += 1 j += 1 return i == len(s)5.3 动态规划解法
也可以使用类似LCS的DP方法:
def isSubsequence(s: str, t: str) -> bool: m, n = len(s), len(t) dp = [[0]*(n+1) for _ in range(m+1)] for i in range(1, m+1): for j in range(1, n+1): if s[i-1] == t[j-1]: dp[i][j] = dp[i-1][j-1] + 1 else: dp[i][j] = dp[i][j-1] return dp[m][n] == len(s)6. 动态规划问题总结
6.1 解题思路
- 定义状态:明确dp数组的含义
- 确定状态转移方程:找出dp[i]与之前状态的关系
- 初始化:确定边界条件
- 确定遍历顺序
- 举例推导dp数组
6.2 常见错误
- 状态定义不准确
- 边界条件处理不当
- 遍历顺序错误
- 空间优化时状态覆盖问题
6.3 实战技巧
- 先画状态转移表帮助理解
- 打印中间结果调试
- 从暴力解法开始思考,再优化
- 注意空间优化时的状态依赖关系
7. 进阶练习建议
- 尝试解决LeetCode 583. 两个字符串的删除操作
- 挑战LeetCode 72. 编辑距离
- 解决LeetCode 115. 不同的子序列
- 尝试用多种方法解决每个问题
在实际刷题过程中,我发现动态规划问题最重要的是理解状态定义和转移方程。建议每个问题都手动推导几个例子,这样能更好地掌握DP思想。对于空间优化,一定要在完全理解二维DP后再尝试,避免过早优化导致思维混乱。