代码随想录训练营 Day45打卡 动态规划 part12
一、力扣115. 不同的子序列
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109 + 7 取模。
示例 :
输入:s = “rabbbit”, t = “rabbit”
输出:3
解释:
如下所示, 有 3 种可以从 s 中得到 “rabbit” 的方案。
rabbbit
rabbbit
rabbbit
-
确定 dp 数组的定义
dp[i][j] 表示 s 的前 i 个字符中子序列等于 t 的前 j 个字符的个数。 -
状态转移方程
当 s[i-1] == t[j-1] 时,有两种情况:
(1)使用 s[i-1] 来匹配:那么匹配个数等于 dp[i-1][j-1],即不考虑当前 s 和 t 的最后一个字符,子序列的数量。
(2)不使用 s[i-1] 来匹配:匹配个数等于 dp[i-1][j],即仅用 s 的前 i-1 个字符的子序列中 t 的前 j 个字符出现的次数。
状态转移方程: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],即不使用 s[i-1] 来匹配的情况。
状态转移方程:dp[i][j] = dp[i-1][j] -
dp 数组的初始化
dp[i][0] = 1:无论 s 有多少字符,当 t 为空字符串时,s 的子序列中等于 t 的个数为 1(即空子序列)。
dp[0][j] = 0:当 s 为空字符串但 t 不为空时,子序列中等于 t 的个数为 0,因为空字符串无法匹配非空字符串。 -
确定遍历顺序
由于 dp[i][j] 依赖于 dp[i-1][j-1] 和 dp[i-1][j],所以遍历时需要从左到右,从上到下进行。 -
最终结果
dp[len(s)][len(t)] 就是字符串 s 的子序列中 t 出现的次数。
代码实现
class Solution:
def numDistinct(self, s: str, t: str) -> int:
# 创建一个二维数组 dp,初始化为 0
dp = [[0] * (len(t) + 1) for _ in range(len(s) + 1)]
# 初始化 dp[i][0] = 1,因为 t 为空字符串时,子序列等于 t 的个数为 1
for i in range(len(s) + 1):
dp[i][0] = 1
# 遍历字符串 s 和 t,填充 dp 数组
for i in range(1, len(s) + 1):
for j in range(1, len(t) + 1):
if s[i-1] == t[j-1]:
# 如果 s[i-1] 和 t[j-1] 相等,dp[i][j] 由两部分组成:
# 1. dp[i-1][j-1] 表示使用 s[i-1] 和 t[j-1] 匹配
# 2. dp[i-1][j] 表示不使用 s[i-1],但保留 s[0:i-2] 和 t[j-1] 的匹配
dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
else:
# 如果 s[i-1] 和 t[j-1] 不相等,则只能不使用 s[i-1] 来匹配
dp[i][j] = dp[i-1][j]
# 返回 dp 数组的最后一个元素,表示 s 的子序列中 t 出现的个数
return dp[-1][-1]
二、力扣583. 两个字符串的删除操作
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 :
输入: word1 = “sea”, word2 = “eat”
输出: 2
解释: 第一步将 “sea” 变为 “ea” ,第二步将 "eat "变为 “ea”
-
确定 dp 数组的定义
dp[i][j] 表示使 word1[0:i-1] 和 word2[0:j-1] 相同所需删除字符的最小总数。 -
状态转移方程
(1)当 word1[i-1] == word2[j-1]:
说明当前字符已经匹配,不需要额外的删除操作,直接继承 dp[i-1][j-1] 的结果,即 dp[i][j] = dp[i-1][j-1]。
(2)当 word1[i-1] != word2[j-1]:
有三种可能的删除操作:
I. 删除 word1[i-1]:那么最少操作次数为 dp[i-1][j] + 1。
II. 删除 word2[j-1]:那么最少操作次数为 dp[i][j-1] + 1。
III. 同时删除 word1[i-1] 和 word2[j-1]:那么最少操作次数为 dp[i-1][j-1] + 2。
状态转移方程:dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1)。
这里特别需要注意的是,dp[i-1][j-1] + 2 是在同时删除两个字符的情况下出现的,而删除两个字符的操作等价于连续执行两次删除操作。因此,上述公式可以简化为:
简化后的公式:dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1)。 -
dp 数组的初始化
dp[i][0] 初始化:
当 word2 为空字符串时,word1[0:i-1] 要删除 i 个字符才能与 word2 相同,因此 dp[i][0] = i。
dp[0][j] 初始化:
当 word1 为空字符串时,word2[0:j-1] 要删除 j 个字符才能与 word1 相同,因此 dp[0][j] = j。 -
确定遍历顺序
由于 dp[i][j] 依赖于 dp[i-1][j-1]、dp[i-1][j] 和 dp[i][j-1],所以遍历时需要从左到右,从上到下进行。 -
最终结果
dp[len(word1)][len(word2)] 就是使两个字符串相同所需删除字符的最小总数。
代码实现
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
# 创建一个二维数组 dp,初始化为 0
dp = [[0] * (len(word2) + 1) for _ in range(len(word1) + 1)]
# 初始化 dp[i][0],表示 word2 为空时,word1 需要删除的字符数
for i in range(len(word1) + 1):
dp[i][0] = i
# 初始化 dp[0][j],表示 word1 为空时,word2 需要删除的字符数
for j in range(len(word2) + 1):
dp[0][j] = j
# 遍历 word1 和 word2,填充 dp 数组
for i in range(1, len(word1) + 1):
for j in range(1, len(word2) + 1):
if word1[i-1] == word2[j-1]:
# 如果 word1[i-1] 和 word2[j-1] 相等,不需要删除任何字符
dp[i][j] = dp[i-1][j-1]
else:
# 如果不相等,取删除操作的最小值
dp[i][j] = min(dp[i-1][j-1] + 2, dp[i-1][j] + 1, dp[i][j-1] + 1)
# 返回 dp 数组的最后一个元素,即使两个字符串相同所需删除字符的最小总数
return dp[-1][-1]
三、力扣72. 编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
-
确定 dp 数组的定义
dp[i][j] 表示将 word1[0:i-1] 转换为 word2[0:j-1] 所使用的最少操作数。 -
状态转移方程
当 word1[i-1] == word2[j-1]:
如果最后一个字符相同,则不需要做任何操作,dp[i][j] 直接等于 dp[i-1][j-1],即 dp[i][j] = dp[i-1][j-1]。
当 word1[i-1] != word2[j-1]:
需要进行编辑操作,分三种情况:
(1)删除 word1[i-1]:转换操作数等于 dp[i-1][j] + 1,表示将 word1[0:i-2] 转换为 word2[0:j-1] 后删除一个字符。
(2)删除 word2[j-1] 或 插入一个字符到 word1:转换操作数等于 dp[i][j-1] + 1,表示将 word1[0:i-1] 转换为 word2[0:j-2] 后在 word1 尾部插入一个字符。
(3)替换 word1[i-1] 为 word2[j-1]:转换操作数等于 dp[i-1][j-1] + 1,表示将 word1[0:i-2] 转换为 word2[0:j-2] 后替换一个字符。
状态转移方程:dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1 -
dp 数组的初始化
dp[i][0] 初始化:
将 word1 转换为空字符串 word2 需要删除所有字符,因此 dp[i][0] = i。
dp[0][j] 初始化:
将空字符串 word1 转换为 word2 需要插入所有字符,因此 dp[0][j] = j。 -
确定遍历顺序
由于 dp[i][j] 依赖于 dp[i-1][j-1]、dp[i-1][j] 和 dp[i][j-1],所以遍历时需要从左到右,从上到下进行。 -
最终结果
dp[len(word1)][len(word2)] 就是将 word1 转换为 word2 所使用的最少操作数。
代码实现
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
# 创建一个二维数组 dp,初始化为 0
dp = [[0] * (len(word2) + 1) for _ in range(len(word1) + 1)]
# 初始化 dp[i][0],表示将 word1 的前 i 个字符转换为空字符串所需的最少操作数
for i in range(len(word1) + 1):
dp[i][0] = i
# 初始化 dp[0][j],表示将空字符串转换为 word2 的前 j 个字符所需的最少操作数
for j in range(len(word2) + 1):
dp[0][j] = j
# 遍历 word1 和 word2,填充 dp 数组
for i in range(1, len(word1) + 1):
for j in range(1, len(word2) + 1):
if word1[i-1] == word2[j-1]:
# 如果 word1[i-1] 和 word2[j-1] 相等,不需要编辑操作
dp[i][j] = dp[i-1][j-1]
else:
# 如果不相等,取增、删、替换操作的最小值加 1
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
# 返回 dp 数组的最后一个元素,即将 word1 转换为 word2 所需的最少操作数
return dp[-1][-1]