Edit Distance

原题链接Edit Distance

题目要求,输入两个字符串word1和word2,计算可以将word1转换成word2的最小的操作次数,可以执行的操作如下,每个操作算作1次

  • 将word1的某个字符删除
  • 在word1当前位置插入一个字符
  • 将word1当前位置的字符替换成另一个字符

上面的三个操作每操作一次总操作次数都需要加一,计算最小的操作次数。


这是典型的动态规划问题。

假设word1的长度为m, word2的长度为n,则可以将word1和word2分别表示成

  • word1[0, 1, …, m - 1]
  • word2[0, 1, …, n - 1]

那么word1[0, 1, ..., i - 1]就表示word1的前i个字符,word2[0, 1, ..., j - 1]就表示word2的前j个字符

定义dp[i][j]表示将word1[0, 1, ..., i - 1]转换到word2[0, 1, ..., j - 1]所需要的最小操作次数。

首先明确一点,动态规划是先求子问题的解,再求实际问题的解,对于本题而言。子问题是对于任意的i和j,计算将word1[0, 1, …, i - 1]转换成word2[0, 1, …, j - 1]的最小次数,也就是计算所有dp[i][j]的值

当所有子问题的解都计算完毕后,就可以求解最终的实际问题,这里隐含了一个信息,那就是当要求解最终问题时,所有子问题的解是已知的。

假设现在要将word1[0, 1, ..., i - 1]转换成word2[0, 1, ..., j - 1],同时假设已经直到了将word1[0, 1, ..., i - 2]转换成word2[0, 1, ..., j - 2]的最小操作此时,即dp[i - 1][j - 1]
根据上面的叙述,此时的实际问题是计算dp[i][j],那么所有子问题的解是已知的,即dp[i - 1][j - 1],dp[i][j - 1]以及dp[i - 1][j]的值是已经知道的(这里只需要使用这三个子问题的解)

那么

  • 如果word1[i - 1] == word2[j - 1],那么对于将字符word1[i - 1]转换到word2[j - 1]是不需要任何操作的

    • word1[0, 1, ..., i - 2]转换到word2[0, 1, ..., j - 2],即dp[i][j] = dp[i - 1][j - 1]
  • 如果word1[i - 1] != word2[j - 1],此时有三种方式可以将word1[0, 1, ..., i - 1]转换到word2[0, 1, ..., j - 1]
    • 将word1[i - 1]替换成word2[j - 1],此时dp[i][j] = 1 + dp[i - 1][j - 1]
    • 将word1[i - 1]删掉,此时dp[i][j] = 1 + dp[i - 1][j]
    • 在word1的i - 1位置插入字符word2[ j - 1],此时dp[i][j] = 1 + dp[i][j - 1]

对于替换操作,因为已经直到将word1[0, 1, ..., i - 2]转换到word2[0, 1, ..., j - 2]的最小操作次数即dp[i - 1][j - 1],那么就可以将word1[i - 1]替换成word2[j - 1]。也就是说,可以先将word1[0 , 1, ..., i - 2]转换到word2[0, 1, ..., j - 2],然后将word1[i - 1]替换成word2[j - 1],以达到将word1[0, 1, ..., i - 1]转换到word2[0, 1, ..., j - 1]的目的


对于删除操作,因为已经知道将word1[0, 1, ..., i - 2]转换到word2[0, 1, ..., j - 1]的最小操作数即dp[i - 1][j],那么就可以将word1[i - 1]删掉,也就是说,可以先将word1[0, 1, ..., i - 2]转换到word2[0, 1, ..., j - 1],然后将word1[i - 1]删掉,以达到将word1[0, 1, ..., i - 1]转换到word2[0, 1, ..., j - 1]的目的


对于插入操作,因为已经直到将word1[0, 1, ..., i - 1]转换到word2[0, 1, ..., j - 2]的最小操作数即dp[i][j - 1],那么就可以将word1[0, 1, ..., i - 1]的后面添加字符word2[j - 1],即word1[0, 1, ..., i - 1] + word2[j - 1] == word2[0, 1, ..., j - 1]。也就是说,可以先将word1[0, 1, ..., i - 1]转换到word2[0, 1, ..., j - 2],然后在word1[0, 1, ..., i - 1]的后面添加字符word2[j - 1],以达到将word1[0, 1, ..., i - 1]转换到word2[0, 1, ..., j - 1]的目的



对于动态规划而言,有递归和迭代两种方法,递归的程序易于理解,但是递归的过程造成的栈开销会影响性能,而迭代恰好相反,不易理解,但是性能高。

