编辑距离(edit distance)

编辑距离

LeetCode 72. 编辑距离

概念

编辑距离,是指将字符串word1通过替换、删除、增加字符的操作,变成字符串word2的最小次数。

用途

编辑距离可以用在自然语言处理中,例如拼写检查可以根据一个拼错的字和其他正确的字的编辑距离,判断哪一个(或哪几个)是比较可能的字。

DNA也可以用A、C 、G和T组成的字符串表示,因此编辑距离也可以用在生物信息学中,判断两个DNA的相似程度。

Unix下的diff及patch命令也是利用编辑距离来进行文本编辑对比的例子。

动态规划简介

动态规划,是指当前状态是由之前的状态通过转移方程(即某一种或几种方跃迁方式)得到的。也就是说,通过设法利用之前所存储的状态(一般是一维、二维、三维数组等存放状态),来更新现有的状态。最后,依次迭代,获得最终状态。

这种类型题是用了动态规划的做法来做。

题目描述

给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  1. 插入一个字符
  2. 删除一个字符
  3. 替换一个字符

示例 1:

输入: word1 = "horse", word2 = "ros"
输出: 3
解释: 
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

输入: word1 = "intention", word2 = "execution"
输出: 5
解释: 
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

初始化dp(重要)

以示例一为例,对二维数组dp进行初始化。dp数组的大小为dp[word1.size()+1][word2.size()+1]。数组大小原因如下:

  1. 可能要对字符串word1的头部插入1到word2.size()个字符,使其变成word2字符串。这便是带有\color{green}{*}一行初始化的操作意义。
  2. 可能对字符串word1的头部进行1到word1.size()的删除,伴随其他增、改操作,以其变成word2字符串。这便是带有\color{red}{*}那一列初始化的操作意义。
0 \color{red}{*} r o s
\color{green}{*} 0 1 2 3
h 1
o 2
r 3
s 4
e 5

状态转移方程(核心)

当前状态,可由上一个状态A加上增加一个字符、上一个状态B修改(或不修改)一个字符或上一个状态C删除一个字符得到。每个状态包含着该状态最小操作步数。

如:

h o r s e

r o s

中, h与r的编辑操作中,显然是将h修改成r为最小操作数(一个操作数)。而不是将h前面加上r,再删除h(两个操作数)或是删除h,再加上r(两个操作数)。

对应到dp数组中,即dp数组中\color{red}{A}状态由标有\color{green}{颜色}的三个状态通过转移方程而来。

0 * r o s
* \color{green}{0} \color{green}{1} 2 3
h \color{green}{1} \color{red}{A}
o 2
r 3
s 4
e 5

状态转移方程为:

dp[i][j] = min\left\{ \begin{array} \\ dp[i][j-1]+1, add\ character; \\ dp[i-1][j]+1, delete\ character; \\ dp[i-1][j-1] + word1[i]==word2[j], \\ change\ if\ it's\ needed. \\ \end{array}\right.

由上面的讲解。可知,\color{red}{A}的最小操作数是一,即由编辑操作中的修改操作而来的。也就是说,从状态转移方程的第三个式子而来。

由状态转移方程得到最终dp数组如下所示:

0 \color{red}{*} r o s
\color{green}{*} 0 1 2 3
h 1 1 2 3
o 2 2 1 2
r 3 2 2 2
s 4 3 3 2
e 5 4 4 3

实现代码

class Solution {
public:
    int minDistance(string word1, string word2) {
        if(word1.empty() || word2.empty()) {
            return max(word1.size(), word2.size());
        }
        
        // 建立dp数组。
        int dp[word1.size()+1][word2.size()+1];
        
        // 初始化dp数组,为删除1到word1.size()个字符。
        for(int i=0; i <= word1.size(); ++i) {
            dp[i][0] = i;
        }
        
        // 初始化dp数组,为增加1到word2.size()个字符。
        for(int i=0; i <= word2.size(); ++i) {
            dp[0][i] = i;
        }
        
        // 进行dp操作,由上述的状态转移方程迁移而来。
        for(int i=1; i <= word1.size(); ++i) {
            for(int j=1; j <= word2.size(); ++j) {
                dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1;
                dp[i][j] = min(dp[i][j], dp[i-1][j-1] + (word1[i-1] == word2[j-1] ? 0 : 1));
            }
        }
        
        // 返回dp的最终结果。
        return dp[word1.size()][word2.size()];
    }
};

评测结果:

  • 执行用时 : 20 ms, 在Edit Distance的C++提交中击败了18.77% 的用户

  • 内存消耗 : 9.4 MB, 在Edit Distance的C++提交中击败了4.60% 的用户

代码优化

从dp的角度来说,时间复杂度基本是O(N^2), 没啥优化空间。

但是,由dp数组的使用上可以观察到,每次dp的时候,只是使用当前行与上一行而已。因此,dp数组只需要开dp[2][word2.size()]大小,复用该数组即可。即空间复杂度从O(N^2)降为O(N).

优化后的代码:

class Solution {
public:
    int minDistance(string word1, string word2) {
        if(word1.empty() || word2.empty()) {
            return max(word1.size(), word2.size());
        }
        
        // 建立dp数组。
        int dp[2][word2.size()+1];
                
        // 初始化dp数组,为增加1到word2.size()个字符。
        for(int i=0; i <= word2.size(); ++i) {
            dp[0][i] = i;
        }
        
        // 进行dp操作,由上述的状态转移方程迁移而来。
        // 增加两个变量,用来标记dp数组的操作方式。
        int last_index = 0;
        int current_index = 1;
        for(int i=1; i <= word1.size(); ++i) {
            // 初始化dp数组,表示删除word字符的操作数
            dp[current_index][0] = i;
            
            // 通过状态方程进行跃迁
            for(int j=1; j <= word2.size(); ++j) {
                dp[current_index][j] = min(dp[last_index][j], dp[current_index][j-1]) + 1;
                dp[current_index][j] = min(dp[current_index][j], dp[last_index][j-1] + (word1[i-1] == word2[j-1] ? 0 : 1));
            }
            
            // 交换这两个变量,复用数组
            swap(last_index, current_index);
        }
        
        // 返回dp的最终结果。
        return dp[last_index][word2.size()];
    }
};

评测结果:

  • 执行用时 : 36 ms, 在Edit Distance的C++提交中击败了10.46% 的用户
  • 内存消耗 : 8.4 MB, 在Edit Distance的C++提交中击败了4.60% 的用户

此次优化,由于是复用dp数组,所以是由原来的O(N^2)变成O(N)。但是从评测结果来说,可知内存的优化上变化不是很明显,说明评测数据量并不大。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容