115.不同的子序列
题目链接 :https://leetcode.cn/problems/distinct-subsequences/description/
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)
题目数据保证答案符合 32 位带符号整数范围。

提示:
- 0 <= s.length, t.length <= 1000
- s 和 t 由英文字母组成
总结
1. 定义 DP 数组
定义 dp[i][j] 为:以 s 的前 i 个字符组成的子串中,出现 t 的前 j 个字符组成的子串的个数。
-
s的长度为m,t的长度为n。 -
dp的大小为(m + 1) x (n + 1)。
2. 初始化
-
当 t 为空字符串时:
无论 s 是什么,空字符串都是 s 的子序列(删除所有字符即可),且只有 1 种方式。
所以,对于任意 i,dp[i][0] = 1。
-
当 s 为空字符串但 t 不为空时:
空字符串无法生成非空字符串。
所以,对于任意 j > 0,dp[0][j] = 0。
3. 状态转移方程
我们需要遍历 s 的每个字符(索引 i 从 1 到 m)和 t 的每个字符(索引 j 从 1 到 n)。
注意:在代码中访问字符串字符时,要用 s.charAt(i-1) 和 t.charAt(j-1)。
有两种情况:
-
情况一:当前字符不匹配 (s[i-1] != t[j-1])
如果 s 的当前字符和 t 的当前字符不同,那么 s 的这个字符对组成 t 没有任何帮助。
此时,在 s 的前 i 个字符中找到 t 的个数,就等于在 s 的前 i-1 个字符中找到 t 的个数。
javadp[i][j] = dp[i-1][j]; -
情况二:当前字符匹配 (s[i-1] == t[j-1])
如果两个字符相同,我们有两种选择:
-
使用
s[i-1]进行匹配 :这意味着我们用掉了s的这个字符和t的这个字符。剩下的问题变成了:在s的前i-1个字符中找t的前j-1个字符。方案数为dp[i-1][j-1]。 -
不使用
s[i-1]进行匹配 :虽然字符相同,但我们可以选择"跳过"s的这个字符,继续尝试用s前面的部分来匹配整个t(也许s前面还有同样的字符)。方案数为dp[i-1][j]。
总方案数为两者之和:
javadp[i][j] = dp[i-1][j] + dp[i-1][j-1];4. 图解示例
以
s = "babgbag",t = "bag"为例,表格填充逻辑如下:"" (0) b (1) a (2) g (3) "" (0) 1 0 0 0 b (1) 1 1 0 0 a (2) 1 1 1 0 b (3) 1 2 1 0 g (4) 1 2 1 1 b (5) 1 3 1 1 a (6) 1 3 4 1 g (7) 1 3 4 5 -
-
解释
dp[3][1](s="bab", t="b"):-
s[2]是'b',t[0]是'b'。 -
用s的第二个'b': 看
dp[2][0](值为1)。 -
不用s的第二个'b': 看
dp[2][1](值为1)。 -
总数 = 1 + 1 = 2。
-
5. 代码实现
java
class Solution {
public int numDistinct(String s, String t) {
int m = s.length();
int n = t.length();
// 如果 s 比 t 短,肯定无法组成子序列
if (m < n) {
return 0;
}
// 定义 dp 数组,多一行一列处理空字符串的情况
// dp[i][j] 表示 s 的前 i 个字符包含 t 的前 j 个字符的方案数
int[][] dp = new int[m + 1][n + 1];
// 初始化:当 t 为空字符串时,只有一种方案(即删除 s 的所有字符)
for (int i = 0; i <= m; i++) {
dp[i][0] = 1;
}
// 注意:Java int数组默认初始化为0,所以 dp[0][j] (j>0) 已经是0了,无需手动设置
// 开始填充 dp 表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 注意:字符串索引从 0 开始,所以是 charAt(i-1) 和 charAt(j-1)
if (s.charAt(i - 1) == t.charAt(j - 1)) {
// 字符匹配:
// 1. 用 s[i-1] 匹配 t[j-1] -> dp[i-1][j-1]
// 2. 不用 s[i-1] 匹配 (跳过它) -> dp[i-1][j]
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
} else {
// 字符不匹配:只能跳过 s[i-1],沿用之前的匹配结果
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[m][n];
}
}
583.两个字符串的删除操作
题目链接 :https://leetcode.cn/problems/delete-operation-for-two-strings/description/
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
示例:
- 输入: "sea", "eat"
- 输出: 2
- 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
总结
1. 定义 DP 数组
dp[i][j] 表示:想要让 word1 的前 i 个字符和 word2 的前 j 个字符变得相同,所需的 最小步数(最小删除次数)。
2. 初始化
-
当 word2 为空 (j=0):
word1 的前 i 个字符要变成空字符串,必须把这 i 个字符全删掉。
所以 dp[i][0] = i。
-
当 word1 为空 (i=0):
同理,word2 的前 j 个字符要变成空字符串,必须全删掉。
所以 dp[0][j] = j。
3. 状态转移方程
我们遍历 word1 (索引 i) 和 word2 (索引 j)。
-
情况一:当前字符相同 (word1[i-1] == word2[j-1])
既然两个字符一样,那就不需要删除它们,它们可以作为最终结果的一部分。
这时候的最小步数等于去掉这两个字符之前的状态。
javadp[i][j] = dp[i-1][j-1] -
情况二:当前字符不同 (word1[i-1] != word2[j-1])
既然不同,为了让它们相同,我们必须至少删除其中一个字符。我们选代价最小的那种方案:
-
删除
word1的当前字符 :步数是dp[i-1][j] + 1。 -
删除
word2的当前字符 :步数是dp[i][j-1] + 1。
取两者的最小值:
javadp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1 -
4. 图解示例
输入:word1 = "sea", word2 = "eat"
| "" (0) | e (1) | a (2) | t (3) | |
|---|---|---|---|---|
| "" (0) | 0 | 1 | 2 | 3 |
| s (1) | 1 | 2 | 3 | 4 |
| e (2) | 2 | 1 | 2 | 3 |
| a (3) | 3 | 2 | 1 | 2 |
-
看
dp[2][1]('se' vs 'e'):-
word1是 'e',word2是 'e'。相等! -
直接继承左上角
dp[1][0](值为 1)。 -
解释:'s' vs "" 需要删1步。加上相同的 'e' 后,步数不变。
-
-
看
dp[3][3]('sea' vs 'eat'):-
word1是 'a',word2是 't'。不相等。 -
删 'a' (看上方
dp[2][3]=3) -> 3+1=4。 -
删 't' (看左方
dp[3][2]=1) -> 1+1=2。 -
取最小:2。
-
5. 代码实现
java
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// dp[i][j] 表示 word1的前i位 和 word2的前j位 达到相同所需的最小删除步数
int[][] dp = new int[m + 1][n + 1];
// 初始化第一列:word2为空,word1需要删 i 次
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
// 初始化第一行:word1为空,word2需要删 j 次
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
// 填充 DP 表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 如果字符相同,不需要删除,直接继承状态
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
// 如果字符不同,尝试删 word1 或 删 word2,取最小代价 + 1
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1;
}
}
}
return dp[m][n];
}
}
72.编辑距离
题目链接 :https://leetcode.cn/problems/edit-distance/description/
给你两个单词 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 由小写英文字母组成
总结
1. 定义 DP 数组
dp[i][j] 表示:将 word1 的前 i 个字符转换成 word2 的前 j 个字符,所需要的 最小操作数。
2. 初始化
-
当 word2 为空 (j=0):
要把 word1 的前 i 个字符变成空串,只能全部删除。
javadp[i][0] = i -
当 word1 为空 (i=0):
要把空串变成 word2 的前 j 个字符,只能全部插入。
javadp[0][j] = j
3. 状态转移方程
我们比较 word1[i-1] 和 word2[j-1]:
情况一:字符相同 (word1[i-1] == word2[j-1])
既然字符一样,我们不需要做任何操作。当前的最小步数直接继承自不包含这两个字符的状态。
java
dp[i][j] = dp[i-1][j-1]
情况二:字符不同 (word1[i-1] != word2[j-1])
我们必须进行一次操作(步数 +1)。我们有三种选择,取最小的那个:
-
插入 (Insert):
我们在 word1 的末尾插入一个字符来匹配 word2[j-1]。
这就相当于:我们搞定了 word2 的当前字符,但 word1 的当前字符还没动。
javadp[i][j-1] + 1 -
删除 (Delete):
我们删除 word1 的当前字符。
这就相当于:word1 的这个字符废了,我们要用 word1 前面的部分去匹配 word2。
javadp[i-1][j] + 1 -
替换 (Replace):
我们将 word1 的当前字符直接替换成 word2 的当前字符。
这就相当于:两个字符都处理完了,大家都退一步。
javadp[i-1][j-1] + 1
总结方程:
java
dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1
4. 图解示例
horse -> ros
| "" (0) | r (1) | o (2) | s (3) | |
|---|---|---|---|---|
| "" (0) | 0 | 1 | 2 | 3 |
| h (1) | 1 | 1(替) | 2 | 3 |
| o (2) | 2 | 2 | 1(等) | 2 |
| r (3) | 3 | 2(等) | 2 | 2 |
| s (4) | 4 | 3 | 3 | 2(等) |
| e (5) | 5 | 4 | 4 | 3 |
-
看
dp[5][3](horse vs ros):-
evss不等。 -
左边
dp[5][2](horse -> ro) = 4,插入 's' -> 5。 -
上边
dp[4][3](hors -> ros) = 2,删除 'e' -> 3。 -
左上
dp[4][2](hors -> ro) = 2,替换 'e' 为 's' -> 3。 -
最小值是 3。
-
5. 代码实现
java
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// dp[i][j] 表示将 word1 的前 i 个字符转换成 word2 的前 j 个字符的最少操作数
int[][] dp = new int[m + 1][n + 1];
// 初始化:word1 为空变成 word2,需要插入 j 次
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
// 初始化:word1 变成空 word2,需要删除 i 次
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
// 遍历填充
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 如果字符相同,不需要操作,直接继承左上角
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
// 如果字符不同,尝试三种操作取最小值
// dp[i-1][j-1] + 1 => 替换
// dp[i][j-1] + 1 => 插入
// dp[i-1][j] + 1 => 删除
dp[i][j] = Math.min(dp[i - 1][j - 1],
Math.min(dp[i][j - 1], dp[i - 1][j])) + 1;
}
}
}
return dp[m][n];
}
}