递归法是从最终问题递归到一个最小的子问题上,可以理解成从上向下进行,如果使用递归法,那么对于动态规划数组的定义是需要有所改变的。原因是实际问题要求计算将word1转换到word2的最小操作,那么随着递归深度的增加最后到达word1[m - 1]word2[n - 1],子问题就变成将word1的某个字符转换成word2的某个字符的最小操作次数。
所以,这里将dp[i][j]的定义改为

  • dp[i][j]表示将word1[i, i+1, …, m)转换到word2[j, j+1, …, n)的最小操作次数。

在实际的操作过程中,仍然使用上述的三种转换方式
代码如下

class Solution {
public:int minDistance(string word1, string word2) {int n1 = word1.size();int n2 = word2.size();vector<vector<int>> dp(n1, vector<int>(n2, INT_MAX));return minDistance(word1, word2, 0, 0, dp);}private:int minDistance(string& word1, string& word2, int i, int j, vector<vector<int>>& dp){/* 如果二者都达到末尾,说明转换完毕,返回0即可 */if(i >= word1.size() && j >= word2.size())return 0;/* 如果其中一个到达末尾,那么只能通过删除/插入方式使二者相等 */else if(i >= word1.size() && j < word2.size())return word2.size() - j;else if(i < word1.size() && j >= word2.size())return word1.size() - i;if(dp[i][j] != INT_MAX)return dp[i][j];if(word1[i] == word2[j])dp[i][j] = std::min(dp[i][j], minDistance(word1, word2, i + 1, j + 1, dp));/* 将word1[i]替换成word2[j] */dp[i][j] = std::min(dp[i][j], 1 + minDistance(word1, word2, i + 1, j + 1, dp));/* 在word1当前位置插入word2[j] */dp[i][j] = std::min(dp[i][j], 1 + minDistance(word1, word2, i, j + 1, dp));/* 删除word1[i] */dp[i][j] = std::min(dp[i][j], 1 + minDistance(word1, word2, i + 1, j, dp));return dp[i][j];}
};

这种方法效率奇低,原因是每次递归都需要向三个不同的方向递归,即使使用了动态规划数组减少递归次数,但是开销仍然很大。不过这种方法倒是最容易想到的

迭代法就是模拟递归的返回过程,即从最深层的位置向上返回,这就避免后向下递归的过程,也没有所谓的递归栈开销

迭代法的dp定义就和上面相同了,即

  • dp[i][j]表示将word1[0, 1, …, i - 1]转换到word2[0, 1, …, j - 1]的最小操作次数

代码如下

class Solution {
public:int minDistance(string word1, string word2) {int n1 = word1.size();int n2 = word2.size();vector<vector<int>> dp(n1 + 1, vector<int>(n2 + 1, 0));/* 如果word2为空,那么转换方式就是将word1每个字符删掉,次数就是word1的个数 */for(int i = 0; i <= n1; ++i)dp[i][0] = i;/* 如果word1为空,那么转换方式就是将依次插入word2对应字符,次数就是word2的个数 */for(int j = 0; j <= n2; ++j)dp[0][j] = j;for(int i = 1; i <= n1; ++i){for(int j = 1; j <= n2; ++j){/* 如果对应字符相等,那么只需要将word1[0, 1, ... i - 2]转换成word2[0, 1, ..., j - 2] */if(word1[i - 1] == word2[j - 1])dp[i][j] = dp[i - 1][j - 1];/* 否则,取三种操作的最小值 */elsedp[i][j] = 1 + std::min(dp[i - 1][j - 1], std::min(dp[i][j - 1], dp[i - 1][j]));}}return dp[n1][n2];}
};

迭代法的效率比较高(至少从代码长度上看也是),不过不太容易理解。
将dp设置为(n1 + 1) * (n2 + 1)的原因是根据dp[i][j]的定义,原因dp[i][j]中的i和j分别表示word1word2的长度,当其中一个是0时,对相当于对应字符串是空字符

  • dp[i][0]表示将word1[0, 1, …, i - 1]转换成空字符,此时操作次数就应该是word1的长度
  • dp[0][j]表示将空字符转换成word2[0, 1, …, j - 1],此时操作次数就应该是word2的长度
  • dp[0][0]表示将空字符转换成空字符,此时操作次数就应该是0

当然,凡是动态规划的迭代法都有方法将dp数组降一个维度,这里dp数组是一个二维数组,那么就有办法用一个一维数组解决问题

