书籍简介
第一章 字符串
1.0 本章导读
1.1 旋转字符串
1.2 字符串包含
1.3 字符串转换成整数
1.4 回文判断
1.5 最长回文子串
1.6 字符串的全排列
1.10 本章习题
第二章 数组
2.0 本章导读
2.1 寻找最小的 k 个数
2.2 寻找和为定值的两个数
2.3 寻找和为定值的多个数
2.4 最大连续子数组和
2.5 跳台阶
2.6 奇偶排序
2.7 荷兰国旗
2.8 矩阵相乘
2.9 完美洗牌
2.10 K个最小和 (UVA 11997 K Smallest Sums)
2.15 本章习题
第三章 树
3.0 本章导读
3.1 红黑树
3.2 B树
3.3 最近公共祖先LCA
3.5 R树:处理空间存储问题
3.10 本章习题
第四章 查找匹配
4.1 有序数组的查找
4.2 行列递增矩阵的查找
4.3 出现次数超过一半的数字
第五章 动态规划
5.0 本章导读
5.1 最大连续乘积子串
5.2 字符串编辑距离
5.3 格子取数
5.4 交替字符串
5.6 最长递增子序列
5.10 本章习题
第六章 海量数据处理
6.0 本章导读
6.1 关联式容器
6.2 分而治之
6.3 simhash算法
6.4 外排序
6.5 MapReduce
6.6 多层划分
6.7 Bitmap
6.8 Bloom filter
6.9 Trie树
6.10 数据库
6.11 倒排索引
6.15 本章习题
第七章 机器学习
7.1 K 近邻算法
7.2 支持向量机
附录 更多题型
附录A 语言基础
附录B 概率统计
附录C 智力逻辑
附录D 系统设计
附录E 操作系统
附录F 网络协议
5.2 字符串编辑距离 - 《程序员编程艺术:面试和算法心得》 - 光年文档管理系统(Light Year Doc)
网站首页
5.2 字符串编辑距离
## 题目描述 给定一个源串和目标串,能够对源串进行如下操作: 1. 在给定位置上插入一个字符 2. 替换任意字符 3. 删除任意字符 写一个程序,返回最小操作数,使得对源串进行这些操作后等于目标串,源串和目标串的长度都小于2000。 ### 分析与解法 此题常见的思路是动态规划,假如令dp[i][j] 表示源串S[0…i] 和目标串T[0…j] 的最短编辑距离,其边界:dp[0][j] = j,dp[i][0] = i,那么我们可以得出状态转移方程: - dp[i][j] =min{ - dp[i-1][j] + 1 , S[i]不在T[0…j]中 - dp[i-1][j-1] + 1/0 , S[i]在T[j] - dp[i][j-1] + 1 , S[i]在T[0…j-1]中 } 接下来,咱们重点解释下上述3个式子的含义 - 关于dp[i-1][j] + 1, s.t. s[i]不在T[0…j]中的说明 - s[i]没有落在T[0…j]中,即s[i]在中间的某一次编辑操作被删除了。因为删除操作没有前后相关性,不妨将其在第1次操作中删除。除首次操作时删除外,后续编辑操作是将长度为i-1的字符串,编辑成长度为j的字符串:即dp[i-1][j]。 - 因此:dp[i][j] = dp[i-1][j] + 1。 - 关于dp[i-1][j-1] + 0/1, s.t. s[i] 在T[j]的说明 - 若s[i]经过编辑,最终落在T[j]的位置。 - 则要么s[i] == t[j],s[i]直接落在T[j]。这种情况,编辑操作实际上是将长度为i-1的S’串,编辑成长度为j-1的T’串:即dp[i-1][j-1]; - 要么s[i] ≠ t[j],s[i] 落在T[j]后,要将s[i]修改成T[j],即在上一种情况的基础上,增加一次修改操作:即dp[i-1][j-1] + 1。 - 关于dp[i][j-1] + 1, s.t. s[i]在T[0…j-1]中的说明 - 若s[i]落在了T[1…j-1]的某个位置,不妨认为是k,因为最小编辑步数的定义,那么,在k+1到j-1的字符,必然是通过插入新字符完成的。因为共插入了(j-k)个字符,故编辑次数为(j-k)次。而字符串S[1…i]经过编辑,得到了T[1…k],编辑次数为dp[i][k]。故: dp[i][j] = dp[i][k] + (j-k)。 - 由于最后的(j-k)次是插入操作,可以讲(j-k)逐次规约到dp[i][k]中。即:dp[i][k]+(j-k)=dp[i][k+1] + (j-k-1) 规约到插入操作为1次,得到 dp[i][k]+(j-k) =dp[i][k+1] + (j-k-1) =dp[i][k+2] + (j-k-2)=… =dp[i][k+(j-k-1)] + (j-k)-(j-k-1) =dp[i][j-1] + 1。 上述的解释清晰规范,但为啥这样做呢? 换一个角度,其实就是字符串对齐的思路。例如把字符串“ALGORITHM”,变成“ALTRUISTIC”,那么把相关字符各自对齐后,如下图所示:  把图中上面的源串S[0…i] = “ALGORITHM”编辑成下面的目标串T[0…j] = “ALTRUISTIC”,我们枚举字符串S和T最后一个字符s[i]、t[j]对应四种情况:(字符-空白)(空白-字符)(字符-字符)(空白-空白)。 由于其中的(空白-空白)是多余的编辑操作。所以,事实上只存在以下3种情况: - 下面的目标串空白,即S + 字符X,T + 空白,S变成T,意味着源串要删字符 - dp[i - 1, j] + 1 - 上面的源串空白,S + 空白,T + 字符,S变成T,最后,在S的最后插入“字符”,意味着源串要添加字符 - dp[i, j - 1] + 1 - 上面源串中的的字符跟下面目标串中的字符不一样,即S + 字符X,T + 字符Y,S变成T,意味着源串要修改字符 - dp[i - 1, j - 1] + (s[i] == t[j] ? 0 : 1) 综上,可以写出简单的DP状态方程: ```c //dp[i,j]表示表示源串S[0…i] 和目标串T[0…j] 的最短编辑距离 dp[i, j] = min { dp[i - 1, j] + 1, dp[i, j - 1] + 1, dp[i - 1, j - 1] + (s[i] == t[j] ? 0 : 1) } //分别表示:删除1个,添加1个,替换1个(相同就不用替换)。 ``` 参考代码如下: ```c //dp[i][j]表示源串source[0-i)和目标串target[0-j)的编辑距离 int EditDistance(char *pSource, char *pTarget) { int srcLength = strlen(pSource); int targetLength = strlen(pTarget); int i, j; //边界dp[i][0] = i,dp[0][j] = j for (i = 1; i <= srcLength; ++i) { dp[i][0] = i; } for (j = 1; j <= targetLength; ++j) { dp[0][j] = j; } for (i = 1; i <= srcLength; ++i) { for (j = 1; j <= targetLength; ++j) { if (pSource[i - 1] == pTarget[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1]); } } } return dp[srcLength][targetLength]; } ``` ## 举一反三 1、传统的编辑距离里面有三种操作,即增、删、改,我们现在要讨论的编辑距离只允许两种操作,即增加一个字符、删除一个字符。我们求两个字符串的这种编辑距离,即把一个字符串变成另外一个字符串的最少操作次数。假定每个字符串长度不超过1000,只有大写英文字母组成。 2、有一亿个数,输入一个数,找出与它编辑距离在3以内的数,比如输入6(0110),找出0010等数,数是32位的。 ## 问题扩展 实际上,关于这个“编辑距离”问题在搜索引擎中有着重要的作用,如搜索引擎关键字查询中拼写错误的提示,如下图所示,当你输入“[Jult](https://www.google.com.hk/search?hl=zh-CN&newwindow=1&safe=strict&site=&source=hp&q=Jult&btnK=Google+%E6%90%9C%E7%B4%A2)”后,因为没有这个单词“Jult”,所以搜索引擎猜测你可能是输入错误,进而会提示你是不是找“July”:  当然,面试官还可以继续问下去,如请问,如何设计一个比较这篇文章和上一篇文章相似性的算法?
上一篇:
5.1 最大连续乘积子串
下一篇:
5.3 格子取数