每天一道LeetCode-----使用最少的操作将一个字符串转换成另一个字符串,只有插入,删除,替换三种操作
来源:互联网 发布:加工中心编程实例100例 编辑:程序博客网 时间:2024/06/15 15:27
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]; /* 否则,取三种操作的最小值 */ else dp[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-----使用最少的操作将一个字符串转换成另一个字符串,只有插入,删除,替换三种操作
- 字符串操作——插入、替换、删除
- 每天一道LeetCode-----找到一个字符串在另一个字符串出现的位置,字符串内部顺序无要求
- 将一个字符串中的字符替换成另一个字符串
- 编辑距离:动态规划【用最少的字符操作将字符串A 转换为字符串B】
- 将一个字符串插入到另一个字符串的某个位置
- 将一个字符串插入到另一个字符串的某个位置
- C++ 将一个文件里面的相同的一个字符串替换成另一个字符串
- 每天一道LeetCode-----将字符串的连续相同的字符合并成一个字符后加个数
- C++中对字符串进行插入、替换、删除操作
- 每天一道LeetCode-----将字符串拆分成有效的ip地址
- c++字符串的插入与删除操作
- 字符串的操作:取字符, 替换字符,字符拼接,转换,插入字符等
- 【字符串操作】06.将一个字符串的指定位数插入空格
- 每天一道LeetCode-----字符串乘法
- 第十五周 OJ 将一个字符串插入另一个字符串的指定位置
- python--字符串操作(删除,替换)
- python--字符串操作(删除,替换)
- 信号量:整型、记录型信号量以及利用信号量实现进程互斥和前驱关系
- A Survey on Automatic Text Summarization
- git常用命令
- 欢迎使用CSDN-markdown编辑器
- 编写一个Book类
- 每天一道LeetCode-----使用最少的操作将一个字符串转换成另一个字符串,只有插入,删除,替换三种操作
- Hash算法总结
- coursera机器学习课程第八周——课程笔记(一)
- SpringMVC整合fastdfs-client-java实现web文件上传下载
- ansible 条件判断+循环
- kafka0.8--0.11各个版本特性预览介绍
- 简单成绩判断
- 【微信】微信开发(3)不要掉进Cookie的坑!
- 15款Cocos2d-x游戏源码