文章目录
day56学习内容
day56主要内容
- 2个字符串的删除操作
- 编辑距离
声明
本文思路和文字,引用自《代码随想录》
一、2个字符串的删除操作
LCS:求解两个字符串的最长公共子序列
1.1、动态规划五部曲
1.1.1、 确定dp数组(dp table)以及下标的含义
- dp[i][j]表示字符串 word1 的前 i 个字符和字符串 word2 的前 j 个字符的最长公共子序列的长度。
1.1.2、确定递推公式
考虑两种情况:
-
字符匹配情况:
- 如果
word1
的第i
个字符和word2
的第j
个字符相同,即word1[i-1] == word2[j-1]
(因为字符串的索引从0开始),那么这两个字符可以成为LCS的一部分。 - 在这种情况下,这两个字符增加了LCS的长度,因此
dp[i][j] = dp[i-1][j-1] + 1
。这表明我们将这两个匹配的字符加入到LCS中,从而增加LCS的长度。
- 如果
-
字符不匹配情况:
- 如果
word1[i-1]
和word2[j-1]
不相同,我们需要从两种选择中选取较优的一种,即不包括word1
的第i
个字符或不包括word2
的第j
个字符。 - 因此,我们可以考虑两种情况,一种是保留
word1
的前i-1
个字符和word2
的前j
个字符的LCS(即不包括word1[i-1]
),另一种是保留word1
的前i
个字符和word2
的前j-1
个字符的LCS(即不包括word2[j-1]
)。这样,dp[i][j] = max(dp[i-1][j], dp[i][j-1])
。
- 如果
1.1.3、 dp数组如何初始化
- 当
i = 0
或j = 0
时,即一个字符串的长度为0,显然最长公共子序列的长度也为0,因为空字符串与任何字符串的公共子序列也是空,所以dp[0][j] = 0
和dp[i][0] = 0
。
1.1.4、确定遍历顺序
从小到大遍历
1.1.5、输出结果
- 最终的最长公共子序列长度存储在
dp[len1][len2]
。 - 最小编辑距离计算为
len1 + len2 - 2 * dp[len1][len2]
。这个公式的由来是:len1 - dp[len1][len2]
表示word1
需要删除的字符数,len2 - dp[len1][len2]
表示需要向word1
插入word2
中的字符数,因此总编辑距离是两者之和。
1.2、代码
class Solution {
public int minDistance(String word1, String word2) {
// 获取两个字符串的长度
int len1 = word1.length();
int len2 = word2.length();
// 创建一个二维数组dp,用于存储最长公共子序列的长度
int[][] dp = new int[len1 + 1][len2 + 1];
// 初始化dp数组的第0行和第0列,因为任何字符串与空字符串的LCS长度为0
for (int i = 0; i <= len1; i++) {
dp[i][0] = 0;
}
for (int j = 0; j <= len2; j++) {
dp[0][j] = 0;
}
// 填充dp数组
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
// 如果当前两个字符相同,增加LCS的长度
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
// 如果不相同,取两种可能的最大LCS长度
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// 计算将一个字符串转换成另一个字符串的最小编辑次数
// 总字符数减去两倍的LCS长度
return len1 + len2 - dp[len1][len2] * 2;
}
}
二、编辑距离
编辑距离是指将一个字符串转换成另一个字符串所需的最小单字符编辑操作次数,包括插入、删除和替换操作。
2.1、动态规划五部曲
2.1.1、 确定dp数组(dp table)以及下标的含义
- 其中dp[i][j] 表示word1 的前 i 个字符转换成 word2 的前 j 个字符所需的最小编辑距离。
2.1.2、确定递推公式
定义 dp[i][j]
为字符串 word1
的前 i
个字符到字符串 word2
的前 j
个字符的最小编辑距离。这里 word1[0...i-1]
和 word2[0...j-1]
分别表示字符串 word1
和 word2
的子串。
-
初始化:
dp[i][0] = i
表示将word1
的前i
个字符全部删除到空字符串的编辑距离,需要i
次删除。dp[0][j] = j
表示从空字符串插入到word2
的前j
个字符的编辑距离,需要j
次插入。
-
填充表格:
- 对于每一对字符
word1[i-1]
和word2[j-1]
,我们考虑以下情况:- 如果两个字符相同(
word1[i-1] == word2[j-1]
),那么不需要额外操作,此时dp[i][j] = dp[i-1][j-1]
。 - 如果两个字符不同,我们可以进行三种操作:
- 替换:将
word1[i-1]
替换为word2[j-1]
,使其相同,此时考虑的状态为dp[i-1][j-1]
,操作后成本增加1,即dp[i][j] = dp[i-1][j-1] + 1
。 - 插入:在
word1
中的位置i
插入word2[j-1]
,此时word2
的位置j
已被匹配,考虑的状态为dp[i][j-1]
,操作后成本增加1,即dp[i][j] = dp[i][j-1] + 1
。 - 删除:删除
word1
中的字符word1[i-1]
,此时考虑的状态为dp[i-1][j]
,操作后成本增加1,即dp[i][j] = dp[i-1][j] + 1
。
- 替换:将
- 选择上述三种操作中的最小成本作为
dp[i][j]
的值。
- 如果两个字符相同(
- 对于每一对字符
2.1.3、 dp数组如何初始化
- 字符串长度:
m
和n
分别是word1
和word2
的长度。 - 动态规划数组
dp
:大小为(m+1) x (n+1)
的二维数组,dp[i][j]
表示word1
的前i
个字符转换成word2
的前j
个字符所需的最小编辑距离。 - 初始化行和列:
- 第一列:
dp[i][0]
表示把word1
的前i
个字符全部删除变为空字符串所需的最小操作次数,因此为i
。 - 第一行:
dp[0][j]
表示从空字符串通过插入操作变成word2
的前j
个字符所需的最小操作次数,因此为j
。
2.1.4、确定遍历顺序
-
初始化阶段:
- 第一列初始化:从
i = 1
到i = m
。这一步设置了从空字符串到word1
的前i
个字符的转换,每步都需要一次删除操作。 - 第一行初始化:从
j = 1
到j = n
。这一步设置了从空字符串到word2
的前j
个字符的转换,每步都需要一次插入操作。
- 第一列初始化:从
-
填充动态规划表:
- 双层循环遍历:
- 外层循环(对
word1
的遍历):从i = 1
到i = m
。这个循环逐个考虑word1
的每一个字符,直到最后一个字符。 - 内层循环(对
word2
的遍历):从j = 1
到j = n
。在每次外层循环中,这个内层循环逐个考虑word2
的每一个字符,直到最后一个字符。
- 外层循环(对
- 双层循环遍历:
2.1.5、输出结果
- 最后一个元素:
dp[m][n]
表示将word1
完全转换为word2
的最小编辑距离。
2.2、代码
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length(); // 获取 word1 的长度
int n = word2.length(); // 获取 word2 的长度
// 创建动态规划表,大小为 (m+1) x (n+1)
int[][] dp = new int[m + 1][n + 1];
// 初始化第一列,从空字符串变为 word1 的前 i 个字符需要 i 次删除
for (int i = 1; i <= m; i++) {
dp[i][0] = i;
}
// 初始化第一行,从空字符串变为 word2 的前 j 个字符需要 j 次插入
for (int j = 1; 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 {
// 如果字符不同,计算替换、插入和删除操作的最小成本
// dp[i-1][j-1] + 1: 替换 word1[i-1] 使其变为 word2[j-1]
// dp[i][j-1] + 1: 在 word1 中插入 word2[j-1]
// dp[i-1][j] + 1: 从 word1 中删除字符 word1[i-1]
dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
}
}
}
// 最终的 dp[m][n] 就是从 word1 变为 word2 所需的最小编辑次数
return dp[m][n];
}
}
总结
1.感想
- 明天就要结束动态规划了。。刷了半个月
2.思维导图
本文思路引用自代码随想录,感谢代码随想录作者。