1. 动态规划专题精讲
这三个题目都是经典的字符串动态规划问题,它们代表了动态规划在字符串处理中的典型应用场景。作为算法训练营的进阶内容,这些题目考察的是对动态规划思想的深入理解和灵活运用能力。
我在刷题和面试辅导过程中发现,很多同学对这类字符串DP问题存在畏难心理。实际上只要掌握正确的分析方法和解题模板,这类问题都有清晰的解决路径。今天我们就来彻底攻克这三个经典问题,建立完整的解题思维框架。
2. 115.不同的子序列
2.1 问题重述与分析
给定字符串s和t,计算s的子序列中等于t的个数。子序列是指通过删除某些字符而不改变剩余字符相对位置形成的新字符串。
例如:
s = "rabbbit", t = "rabbit"
输出:3
解释:
有3种可以从s中得到"rabbit"的方案
这个问题的难点在于如何避免重复计算,同时正确处理字符匹配和不匹配的情况。我们需要设计一个状态转移方程来系统性地计算所有可能性。
2.2 DP状态定义
定义dp[i][j]表示s的前i个字符中,t的前j个字符出现的次数。我们的目标是求dp[len(s)][len(t)]。
初始化条件:
- dp[i][0] = 1 (空串是任何字符串的子序列)
- dp[0][j] = 0 (j>0时,空串无法组成非空串)
2.3 状态转移方程
当s[i-1] == t[j-1]时:
dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
(匹配当前字符的情况 + 不匹配当前字符的情况)
当s[i-1] != t[j-1]时:
dp[i][j] = dp[i-1][j]
(只能不匹配当前字符)
2.4 代码实现
python复制def numDistinct(s: str, t: str) -> int:
m, n = len(s), len(t)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = 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] + dp[i-1][j]
else:
dp[i][j] = dp[i-1][j]
return dp[m][n]
2.5 优化与注意事项
- 空间优化:可以使用一维数组来优化空间复杂度到O(n)
- 大数处理:当结果很大时,需要注意整数溢出问题
- 边界条件:特别注意空字符串的处理
- 测试用例:建议测试s和t完全相同、完全不同、t比s长等情况
3. 583. 两个字符串的删除操作
3.1 问题理解
给定两个单词word1和word2,找到使word1和word2相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
例如:
输入: "sea", "eat"
输出: 2
解释:
第一步将"sea"变为"ea"
第二步将"eat"变为"ea"
这个问题可以转化为求两个字符串的最长公共子序列(LCS),然后用两个字符串的长度之和减去两倍的LCS长度。
3.2 解法一:直接DP
定义dp[i][j]为使word1前i个字符和word2前j个字符相同所需的最小删除步数。
状态转移方程:
- 当word1[i-1] == word2[j-1]时:
dp[i][j] = dp[i-1][j-1] - 否则:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1
3.3 解法二:LCS转换
- 先求出两个字符串的最长公共子序列长度lcs_len
- 结果为len(word1) + len(word2) - 2 * lcs_len
3.4 代码实现
python复制def minDistance(word1: str, word2: str) -> int:
# 解法一:直接DP
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1
return dp[m][n]
3.5 关键点分析
- 两种解法的时间复杂度都是O(mn),空间复杂度可以通过滚动数组优化到O(min(m,n))
- 直接DP解法更直观,LCS解法需要先掌握LCS问题
- 初始化时,dp[i][0] = i表示将word1前i个字符变为空串需要i次删除
- 实际面试中,建议先解释思路再写代码,特别是状态转移方程
4. 72. 编辑距离
4.1 问题描述
给定两个单词word1和word2,计算出将word1转换成word2所使用的最少操作数。允许的操作包括:
- 插入一个字符
- 删除一个字符
- 替换一个字符
例如:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (替换'h'为'r')
rorse -> rose (删除'r')
rose -> ros (删除'e')
4.2 DP状态定义
定义dp[i][j]为word1前i个字符转换为word2前j个字符所需的最小操作数。
4.3 状态转移方程
- 当word1[i-1] == word2[j-1]时:
dp[i][j] = dp[i-1][j-1]
(不需要操作) - 否则:
dp[i][j] = min(
dp[i-1][j] + 1, # 删除word1[i-1]
dp[i][j-1] + 1, # 插入word2[j-1]
dp[i-1][j-1] + 1 # 替换word1[i-1]为word2[j-1]
)
4.4 代码实现
python复制def minDistance(word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(
dp[i-1][j] + 1,
dp[i][j-1] + 1,
dp[i-1][j-1] + 1
)
return dp[m][n]
4.5 优化与扩展
- 空间优化:可以使用两个一维数组交替计算,空间复杂度降到O(n)
- 操作记录:如果需要输出具体操作序列,可以额外维护一个操作矩阵
- 加权编辑距离:不同操作可以有不同的代价,修改状态转移方程即可
- 应用场景:拼写检查、DNA序列比对、模糊搜索等
5. 三题对比与总结
5.1 解题模式对比
| 题目 | 状态定义 | 转移方程特点 | 初始化 | 典型应用 |
|---|---|---|---|---|
| 不同的子序列 | s前i个中t前j个出现次数 | 匹配时相加,不匹配时继承 | dp[i][0]=1 | 序列匹配统计 |
| 两个字符串的删除 | 使前i和前j相同的最小删除 | 取最小删除操作 | dp[i][0]=i | 文档差异分析 |
| 编辑距离 | 前i个转前j个的最小操作 | 三种操作取最小 | dp[i][0]=i | 拼写纠正 |
5.2 常见错误与调试技巧
- 索引混淆:字符串下标和DP数组下标容易搞混,建议统一用前i/j个字符的思路
- 初始化错误:特别注意空字符串的处理方式
- 方向错误:双重循环的方向要一致,通常都是从小到大
- 打印DP表:调试时可以打印整个DP表检查中间结果
- 小规模测试:先用小例子手动计算验证
5.3 进阶练习建议
- 尝试空间优化的实现版本
- 修改问题条件,如允许不同的操作代价
- 输出具体的操作序列而不仅仅是次数
- 解决相关变种问题,如最长公共子串、最短公共超序列等
- 在实际项目中寻找应用场景,如文本差异比较工具
这三道题代表了字符串动态规划的经典模式,掌握它们对提升DP解题能力至关重要。建议先理解透基本解法,再尝试优化和扩展,最后应用到实际问题中。动态规划的关键在于定义好状态和转移方程,这需要大量的练习和经验积累。