给你两个单词
word1
和word2
, 请返回将word1
转换成word2
所使用的最少操作数 。你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
解法 动态规划+滚动数组
思路
dp数组的定义为 dp[i][j]
表示了 word1[0..i-1]
转换到 word2[0..j-1]
的最小操作数。
为什么是 i-1
和 j-1
呢?因为要将第一列和第一行表示为空字符串。
所以dp数组在初始化的时候要初始化第一行和第一列。
然后再思考状态转移方程:如果当前字符相等,那么不需要增加操作数。反之,则要更新 dp[i][j]
为
- 删除
word1[i - 1]
,即dp[i - 1][j] + 1
- 插入
word2[j - 1]
,即dp[i][j - 1] + 1
- 替换
word1[i - 1] -> word2[j - 1]
,即dp[i - 1][j - 1] + 1
这三种操作的最小操作步数。
当然这是经典动态规划的方法,还有利用滚动数组来优化空间。仔细观察会发现 dp[i][j]
只与上一行相关,所以可以将二维数组优化为两个一维数组。
代码
经典动态规划
function minDistance(word1: string, word2: string): number {
const m = word1.length;
const n = word2.length;
const dp = Array.from({length: m + 1}, () => Array(n + 1).fill(-1));
for (let i = 0; i <= n; i++) {
dp[0][i] = i;
}
for (let i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
}
}
}
return dp[m][n];
};
滚动数组优化空间
function minDistance(word1: string, word2: string): number {
const m = word1.length;
const n = word2.length;
let prev = new Array(n + 1).fill(0); // 上一行
let curr = new Array(n + 1).fill(0); // 当前行
for (let i = 0; i <= n; i++) {
prev[i] = i;
}
for (let i = 1; i <= m; i++) {
curr[0] = i; // 空串转换成 word1 的前 i 个字符,需要 i 次删除
for (let j = 1; j <= n; j++) {
if (word1[i - 1] === word2[j - 1]) {
curr[j] = prev[j - 1];
} else {
curr[j] = 1 + Math.min(prev[j], prev[j - 1], curr[j - 1]);
}
}
[prev, curr] = [curr, prev];
}
return prev[n];
};
时空复杂度
时间复杂度:O(m * n)
空间复杂度:经典动态规划 O(m * n)
,滚动数组 O(n)