1. 动态规划经典问题实战解析
今天要和大家分享的是动态规划领域中四个经典问题的解法与思考过程。这些问题看似独立,实则存在内在联系,通过对比分析可以更深入理解动态规划的核心思想。我们将从问题定义、状态转移方程推导到代码实现,完整走通每个问题的解决路径。
1.1 题目背景与关联性分析
这组题目都涉及到序列比对和最优子结构特性:
- 1143.最长公共子序列(LCS):经典二维DP问题
- 1035.不相交的线:LCS的变形应用
- 53.最大子序和:一维DP代表问题
- 392.判断子序列:双指针与DP两种解法
关键洞察:LCS问题是这组题目的核心,其他问题都可以看作它的变种或简化版本。理解LCS的状态转移逻辑是攻克这组题目的关键。
2. 最长公共子序列(LCS)深度剖析
2.1 问题定义与状态设计
给定两个字符串text1和text2,返回它们的最长公共子序列的长度。子序列不要求连续,但相对顺序必须保持一致。
状态定义:
- dp[i][j]:text1[0..i-1]和text2[0..j-1]的LCS长度
- 初始化:dp[0][j] = dp[i][0] = 0(空串与任何串的LCS为0)
2.2 状态转移方程推导
状态转移分两种情况:
- 当前字符匹配:dp[i][j] = dp[i-1][j-1] + 1
- 当前字符不匹配:dp[i][j] = max(dp[i-1][j], dp[i][j-1])
python复制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.3 空间优化技巧
二维DP可以优化为O(n)空间:
python复制def lcs_space_optimized(text1, text2):
if len(text1) < len(text2):
text1, text2 = text2, text1
m, n = len(text1), len(text2)
prev = [0]*(n+1)
for i in range(1, m+1):
curr = [0]*(n+1)
for j in range(1, n+1):
if text1[i-1] == text2[j-1]:
curr[j] = prev[j-1] + 1
else:
curr[j] = max(prev[j], curr[j-1])
prev = curr
return prev[n]
3. 不相交的线问题转化
3.1 问题重述与分析
1035题要求在两组数字之间绘制不相交的连线,连接相等的数字。这实际上就是求两个数组的最长公共子序列。
关键观察:
- 连线不相交 ↔ 数字相对顺序保持不变
- 这就是LCS问题的实际应用场景
3.2 直接应用LCS解法
python复制def maxUncrossedLines(nums1, nums2):
# 完全等同于LCS解法
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. 最大子序和问题解析
4.1 问题特点与简化
53题要求找出具有最大和的连续子数组。相比LCS,这是一维DP问题,状态转移更简单。
状态定义:
- dp[i]:以nums[i]结尾的最大子序和
- 状态转移:dp[i] = max(nums[i], dp[i-1]+nums[i])
4.2 实现与空间优化
python复制def maxSubArray(nums):
if not nums:
return 0
dp = [0]*len(nums)
dp[0] = nums[0]
max_sum = dp[0]
for i in range(1, len(nums)):
dp[i] = max(nums[i], dp[i-1]+nums[i])
max_sum = max(max_sum, dp[i])
return max_sum
优化为O(1)空间:
python复制def maxSubArray_optimized(nums):
if not nums:
return 0
current_max = global_max = nums[0]
for num in nums[1:]:
current_max = max(num, current_max + num)
global_max = max(global_max, current_max)
return global_max
5. 判断子序列的双解法
5.1 双指针解法
392题判断s是否为t的子序列,最简单的方法是双指针:
python复制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.2 DP解法及其意义
虽然双指针更高效,但DP解法有助于理解后续更复杂的问题:
python复制def isSubsequence_dp(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] # 注意这里与LCS的区别
return dp[m][n] == len(s)
关键区别:当字符不匹配时,只考虑t的指针前移(dp[i][j-1]),因为s是待匹配的目标串。
6. 问题对比与经验总结
6.1 四题对比分析
| 题目 | 问题类型 | 状态维度 | 关键转移逻辑 | 时间复杂度 |
|---|---|---|---|---|
| 1143 | LCS | 二维 | max(dp[i-1][j], dp[i][j-1]) | O(mn) |
| 1035 | LCS变种 | 二维 | 同1143 | O(mn) |
| 53 | 最大和 | 一维 | max(nums[i], dp[i-1]+nums[i]) | O(n) |
| 392 | 子序列判断 | 二维 | dp[i][j-1](单方向转移) | O(mn) |
6.2 常见错误与调试技巧
-
边界条件处理:
- 空输入处理
- 初始化时行列大小是否正确
- 字符串/数组的索引偏移(通常用i-1访问元素)
-
状态转移错误:
- LCS问题容易混淆三种情况的优先级
- 最大子序和容易遗漏当前元素单独成段的case
-
空间优化陷阱:
- 二维降一维时注意依赖关系
- 滚动数组需要正确维护前状态
6.3 扩展思考
这组题目展示了动态规划解决序列问题的通用模式:
- 定义子问题状态(通常与问题规模相关)
- 建立状态转移方程(关键难点)
- 确定初始条件和边界情况
- 考虑空间优化可能性
对于更复杂的问题如编辑距离、通配符匹配等,都可以沿用类似的思考框架。建议在理解这组基础问题后,尝试解决72.编辑距离和44.通配符匹配等进阶题目。