对比上面的迭代法,每次循环需要的数值有

  • dp[i - 1][j - 1],表示将word1[0, 1, …, i - 2]转换成word2[0, 1, …, j - 2]的最小操作次数
  • dp[i][j - 1],表示将word1[0, 1, …, i - 1]转换成word2[0, 1, …, j - 2]的最小操作次数
  • dp[i - 1][j],表示将word1[0, 1, …, i - 2]转换成word2[0, 1, …, j - 1]的最小操作次数

那么,可不可以只用一维数组就表示上面三个数值呢。
因为是将word1转换成word2,那么假设dp数组的定义为vector<int> dp(word1.size() + 1, 0);

定义dp[i]表示将word1[0, 1, ..., i - 1]转换到word2的最小操作次数,word2的长度是任意的,换句话说,对于任意的j,dp[i]都表示将word1[0, 1, ..., i - 1]转换到word2[0, 1, ..., j - 1]的最小操作次数。

那么,肯定需要从最小的j开始计算,那么可以先将j放在最外层的for循环中,大体的框架如下

class Solution {
public:int minDistance(string word1, string word2) {int n1 = word1.size();int n2 = word2.size();vector<int> dp(n1 + 1, 0);for(int i = 0; i <= n1; ++i)dp[i] = i;for(int j = 1; j <= n2; ++j)    //外层{for(int i = 1; i <= n1; ++i)    //内层{}}return dp[n1];}
};

想一下,应该如何表示dp[i - 1][j - 1]dp[i][j - 1]以及dp[i - 1][j]

假设当j = 1时执行一次内层循环,此时dp1, dp2, …., dp[n1] 
j= 2时再次执行内层循环时,正要计算还没有计算dp[i]时,dp[i]的值是当j = 1时的值
也就是说此时的dp[i]等价于dp[i][j - 1]

假设当j = 1时执行一次内层循环,在内层循环中计算了dp[1], dp[2], ..., dp[i - 1]
正要计算dp[i]时,dp[i - 1]的值等价于dp[i - 1][j]

假设当j = 1时执行了一次内层循环,此时dp1, dp2, …, dp[n1]
j = 2时再次执行内层循环,在内层循环中计算了dp[1], dp[2], ..., dp[i],计算dp[i]时记录与dp[i][j - 1]等价的dp[i],也就是还没有更新dp[i]时的值,记录在遍历prev中。
此时prev表示将word1[0, 1, ..., i - 1]转换成word2[0, 1, ..., j - 2]的值,即dp[i][j - 1]
当计算dp[i + 1]时,prev中的i就变成了i-1,所以表示成dp[i - 1][j - 1],说明prev正是与dp[i - 1][j - 1]等价的值
(注,如果dp[i + 1 - 1][j - 1]不容易理解,可以将i + 1看成n,此时要计算将word1[0, 1, ..., n - 1]转换成word2[0, 1, ..., j - 1]的最小操作次数,即dp[n - 1][j - 1]

以上只是推导了一下二维数组中的dp[i - 1][j - 1]dp[i][j - 1]dp[i - 1][j]等价的一维数组对应的值

代码如下

class Solution {
public:int minDistance(string word1, string word2) {int n1 = word1.size();int n2 = word2.size();vector<int> dp(n1 + 1, 0);for(int i = 0; i <= n1; ++i)dp[i] = i;for(int j = 1; j <= n2; ++j){/* dp[0]等价与dp[0][j - 1] */int prev = dp[0];dp[0] = j;for(int i = 1; i <= n1; ++i){/* 此时的dp[i]等价于dp[i][j - 1],将其赋值给prev* 在下次循环时,prev就代表dp[i - 1][j - 1]* 因为i增加了,此时的i就变为i-1了  */int temp = dp[i];if(word1[i - 1] == word2[j - 1])dp[i] = prev;else/* 此时的dp[i]等价于dp[i][j - 1],而dp[i - 1]等价于dp[i - 1][j] *//* prev等价于dp[i - 1][j - 1],因为prev中的i是此时的i - 1 */dp[i] = 1 + std::min(prev, std::min(dp[i], dp[i - 1]));prev = temp;}}return dp[n1];}
};


上述分别用递归,迭代法实现动态规划,可以发现,递归的动态规划性能不如迭代法。
另外,迭代法可以将动态规划数组降维,从而进一步减少了空间复杂度

每天一道LeetCode-----使用最少的操作将一个字符串转换成另一个字符串,只有插入,删除,替换三种操作相关推荐

  1. R语言使用magick包的image_animate函数和image_morph函数创建一个由n个图像组成的序列,逐渐将一个图像转换成另一个图像(sequence of image morph by)

    R语言使用magick包的image_animate函数和image_morph函数创建一个由n个图像组成的序列,逐渐将一个图像转换成另一个图像(Creates a sequence of n ima ...

  2. char N2Char(int n)函数:将一个整数转换为字符串,并放入一个字符串中

    //将一个整数转换为字符串,并放入一个字符串中 char N2Char(int n)//一次只能转换一个数 {int i;char c;if ((i = n / 10) != 0)N2Char(i); ...

  3. php 将一个字符串转换成数组,PHP将一个字符串转换成数组

    PHP将一个字符串转换成数组,支持中文/** * 将一个字符串转换成数组,支持中文 * @param string $string 待转换成数组的字符串 * @return string 转换后的数组 ...

  4. java中实现将一个数字字符串转换成逗号分隔的数字串, 即从右边开始每三个数字用逗号分隔

    源代码如下: /*将一个数字字符串转换成逗号分隔的数字串,即从右边开始每三个数字用逗号分隔 */public static void testFenGeNumber(){String number = ...

  5. leetcode —— 面试题67. 把字符串转换成整数

    写一个函数 StrToInt,实现把字符串转换成整数这个功能.不能使用 atoi 或者其他类似的库函数. 首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止. 当我们寻找到 ...

  6. 使用Jackson将一个对象转换成一个JSON字符串

    由于ajax的流行,在程序中使用了越来越多的json来进行数据的传输,而Jackson可以将一个普通的java对象转换成一个json的字符串,帮助程序员进行前后端数据的传输. 首先需要导入程序依赖的j ...

  7. 【LeetCode】剑指 Offer 67. 把字符串转换成整数

    [LeetCode]剑指 Offer 67. 把字符串转换成整数 文章目录 [LeetCode]剑指 Offer 67. 把字符串转换成整数 package offer;public class So ...

  8. http_build_query()就是将一个数组转换成url 问号?后面的参数字符串,并且会自动进行urlencode处理,及它的逆向函数...

    http_build_query()就是将一个数组转换成url 问号?后面的参数字符串,并且会自动进行urlencode处理 例如: $data = array('foo'=>'bar', 'b ...

  9. 字符串转换成对象的操作

    前言: 在实际项目中,原始拿到的数据不一定是我们想要的类型,我们就需要对它进行处理,今天带来的是字符串转换成对象的操作案例. 问题描述 将字符串 postId=79&id=220027964 ...

最新文章

  1. airtestide 下载后打不开_微信收到CAD图纸打不开怎么办?2种方法教你手机CAD快速看图...
  2. 再谈序列化推荐-集成item类目属性
  3. 博主的办公室和他的工作台
  4. QQuickWidget + QML编程实现酷炫动态动画效果
  5. curl http_code 状态码 意义及信息
  6. Java-虚拟机-垃圾收集器/垃圾收集算法/GCROOT根
  7. 放生大海的鱼,为什么要在鱼肚子上捅一个洞?
  8. AcWing 897. 最长公共子序列(LCS朴素版)
  9. html乱码原因与网页乱码解决方法
  10. 批处理for循环命令详解
  11. 3.3 腾讯云AI案例
  12. java 调用 swf 文件上传,swfupload 文件 上传
  13. Echarts 配置渐变
  14. ckplayer php,ckplayer播放器
  15. ThoughtWorks培训感想
  16. 丢失数据文件和控制文件的恢复案例(zt)
  17. 阳光点歌系统服务器说明书,天行阳光机顶盒点歌系统安装及配置说明
  18. 3.1 人生规划的秘密:一个人活成一支队伍
  19. RTX30系列-Ubuntu系统配置与深度学习环境Pytorch配置
  20. CSDN页面打印不正常的解决方法

热门文章

  1. react 动态路 嵌套动子路由_2020年,我是如何从一名Vueer转岗到React阵营
  2. java 数字的位数_Java判断数字位数的方法总结
  3. Codeforces Round #401 (Div. 1) C(set+树状数组)
  4. kubernetes1.5即将发布
  5. webservice 缓存机制
  6. Visual C++ 的代码折叠
  7. Windows XP文件夹右键属性没有“安全”选项卡的解决
  8. 美国纽约的一个摄像头!刷新即现奇迹!
  9. [渗透攻防] 四.详解MySQL数据库攻防及Fiddler神器分析数据包
  10. C# 系统应用之通过注册表获取USB使用记录(一)