每天一道LeetCode-----使用最少的操作将一个字符串转换成另一个字符串,只有插入,删除,替换三种操作
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分别表示word1
和word2
的长度,当其中一个是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-----使用最少的操作将一个字符串转换成另一个字符串,只有插入,删除,替换三种操作相关推荐
- 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 ...
- char N2Char(int n)函数:将一个整数转换为字符串,并放入一个字符串中
//将一个整数转换为字符串,并放入一个字符串中 char N2Char(int n)//一次只能转换一个数 {int i;char c;if ((i = n / 10) != 0)N2Char(i); ...
- php 将一个字符串转换成数组,PHP将一个字符串转换成数组
PHP将一个字符串转换成数组,支持中文/** * 将一个字符串转换成数组,支持中文 * @param string $string 待转换成数组的字符串 * @return string 转换后的数组 ...
- java中实现将一个数字字符串转换成逗号分隔的数字串, 即从右边开始每三个数字用逗号分隔
源代码如下: /*将一个数字字符串转换成逗号分隔的数字串,即从右边开始每三个数字用逗号分隔 */public static void testFenGeNumber(){String number = ...
- leetcode —— 面试题67. 把字符串转换成整数
写一个函数 StrToInt,实现把字符串转换成整数这个功能.不能使用 atoi 或者其他类似的库函数. 首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止. 当我们寻找到 ...
- 使用Jackson将一个对象转换成一个JSON字符串
由于ajax的流行,在程序中使用了越来越多的json来进行数据的传输,而Jackson可以将一个普通的java对象转换成一个json的字符串,帮助程序员进行前后端数据的传输. 首先需要导入程序依赖的j ...
- 【LeetCode】剑指 Offer 67. 把字符串转换成整数
[LeetCode]剑指 Offer 67. 把字符串转换成整数 文章目录 [LeetCode]剑指 Offer 67. 把字符串转换成整数 package offer;public class So ...
- http_build_query()就是将一个数组转换成url 问号?后面的参数字符串,并且会自动进行urlencode处理,及它的逆向函数...
http_build_query()就是将一个数组转换成url 问号?后面的参数字符串,并且会自动进行urlencode处理 例如: $data = array('foo'=>'bar', 'b ...
- 字符串转换成对象的操作
前言: 在实际项目中,原始拿到的数据不一定是我们想要的类型,我们就需要对它进行处理,今天带来的是字符串转换成对象的操作案例. 问题描述 将字符串 postId=79&id=220027964 ...
最新文章
- airtestide 下载后打不开_微信收到CAD图纸打不开怎么办?2种方法教你手机CAD快速看图...
- 再谈序列化推荐-集成item类目属性
- 博主的办公室和他的工作台
- QQuickWidget + QML编程实现酷炫动态动画效果
- curl http_code 状态码 意义及信息
- Java-虚拟机-垃圾收集器/垃圾收集算法/GCROOT根
- 放生大海的鱼,为什么要在鱼肚子上捅一个洞?
- AcWing 897. 最长公共子序列(LCS朴素版)
- html乱码原因与网页乱码解决方法
- 批处理for循环命令详解
- 3.3 腾讯云AI案例
- java 调用 swf 文件上传,swfupload 文件 上传
- Echarts 配置渐变
- ckplayer php,ckplayer播放器
- ThoughtWorks培训感想
- 丢失数据文件和控制文件的恢复案例(zt)
- 阳光点歌系统服务器说明书,天行阳光机顶盒点歌系统安装及配置说明
- 3.1 人生规划的秘密:一个人活成一支队伍
- RTX30系列-Ubuntu系统配置与深度学习环境Pytorch配置
- CSDN页面打印不正常的解决方法
热门文章
- react 动态路 嵌套动子路由_2020年,我是如何从一名Vueer转岗到React阵营
- java 数字的位数_Java判断数字位数的方法总结
- Codeforces Round #401 (Div. 1) C(set+树状数组)
- kubernetes1.5即将发布
- webservice 缓存机制
- Visual C++ 的代码折叠
- Windows XP文件夹右键属性没有“安全”选项卡的解决
- 美国纽约的一个摄像头!刷新即现奇迹!
- [渗透攻防] 四.详解MySQL数据库攻防及Fiddler神器分析数据包
- C# 系统应用之通过注册表获取USB使用记录(一)