[Week 9] LeetCode 72. Edit Distance

LeetCode 72. Edit Distance

问题描述

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:

  1. Insert a character
  2. Delete a character
  3. Replace a character

示例

Example 1

Input: word1 = "horse", word2 = "ros"
Output: 3
Explanation: 
horse -> rorse (replace 'h' with 'r')
rorse -> rose (remove 'r')
rose -> ros (remove 'e')

Example 2

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 所指字符,i 往右移,j 不动
    • 添加 j 所指字符,i 不动,j 往右移
    • 更改 i 所指字符,i 和 j 同时往右移
  • 如果 s 和 t 扫描未完成,继续上面的操作;否则:
    • s 扫描完,在 s 后按序添加 j 后所有字符
    • t 扫描完,删除 i 后所有字符

问题的关键就在 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) 就是我们的解!

废话不多少,直接上代码。

Code

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} 之间的距离。

你可能感兴趣的:(c++,算法,LeetCode,Dynamic,Programming)