Given two words word1 and word2, find the minimum number of operations required to convert word1 to word2.
You have the following 3 operations permitted on a word:
Input: word1 = "horse", word2 = "ros"
Output: 3
Explanation:
horse -> rorse (replace 'h' with 'r')
rorse -> rose (remove 'r')
rose -> ros (remove 'e')
Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation:
intention -> inention (remove 't')
inention -> enention (replace 'i' with 'e')
enention -> exention (replace 'n' with 'x')
exention -> exection (replace 'n' with 'c')
exection -> execution (insert 'u')
Edit Distance
是 DP 的一个经典问题了,其求解的是两个不同的字符串如何通过最少的操作来转换。
DP 最重要的是如何设计状态转移方程,我们可以从这个角度入手,修改一个字符串有三个操作:删除、添加或更换字符,这对于我们设计子问题具有启发性!
考虑长度为 m 的字符串 s 和长度为 n 的字符串 t,最简单的想法就是分别从头到尾扫描 s 和 t:(其中 i 为扫描 s 的指针,j 为扫描 t 的指针)
i = j
,则 i 和 j 同时往右移i != j
,则对 s(三选一):
问题的关键就在 i != j
这一步,三种方式我们应该选择哪一种呢?或许我们可以把问题抽象成 E(i, j)
表示 s[1..i]
转化成 t[1..j]
所需操作的最少步数。如果得到 E(i, j)
这个状态呢?答案是:
E(i, j) = min{E(i-1, j) + 1, E(i, j-1) + 1, E(i-1, j-1) + diff(i, j)}
其中:
E(i-1, j) + 1
表示 s[1..i-1]
与 t[1..j]
匹配,即删除 s[i]
E(i, j-1) + 1
表示 s[1..i]
与 t[1..j-1]
匹配,即添加 t[j]
至 s[i]
后E(i-1, j-1) + diff(i, j)
表示 s[1..i-1]
与 t[1..j-1]
匹配,即根据 s[i]
与 t[j]
异同决定修改观察一下状态转移方程 E(i, j) = min{E(i-1, j) + 1, E(i, j-1) + 1, E(i-1, j-1) + diff(i, j)}
,其依赖的数据都是比它更小的问题,说明我们只需要从小问题算起,最后 E(m, n)
就是我们的解!
废话不多少,直接上代码。
class Solution {
public:
int minDistance(string word1, string word2)
{
int subProblemTable[word1.size() + 1][word2.size() + 1];
// word1[1..i] -> "" needs at least i times operation
for (size_t i = 0; i <= word1.size(); ++i)
subProblemTable[i][0] = i;
// word2[1..i] -> "" needs at least i times operation
for (size_t i = 0; i <= word2.size(); ++i)
subProblemTable[0][i] = i;
for (size_t i = 1; i <= word1.size(); ++i)
{
for (size_t j = 1; j <= word2.size(); ++j)
{
int min = subProblemTable[i][j - 1] + 1;
if (min > (subProblemTable[i - 1][j] + 1))
min = subProblemTable[i - 1][j] + 1;
int diff = word1[i - 1] == word2[j - 1] ? 0 : 1;
if (min > (subProblemTable[i - 1][j - 1] + diff))
min = subProblemTable[i - 1][j - 1] + diff;
subProblemTable[i][j] = min;
}
}
return subProblemTable[word1.size()][word2.size()];
}
};
如果你到这里还是没明白,不妨把 subProblemTable 打印一下:
# word1 = "intention"
# word2 = "execution"
. . e x e c u t i o n
. 0 1 2 3 4 5 6 7 8 9
i 1 1 2 3 4 5 6 6 7 8
n 2 2 2 3 4 5 6 7 7 7
t 3 3 3 3 4 5 5 6 7 8
e 4 3 4 3 4 5 6 6 7 8
n 5 4 4 4 4 5 6 7 7 7
t 6 5 5 5 5 5 5 6 7 8
i 7 6 6 6 6 6 6 5 6 7
o 8 7 7 7 7 7 7 6 5 6
n 9 8 8 8 8 8 8 7 6 5
# Output: 5
# Explanation:
# intention -> inention (remove 't')
# inention -> enention (replace 'i' with 'e')
# enention -> exention (replace 'n' with 'x')
# exention -> exection (replace 'n' with 'c')
# exection -> execution (insert 'u')
从代码看到,事实上我们是在逐行填写 subProblemTable,在填写每个单元格用时都是 O(1),因此总的时间复杂度恰好是表格的规则,即 O(mn)
每个动态规划都隐含着一个 dag 结构:试想用每个节点表示一个子问题,而每条边表示解决子问题时所需要遵循的先后约束。在编辑距离问题中,dag 中的节点对应于子问题,或者,等价地说,对应于表格中的位置 (i, j)
。其边为先后关系的约束,形如 (i-1, j) -> (i, j)
、(i, j-1) -> (i, j)
和 (i-1, j-1) -> (i, j)
。实际上,我们可以更进一步,在边上赋予一定的权值,于是求编辑距离就变成了求 dag 中的最短路径!为了看清这一点,除了令 {(i-1, j-1) -> (i, j): s[i] = t[j]}
中的边的长度长度都为 0 外,我们设其它所有边的长度均为 1。编辑距离的最终答案就是点 s={0, 0}
与 t={m, n}
之间的距离。