经典动态规划:编辑距离

72. 编辑距离

 

进入今天内容之前,可以先阅读 动态规划解题套路框架动态规划设计:最长递增子序列 回顾一下动态规划问题的解题框架和套路

问题分析

详情可见题目 编辑距离

对于样例s1="horse", s2="ros",我们来手动模拟一下过程

前提:我们固定s2不变,对s1进行操作,使s1 = s2停止;同时我们从后往前开始分析

  1. 首先看一下初始状态,如下图 1 所示;
  2. 此时我们要处理的字符为[e, s],执行的操作是删除字符ei向前移动一位,现在状态如下图 2 所示;
  3. 此时我们要处理的字符为[s, s],不执行任何操作,i, j均向前移动一位,现在状态如下图 3 所示;
  4. 此时我们要处理的字符为[r, o],执行的操作是删除字符ri向前移动一位,现在状态如下图 4 所示;
  5. 此时我们要处理的字符为[o, o],不执行任何操作,i, j均向前移动一位,现在状态如下图 5 所示;
  6. 此时我们要处理的字符为[h, r],执行的操作是替换字符:h -> ri, j均向前移动一位,现在状态如下图 6 所示;

10

好了,到现在为止,整个模拟过程结束。我们执行了 3 次操作!!现在如果把上述过程交给计算机去处理,又应该如何进行呢??

我们可以人为的决策出最优的操作,但是计算机不可以。它不知道每一次该如何选择删除, 插入 or 替换

计算机虽然笨,但是它快呀!我们可以让计算机暴力穷举所有的选择,即:每一次进行选择的时候,把上述三种选择都执行一遍,然后选择出最优解!!

结合框架分析

对于这个问题,我们先明确一下「原问题」和「子问题」分别是什么?

「原问题」:s1[0..m]s2[0..n]的编辑距离

「子问题」:s1[0..i]s2[0..j]的编辑距离,其中0 <= i < m and 0 <= j < n

回顾一下动态规划的框架:明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义

递归暴力实现

根据上面的分析,我么可以写出第一版暴力的递归版本代码,如下:

此版本没有任何的优化,纯递归暴力!!

「递归+备忘录」实现

其实我们优化也是从「重叠子问题」入手!首先判断是否存在「重叠子问题」,然后再用一个「备忘录」记录子问题的结果

这里介绍一种判断是否存在「重叠子问题」小技巧

我们把上面的dp()函数简化一下,只抽出核心部分分析。简化版本如下所示:

现在画出执行时的部分递归树:

7

可以很明显的看到存在「重叠子问题」

所以优化思路也很容易给出:

「递推」实现

上面的「递归」方法是自顶向下,下面介绍「递推」自底向上的方法

「递归」核心是dp()函数,参数为状态,返回值为子问题的最优解

「递推」核心是dp[]数组,下标为状态,数组值为子问题的最优解

显然这个问题我们需要用到二维dp[][]数组,即dp[i][j]表示s1[0..i]s2[0..j]的编辑距离

确定 base case

同时我们还需要明确 base case 是什么?这个和「递归」中的 base case 有些许区别!!

首先我们将dp[][]和字符串的对应关系整体偏移一位,什么意思呢?直接看图:

8

可以看到 base case 即为第一行和第一列的值,至于为什么?我们可以这样理解!!

所以就很好理解了为什么该方法中的 base case 是这个样子了

确定遍历方向

现在又出现了另外一个问题,怎么遍历来填满dp[][]

9

注意看黄色区域,当s1[i] = s2[j]时,直接dp[i][j] = dp[i - 1][j - 1]

所以根据「当前状态是根据哪些之前状态转换而来」可以确定遍历的顺序,代码如下所示:

所以最终代码也可以写出: