最少字符运算数
题目描述
给定两个字符串A和B以及下列三种字符运算:
(1)删除一个字符;
(2)插入一个字符;
(3)将一个字符改写为另一个字符。
问题是需要求出将字符串A转换为字符串B,所需要的最少的字符运算数。请设计一个有效算法,对任给定的2个字符串A和B,输出将字符串A转换为字符串B所需的最少字符运算数。
(来源:张德富,《算法设计与分析》 P93-P94:6.19)
题目分析
本题与求两个字符串的最长公共子序列类似,具体参考力扣1143。通过分析可知该问题存在重叠子问题,因此我们考虑动态规划算法。
以下提供两种思路:
(1)根据字符运算规则直接求递推式,通过循环嵌套直接得出结果。
(2)将字符串A转换为字符串B,只需要将A中与B不同的部分改为与B相同即可。
方法(一):直接求递推式
算法分析
(1)当A或B为空串时,dp[i,j]等于i或者j。
(2)i>0并且j>0时,对于dp[i,j],考虑四种操作情况:
①A子串删除A[j],有A[j-1]=B[i]:dp[i,j]=dp[i,j-1]+1;
②A子串插入A[j],有A[j]=B[i]:dp[i,j]=dp[i-1,j]+1;
③A子串修改A[j],使A[j]=B[i]:dp[i,j]=dp[i-1,j-1]+c,c=1;
④A[j]等于B[j],无需操作:dp[i,j]=dp[i-1,j-1]+c,c=0;
于是我们就得到了递推式:
**dp[i, j] = min{dp[i, j-1]+1, dp[i-1, j]+1, dp[i-1, j-1]+c}**
其中c视情况而定。以下是该算法代码:
解决方案
#include<iostream>
#include<vector>
using namespace std;
int main()
{
string a, b;
cin >> a >> b;
int lena = a.size(), lenb = b.size();
vector< vector<int> >dp(lenb + 1, vector<int>(lena + 1, 0));
for (int i = 0; i <= lenb; i++)
dp[i][0] = i;
for (int j = 0; j <= lena; j++)
dp[0][j] = j;
for (int i = 1; i <= lenb; i++)
{
for (int j = 1; j <= lena; j++)
{
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1); //第(1)(2)种情况
int t;
if (b[i] == a[j])
t = 0;
else
t = 1;
dp[i][j] = min(dp[i][j], dp[i - 1][j - 1] + t); //第(3)(4)种情况
}
}
cout << "The minimum number of operations is " << dp[lenb][lena];
return 0;
}
输入样例:
abbbcdddefshsdf
azzcdffefssdfsd
输出样例:
The minimum number of operations is 8
方法(二):改异为同
算法分析
先求出最大相同的部分,剩下的就是不同的部分,前者即为A与B的最长公共子序列(LCS)。用动态规划算法求出LCS并记录LCS中每一个字母在A和B中的位置,然后找到不同的部分。假设LCS共有n个字母,则A和B去掉LCS后都剩下n+1个部分(不含去掉的n个字母,有些部分可以是空串,相当于一条绳子切了n刀)。此时A和B中对应的部分不含公共子序列,假设两个部分的长度分别为N和M,则最少的运算次数肯定是max{N,M}。对每部分计算并求和即可求得最少运算次数。
程序中还添加了输出部分,用于显示A和B中的2n+1个部分,这样方便读者了解运算的过程。代码如下:
解决方案
#include <iostream>
#include <vector>
using namespace std;
struct LCS
{
int length; //公共子串长度
vector<int>cha, chb; //公共子串的字母在两个字符串的位置
};
int main()
{
string a, b;
cin >> a >> b;
int lena = a.size(), lenb = b.size();
vector< vector<LCS> >dp(lenb + 1, vector<LCS>(lena + 1));
for (int i = 1; i <= lenb; i++)
{
for (int j = 1; j <= lena; j++)
{
if (b[i - 1] == a[j - 1])
{
dp[i][j].length = dp[i - 1][j - 1].length + 1;
dp[i][j].chb = dp[i - 1][j - 1].chb;
dp[i][j].chb.push_back(i - 1);
dp[i][j].cha = dp[i - 1][j - 1].cha;
dp[i][j].cha.push_back(j - 1);
}
else if (dp[i - 1][j].length > dp[i][j - 1].length)
{
dp[i][j].length = dp[i - 1][j].length;
dp[i][j].chb = dp[i - 1][j].chb;
dp[i][j].cha = dp[i - 1][j].cha;
}
else
{
dp[i][j].length = dp[i][j - 1].length;
dp[i][j].chb = dp[i][j - 1].chb;
dp[i][j].cha = dp[i][j - 1].cha;
}
}
}
LCS result = dp[lenb][lena];
cout << "The length of LCS is " << result.length << endl;
int len = result.cha.size();
int transfer_time = 0; //记录字符运算次数
if ((result.cha[0]) || (result.chb[0]))
{
transfer_time += max(result.cha[0], result.chb[0]);
cout << a.substr(0, result.cha[0]) << " --> " << b.substr(0, result.chb[0]) << endl;
}
for (int i = 0; i < len - 1; i++)
{
cout << a[result.cha[i]] << " = " << b[result.chb[i]] << endl;
if ((result.cha[i + 1] - result.cha[i] - 1) || (result.chb[i + 1] - result.chb[i] - 1))
{
transfer_time += max(result.cha[i + 1] - result.cha[i] - 1, result.chb[i + 1] - result.chb[i] - 1);
cout << a.substr(result.cha[i] + 1, result.cha[i + 1] - result.cha[i] - 1) << " --> " << b.substr(result.chb[i] + 1, result.chb[i + 1] - result.chb[i] - 1) << endl;
}
}
cout << a[result.cha[len - 1]] << " = " << b[result.chb[len - 1]] << endl;
if ((lena - result.cha[len - 1] - 1) || (lenb - result.chb[len - 1] - 1))
{
transfer_time += max(lena - result.cha[len - 1] - 1, lenb - result.chb[len - 1] - 1);
cout << a.substr(result.cha[len - 1], lena - result.cha[len - 1] - 1) << " --> " << b.substr(result.chb[len - 1] + 1, lenb - result.chb[len - 1] - 1) << endl;
}
cout << "The minimum number of operations is " << transfer_time << endl;
return 0;
}
输入样例:
abbbcdddefshsdf
azzcdffefssdfsd
输出样例:
The length of LCS is 9
a = a
bbb --> zz
c = c
d = d
dd --> ff
e = e
f = f
s = s
h -->
s = s
d = d
f = f
--> sd
The minimum number of operations is 8