115. 不同的子序列
题目描述
给你两个字符串 s
和 t
,统计并返回在 s
的 子序列 中 t
出现的个数,结果需要对
1
0
9
+
7
10^9 + 7
109+7 取模。
示例 1:
输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
示例 2:
输入:s = "babgbag", t = "bag"
输出:5
解释:
如下所示, 有 5 种可以从 s 中得到 "bag" 的方案。
babgbag
^^ ^
babgbag
^^ ^
babgbag
^ ^^
babgbag
^ ^^
babgbag
^^^
提示:
1 <= s.length, t.length <= 1000
s
和t
由英文字母组成
题解
动态规划解决,基本还是采用子序列问题的框架,不过为了求序列的不同组合方式,需要修改一下状态转移方程。
-
dp
数组的含义:dp[i][j]
表示在s
的前i
个字符组成的子串s[0...i-1]
中,t
的前j
个字符组成的子串t[0...j-1]
出现的次数。 -
状态转移方程:
-
如果
s[i - 1] == t[j - 1]
,匹配成功,一方面可以“继承”此前的结果,即dp[i - 1][j - 1]
;另一方面,也要加上dp[i - 1][j]
,即s
不添加最后这个s[j - 1]
之前,子序列中t[0...j-1]
的个数。故最终有dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
。例如,考虑
s = "bagg"
和t = "bag"
:s[3] == t[2]
,如果用它匹配,相当于取s[0, 1, 3]
来构成t
,此时dp[i][j] = dp[i - 1][j - 1]
;但是,我们也可以不用
s[3]
而用s[2]
来匹配'g'
,相当于取s[0, 1, 2]
来构成t
,此时dp[i][j] = dp[i - 1][j]
。因此,上述两种情况都要加上才是完整的
dp[i][j]
。 -
否则,说明
s[i - 1]
是 “没用的” ,t[0...j-1]
出现次数和上次一样 ,即dp[i][j] = dp[i - 1][j]
。
-
-
初始化:要在
s
的子序列中找t
,那么如果t
是空串,自然存在于任意长度的s
的子序列中(算作出现1次),即dp[i][0] = 1
。
代码(C++)
int numDistinct(string s, string t)
{
vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1, 0));
for (int i = 0; i < dp.size(); ++i)
dp[i][0] = 1;
for (int i = 1; i <= s.size(); ++i) {
for (int j = 1; j <= t.size(); ++j) {
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[s.size()][t.size()] % ((uint64_t)1e9 + 7);
}
583. 两个字符串的删除操作
题目描述
给定两个单词 word1
和 word2
,返回使得 word1
和 word2
相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 1:
输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"
示例 2:
输入:word1 = "leetcode", word2 = "etco"
输出:4
提示:
1 <= word1.length, word2.length <= 500
word1
和word2
只包含小写英文字母
题解
由于只提供删除操作,这题本质上就是一个最长公共子序列问题:找到两个单词的最长公共子序列,然后各自将多余的字符删除即可,这样的操作步数显然也是最少的。
代码(C++)
int minDistance(string word1, string word2)
{
// 求最长公共子序列长度
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
for (int i = 1; i <= word1.size(); ++i) {
for (int j = 1; j <= word2.size(); ++j) {
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]);
}
}
int maxPubLen = dp.back().back();
return word1.size() - maxPubLen + word2.size() - maxPubLen;
}
72. 编辑距离
题目描述
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
提示:
0 <= word1.length, word2.length <= 500
word1
和word2
由小写英文字母组成
题解
这题感觉是动态规划子序列问题里比较综合的了,得对 dp
数组的意义和用法有比较好的认识才行。
🔗 以下内容主要基于 LeetCode官方题解
根据题目,我们可以对任意一个单词进行三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
我们将给定的两个单词分别记为 A
和 B
,那么一共就有6种操作。
我感觉题目的描述有些误导性 🤔 :所谓 “将
word1
转换成word2
” 并不是说word2
不变、让word1
往word2
变,而是说通过对这两个单词的操作,最终使它俩相同。因此,下面将 “
A
到B
的编辑距离” 之类的表达统一改为 “A
和B
的编辑距离” ,以免歧义
不过,可以发现:
- 对单词
A
删除一个字符和对单词B
插入一个字符是等价的。 例如当单词A
为doge
,单词B
为dog
时,我们既可以删除单词A
的最后一个字符e
,得到相同的dog
,也可以在单词B
末尾添加一个字符e
,得到相同的doge
- 同理, 对单词
B
删除一个字符和对单词A
插入一个字符也是等价的 - 对单词
A
替换一个字符和对单词B
替换一个字符是等价的。 例如当单词A
为bat
,单词B
为cat
时,我们修改单词A
的第一个字母b -> c
,和修改单词B
的第一个字母c -> b
是等价的
所以,实际上我们只需要考虑三种不同的操作:
- 在
A
中插入一个字符 - 在
B
中插入一个字符 - 替换
A
中的一个字符
下面,以 A = "horse"
和 B = "ros"
为例进一步探索:
-
在
A
中插入一个字符: 如果我们知道horse
和ro
(即A
到B - 's'
)的编辑距离为a
,那么显然horse
和ros
(即A
和B
)的编辑距离不会超过a + 1
。这是因为我们可以在a
次操作后将A
和B - 's'
变为相同的字符串,只需要额外的1
次操作,在单词A
的末尾添加字符's'
,就能在a + 1
次操作后将A
和B
变为相同的字符串 -
在 B 中插入一个字符: 如果我们知道
hors
到ros
的编辑距离为b
,那么显然horse
到ros
的编辑距离不会超过b + 1
,原因同上 -
替换
A
的一个字符: 如果我们知道hors
到ro
的编辑距离为c
,那么显然horse
到ros
的编辑距离不会超过c + 1
,原因同上(可以理解为:两个单词经过c
次操作已经相同了,此时它们末尾各新增了一个字符且不相同,那么将其中一个替换成另一个就行了)
综上所述, horse
和 ros
的编辑距离应该为 min(a + 1, b + 1, c + 1)
。
不过对于第三种情况,如果两个单词新增的字符是相同的,自然不用替换,操作次数仍是 c
。
⚠️ 注意:为什么我们总是在单词
A
和B
的末尾插入或者修改字符,能不能在其它的地方进行操作呢?答案是可以的,但是我们知道,操作的顺序是不影响最终的结果的。例如对于单词
cat
,我们希望在c
和a
之间添加字符d
并且将字符t
修改为字符b
,那么这两个操作无论为什么顺序,都会得到最终的结果cdab
。
于是我们可以开始分析动态规划的老朋友们:
-
dp
数组的含义:dp[i][j]
表示word1
的前i
个字符组成的子串(即word1[0...i-1]
)和word2
的前j
个字符组成的子串(即word2[0...j-1]
)的编辑距离 -
几个操作对应的状态:基于前面的分析可知
- 在
word1
中插入一个字符,对应dp[i - 1][j] + 1
- 在
word2
中插入一个字符,对应dp[i][j - 1] + 1
- 在某个单词中替换一个字符,对应
dp[i - 1][j - 1] + 1
- 在
-
状态转移方程:
- 如果
word1[i - 1] == word2[j - 1]
,则 “替换” 的操作是可以免除的,所以有dp[i][j] = min({dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1]})
- 否则,上述三种状态取最小值,即
dp[i][j] = min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}) + 1
- 如果
-
初始化:如果某个单词是空串(长度为0),那么编辑过程无非两种
- 向空串逐个插入另一个单词的字符,操作数为另一个单词的长度
- 将另一个单词中的字符逐个删除,操作数仍为它的长度
所以初始化
dp
数组中dp[i][0] = i
,dp[0][j] = j
。
代码(C++)
int minDistance(string word1, string word2)
{
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
for (int i = 1; i <= word1.size(); ++i)
dp[i][0] = i;
for (int j = 1; j <= word2.size(); ++j)
dp[0][j] = j;
for (int i = 1; i <= word1.size(); ++i) {
for (int j = 1; j <= word2.size(); ++j) {
if (word1[i - 1] == word2[j - 1])
dp[i][j] = min({dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1]});
else
dp[i][j] = min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}) + 1;
}
}
return dp[word1.size()][word2.size()];
}
Python
def minDistance(self, word1: str, word2: str) -> int:
dp = [[0] * (len(word2) + 1) for _ in range(len(word1) + 1)]
for i in range(1, len(word1) + 1):
dp[i][0] = i
for j in range(1, len(word2) + 1):
dp[0][j] = j
for i in range(1, len(word1) + 1):
for j in range(1, len(word2) + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1])
else:
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
return dp[-1][-1]
Golang
func minDistance(word1 string, word2 string) int {
dp := make([][]int, len(word1)+1)
for i := range dp {
dp[i] = make([]int, len(word2)+1)
}
for i := 1; i <= len(word1); i++ {
dp[i][0] = i
}
for j := 1; j <= len(word2); j++ {
dp[0][j] = j
}
for i := 1; i <= len(word1); i++ {
for j := 1; j <= len(word2); j++ {
if word1[i-1] == word2[j-1] {
dp[i][j] = min(dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1])
} else {
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
}
}
}
return dp[len(word1)][len(word2)]
}