前言:编辑距离此题为LeetCode的第72号题,本质上可以采用动态规划DP的方法求解,但是在求解的时候理解出现了问题,导致懵逼了一段时间,还好终于想通了,现将自己的理解思路、程序实现以及空间复杂度优化整理如下,希望可以帮到大家。问题描述:
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
---插入一个字符
---删除一个字符
---替换一个字符
注意:首先我们需要明确一点,那就是我们将word1转换成word2,因此我们必须对word1进行上述3种操作,而不是对word2,因此我之前看到不少文章对于此点没有说明白,对于准确理解此题造成了一点阻碍。
问题分析DP问题的求解一般遵循3个步骤:定义数组元素的含义、找出初始值和找出数组元素之间的关系定义数组元素的含义
这一步我们定义起来比较简单,dp[i][j] 代表着word1的前i个字符转换成word2的前j个字符所需要的最少操作步数,虽然将它定义出来比较简单,但是我们必须要理解对数组的含义,我之前就是因为理解有偏差导致迟迟对此问题理解不了,需要注意的是:定义中的i和j代表的是前i个字符和前j个字符,也就是说我只要将word1的前i个字符转换成word2的前j个字符就OK了,后面的字符我不管,至于怎么转换我也不管,只要转换过去就行,理解这一点很重要。找出数组元素之间的关系
DP规划问题的思路就是通过前面已有的结果去推出当前的结果,而不用从头开始去推,对于此问题而言,dp[i][j]代表的是 word1的前i个字符转换成word2的前j个字符所需要的最少操作步数,那我们就需要用到前面已经转换的不同长度的字符串的最少转换步数。由于插入、替换、删除3种操作每次都是影响word1的一个字符,所以求dp[i][j]顶多会用到dp[i-1][j]、dp[i][j-1]和dp[i-1][j-1][j]的值,也就是说在二维网格图中距离dp[i][j]距离为1并且在dp[i][j]左方和上方的数据。
首先,我们需要分情况讨论:
情况1:word1的第i个字符和word2的第j个字符相同
既然相同的话,也就是说我们不用对word1的第i个字符进行任何操作就能够变成word2的第j个字符(因为两者相等),所以我们将前i个字符转换为前j个字符的步数和前i-1转换成前j-1的步数相等,那么我们可以得到关系式: dp[i][j] = dp[i-1][j-1]
情况2:word1的第i个字符和word2的第j个字符不同
既然不同,那么我们就需要在前面不同长度字符转换的基础上对word1进行进一步的操作,注意我们是对word1进行的操作,切记!那么我们进行什么样的操作呢?
操作1:删除
我们可以想一下,什么情况下仅仅需要对word1删除一个元素就可以完成前i个字符转换成word2的前j个字符呢?删除一个字符,意味着word1前i-1个字符已经转换为word2的前j个字符了,我现在要再转换word1的第i个字符,那么多做一步操作直接将其删掉就可以了,那么我们可以得到关系式: dp[i][j] = dp[i-1][j] + 1
操作2:插入
对word1插入一个字符才能完成转换,插入的字符必然和word2的第j个字符相等,也就是说我们之前已经将word1的前i个字符转换为word2的前j-1个字符了,那么如何才能完成转换成word2的前j个字符呢?直接对word1进行近一步操作,再加一个与word2的第j个字符相等的字符就可以了,那么我们就可以到得到关系式: dp[i][j] = dp[i][j-1] + 1
操作3:替换
需要替换是需要将word1的第i个字符替换成word2的第j个字符,也就是说之前我们已经将word1的前i-1个字符转换成word2的前j-1个字符,只需要将word1的第i个字符替换成word2的第j个字符,那么我们可以得到关系式: dp[i][j] = dp[i-1][j-1] + 1
至此我们讨论了3种可以利用之前的结果得到dp[i][j]的方式,并且利用的是前面已经解决的不同的结果,我们需要的是最少的步数,因此总的表达式为:
dp[i][j] = min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]) + 1找出初始值
通过前面的数组之间的关系式,我们可以知道表达式左侧i和j的值最小从1开始,不能为0,因为如果为0的话,表达式右侧数组坐标就会出现负值了。因此我们需要得到dp[0][j],和dp[i][0]
对于dp[0][j]来说,意味着将长度为0的字符串转换为长度为j的字符串,因此执行的操作就是不断的插入值,总共需要j步
因此 dp[0][j] = j
对于dp[i][0]来说,意味着将长度为i的字符串转换为长度为0的字符串,因此执行的操作就是不断的删除值,总共需要i步
因此 dp[i][0] = i
程序实现
//c++编程实现#include#include //字符串标准函数库#include //使用min函数int convert_word1(const string s1, const string s2)
{
int m = s1.length();
int n = s2.length();
//申请动态内存数组 int** dp = new int*[m + 1];
for (int i = 0; i < m + 1; i++) {
dp[i] = new int[n + 1];
}
//赋予初值 for (int i = 0; i < m + 1; i++) {
dp[i][0] = i;
}
for (int j = 0; j < n + 1; j++) {
dp[0][j] = j;
}
//数组元素之间的关系 for (int i = 1; i < m + 1; i++) {
for (int j = 1; j < n + 1; j++) {
if (s1[i - 1] == s2[j - 1]) //------------------------------1 dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
return dp[m][n];
}
程序的编写需要注意一下几点:
若字符串长度为m,n,则在申请内存的时候需要申请[m+1,n+1]大小的内存,因为需要访问dp[m][n]
字符串取具体字符的时候,下标从0开始,因此上述程序中1位置那里为s1[i-1] == s2[j-1]
空间复杂度优化
上述的程序实现所需要申请的内存大小为 m*n,其实通过一些优化方法可以减少算法的空间复杂度,而进行优化的关键是充分利用数组元素之间的关系,这个时候最好的方法就是画图!!!
比如我们在求dp[i][j]的时候,所利用的之前的数组元素为dp[i-1][j],dp[i][j-1],dp[i-1][j-1],通过示意图我们可以更加清晰的看出他们之间的关系:
通过上图我们可以发现:在我们求第i行的时候,只用到了第i行和第i-1行,设想一下,如果我们只申请一维数组在求dp[i][j]的时候,需要用到的编号3是刚刚生成的,编号2是前一个循环生成的,而对于编号1,我们就需要一个中间变量来保存它,并且还需要一个变量来表示之前的这个值,如下图:
优化后的程序如下所示:
//c++编程实现
#include
#include //字符串标准函数库
#include //使用min函数
int convert_word_opti(const string s1, const string s2) {
int m = s1.length();
int n = s2.length();
int a, Pre; //定义中间变量值
int* dp = new int[n + 1];
for (int j = 0; j < n + 1; j++) { //数组初始化
dp[j] = j;
}
for (int i = 1; i < m + 1; i++) {
Pre = dp[0];
dp[0] = i;
for (int j = 1; j < n + 1; j++) {
a = Pre;
Pre = dp[j];
if (s1[i - 1] == s2[j - 1])
dp[j] = a;
else
dp[j] = min(min(dp[j - 1], dp[j]), a) + 1;
}
}
return dp[n];
}