LeetCode 115.不同的子序列:
思路:
分析题目可知要求的是,统计s的子序列中t出现的个数,其中子序列不是连续子序列。
动规五部曲:
- ① dp数组及含义:
dp[i][j]:以s[i - 1]结尾的子序列中含有 t[0:j](即[0, j - 1]的 t )的数量 - ② 递推式
首先依据s[i - 1] =? t[j - 1]来分类- s[i - 1] == t[j - 1]:采用s[i - 1]作为t子序列的一部分dp[i - 1][j - 1];不采用s[i - 1]作为t子序列的一部分dp[i - 1][j],然后dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]。(假设s[0:i]是bagg,t[0:j]是bag,s[3] = t[2],含有的数量应该是bag
g和bagg两个,也就是采用s[i - 1]和不采用s[i - 1]的数量相加) - s[i - 1] != t[j - 1],那么就只有不采用s[i - 1]一种了,dp[i][j] = dp[i - 1][j]
- s[i - 1] == t[j - 1]:采用s[i - 1]作为t子序列的一部分dp[i - 1][j - 1];不采用s[i - 1]作为t子序列的一部分dp[i - 1][j],然后dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]。(假设s[0:i]是bagg,t[0:j]是bag,s[3] = t[2],含有的数量应该是bag
- ③ 初始化
由递推式可知,dp[i][j]由左上方和上方推导得到,因此要初始化第0行和第0列。
从dp数组的含义出发:
dp[i][0]:即s[0: i]中删除字符出现空序列的数量,1
dp[0][j] : 空序列中删除字符出现 t[0 : j]的数量,0
dp[0][0]:空序列中删除字符出现空序列的数量,1 - ④ 遍历方式
由递推公式可知,dp[i][j]由两个蓝色部分推导得到,因此只要是在遍历到dp[i][j]时,两个蓝色部分已经被赋值的遍历方式均可。
先行后列 or 先列后行(顺序遍历)
- ⑤ 举例:
最后在dp[-1][-1]得到最终结果
代码
代码需要注意的是,题目说了最终结果要模7+10^9,那么比较好的做法是,在遍历赋值的时候就将dp的结果进行模运算,而不是求最终结果时再模(可能其它语言的话,最后进行模,dp的值可能在模之前已经溢出了,也就是超出数据类型的范围了)
class Solution:
def numDistinct(self, s: str, t: str) -> int:
lens, lent = len(s), len(t)
if lent <= 0: # t 为空字符串
return 1
if lens <= 0: # s 为空字符串
return 0
IMOD = 7 + 10**9 # 最终结果要对这个数取模
# 初始化
dp = [[0] * (lent + 1) for _ in range(lens + 1)]
for i in range(lens + 1): # 第0列置为1
dp[i][0] = 1
# 遍历
for i in range(1, lens + 1):
for j in range(1, lent + 1):
if s[i - 1] == t[j - 1]:
dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j]) % IMOD
else:
dp[i][j] = dp[i - 1][j]
return dp[lens][lent]
LeetCode 583.两个字符串的删除操作:
思路
动态规划1
动规五部曲
- ① dp数组及含义:
dp[i][j]:使word1[0, i - 1]和word2[0, j - 1]相同的最小步数 - ② 递推式:
首先根据word1[i - 1] =? word2[j - 1]来分类- word1[i - 1] == word2[j - 1],因为需要的是最小步数,因此可以默认相同的话保留为最终相同的字符,dp[i][j] = dp[i - 1][j - 1](或者换种方式理解,要么保留dp[i - 1][j - 1];要么不保留,那么就是删除一个字符:min(dp[i - 1][j] + 1, dp[i][j - 1] + 1),然后三个求min,实际用下来发现最小的都是dp[i - 1][j - 1])
- word1[i - 1] != word2[j - 1]:那就是不保留结尾的字符,也就是word1或word2删除一个字符(实际上两个都删除的情况被包含在删除一个字符中了),dp[i][j] = min(dp[i - 1][j] + 1(删除操作的步数),dp[i][j - 1] + 1)
- ③ 初始化
由递推公式可知,dp[i][j]由下图中三个蓝色部分得到,因此需要初始化第0行和第0列。
接着从dp数组的含义出发:
dp[0][j]:即使空字符和word2[0, j - 1]相同的最小步数为 j (全删除)
同理可得 dp[i][0]= i
- ④ 遍历方式:
先行后列 / 先列后行,但要是顺序遍历 - ⑤ 举例
最后得到dp[-1][-1]的值为最终结果
代码
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
len1, len2 = len(word1), len(word2)
# word1或者word2中有空字符,另一个需要删除全部字符才能相同
if len1 <= 0:
return len2
if len2 <= 0:
return len1
# 初始化
dp = [[0] * (len2 + 1) for _ in range(len1 + 1)]
# dp[0][j] = j, dp[i][0] = i
for i in range(1, len1 + 1):
dp[i][0] = i
for j in range(1, len2 + 1):
dp[0][j] = j
# 遍历
for i in range(1, len1 + 1):
for j in range(1, len2 + 1):
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1) # 删除一个字符
if word1[i - 1] == word2[j - 1]:
dp[i][j] = min(dp[i - 1][j - 1], dp[i][j]) # 删除一个字符与不删除字符的最小步数
return dp[len1][len2]
动态规划2
利用之前求最长公共子序列(非连续),
最小步数 = len(word1) + len(word2) - 长度*2
求最长公共子序列的动态规划:
- 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] + 1
- word1[i - 1] != word2[j - 1]:dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])
- 初始化
初始化为全0 - 遍历方式与动态规划1相同
- 举例
举例可见前面求最长公共子序列的题(非连续子序列)
"""
求最长公共子序列(非连续)的长度,然后len(word1) + len(word2) - 长度*2 = 最小步数
"""
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
len1, len2 = len(word1), len(word2)
# word1或word2中有空字符串
if len1 <= 0:
return len2
if len2 <= 0:
return len1
# 初始化
dp = [[0] * (len2 + 1) for _ in range(len1 + 1)] # 最长公共子序列的长度
# 遍历
for i in range(1, len1 + 1):
for j in range(1, len2 + 1):
if word1[i - 1] == word2[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 len1 + len2 - dp[len1][len2] * 2
LeetCode 72.编辑距离:
思路:
分析题目可知,可以对一个单词进行插入、删除和替换三种操作
动规五部曲:
- ① 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]
如果 !=,那么需要编辑,而编辑操作有三种:插入、删除和替换- 插入:需要理解的是,对word1插入一个字符==对word2删除一个字符。因此,插入操作是通过对word2删除一个字符达到的。也就是将word1[0, i - 1]转换为word2[0, j - 2]的最少操作数 + 1(删除word2[j - 1]),即dp[i][j] = dp[i][j - 1] + 1
- 删除:不相等时删除word1的一个字符,即将word1[0, i - 2]转换为word2[0, j - 1]的最少操作数 + 1(删除word1[i - 1]),dp[i][j] = dp[i - 1][j] + 1
- 替换:将word1[i - 1]替换为word2[j - 1],再求将word1[0, i - 2]转换为word2[j - 2]的最小操作数,dp[i][j] = dp[i - 1][j - 2] + 1。
然后三个求min
- ③ 初始化
由递推式可得dp[i][j]由三个蓝色部分推导得到,因此需要初始化第0行和第0列。
从dp数组的含义来理解
dp[0][j]:将空字符串转换为word2[0, j - 1]的最小操作数,就是插入 j 个字符嘛,即 j
dp[i][0]:将word1[0, i - 1]转换为空字符串的最小操作数,就是删除全部 i 个字符嘛,即 i
- ④ 遍历
先行后列/先列后行, 需要是顺序遍历(不管先行还是先列) - ⑤ 举例
最后的结果是dp[-1][-1]
代码
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
len1, len2 = len(word1), len(word2)
# word1或word2是空字符串
if len1 <= 0:
return len2
if len2 <= 0:
return len1
# 初始化
dp = [[0] * (len2 + 1) for _ in range(len1 + 1)]
for i in range(1, len1 + 1):
dp[i][0] = i
for j in range(1, len2 + 1):
dp[0][j] = j
# 遍历
for i in range(1, len1 + 1):
for j in range(1, len2 + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1
return dp[len1][len2]
学习收获:
不同的子序列:
- 因为要求最多出现的次数,因此即使相等,也要s[i - 1]包括进去的次数 + 不将s[i - 1]包括进去的次数
两个字符串的删除操作: - 方法1,思路类似上一题,但是相等的话不需要分类,不相等的话,因为两个字符串都可以删除,所以要分删哪个字符串
- 方法2:删除最小次数 = 两个字符串的长度 - 最长公共非连续子序列长度 *2 ,转换为求最长公共非连续子序列长度
编辑距离: - 本题的编辑操作有插入、删除和替换,因此不相等时还要分三种编辑操作,需要注意的是,在word1插入相当于在word2删除,从而转换为删除word2元素;以及删除word1元素和替换word1元素,最后三个求min