一、问题描述
有两个字符串,求二者的最长公共子序列。
最长公共子序列:不必连续但必须有序的子列(与子串区分,子串是连续的)
二:解决方法
第一种方法:穷举法 ,就是一个一个的对比,但这个方法的时间复杂度为O(2^n),故而不多做赘述。
第二种方法:分而治之+动态规划法。
三:动态规划
关于动态规划法我的理解是增加空间代价来减少时间代价,这个方法常用于寻找最优解。
与分而治之类似,通过某种手段将一个大问题分解成为若干个小问题,再分解成更小的问题,但这样的话会导致重复计算,就是小问题与小问题之间有相同的地方,从而导致时间代价大大提高,而动态规划法通过将每个小问题的解求出来,放到一个表中,要用的时候拿出来,这样来减少运算量,减少时间代价,提高空间代价,但相比于超高的时间代价而言这是个更优解。
四:问题分析
我们用分而治之的方法来对这个问题进行分析。有如下字符串:
Xi<x1,x2,x3,…,xn>
Yi<y1,y2,y3,…,ym>
1,若xn=ym
这种情况下,最长子序列的最后一个字符一定是xn(ym),所以我们可以将同时去掉这两个字符,之后再将改动后的字符串的最长子序列求出来加上这个字符即可。
2,若xn!=ym
这种情况下我们可以分为两种情况,一种是去掉Xi的最后一个字符,另一个是去掉Yi的最后一个字符,分别对这两个改动后的字符串求最长子序列。
总体来说如下:
从这两种情况来看我们需要用到递归的方法,一次一次的将问题拆解,这也就是分而治之,同时这也暴露的一个问题,也就是我上面所提到的,重复计算,时间代价大大提高,故而在此基础上我们要结合动态规划的方法。
既然我们要将大问题分为小问题,那么为什么不先把小问题的解提前算好呢?
为了先算好这个问题,我们得逆向考虑。不是从尾巴开始一刀一刀砍,而是从头开始一个一个算。这里有两个字符串:
利用这个表我们可以得知每个子问题的解。
那么这个表是怎么来的呢?
我们暂且忽略第一排0和第一列0,这个是用代码实现的时候用到的,也可以理解是两个空字符串。
我们分析第二排,单拎出这个A,和上面这个字符串一个一个对比,A和B存在零个相同的字符,故表格中的数为0,往后移,A和BD有0个相同的字符,故为0,往后移,A和BDC有0个,往后移,A和BDCA有一个相同的字符,故为1,以此推类,A分析完分析AB,以此推类。最终得到这样一个表格。
但这样还不够,我们注意到表中的箭头号,其中左上箭头表示连个字符串的最后一个字符相同,故同时去掉这个箭头就指向对角线,而左箭头和上箭头则指向二者不同从而分为两个子问题,两个子问题中的更大解,这个就联系到了我们上面分而治之所分析的。
综上我们将这个表制作了出来,有子问题的解,也有方向,从而我们就不需要用递归来找了最优解了,而是通过这个表来找个最优解,从这个表中我们找到一条直通0的通路,将这条通路的每个左上箭头所代表的字符连接成串就是我们要找的最长公共子序列,到这里我们的分析结束了,代码方面也可以轻松的写出来。
五、代码实现
代码方面我们只需要做出一个表就行,在这里我用到的是结构体数组,结构体中包含了解和方向。
#include <iostream>
#include <string>
using namespace std;
struct attribute
{
int derection;
int sum;
};
int maxoftwo(int a1, int a2)
{
if (a1 > a2)
return a1;
return a2;
}
void LCS(string s1, string s2, int lth1, int lth2)
{
attribute att[lth1 + 1][lth2 + 1];//加一是因为还有一排0和一列0,这里读者可以结合代码想一想为什么
for (int i = 0; i < lth1 + 1; i++)
{
for (int j = 0; j < lth2 + 1; j++)
{
att[i][j].derection = 1;
att[i][j].sum = 0;
}
}
for (int i = 0; i < lth1; i++)
{
for (int j = 0; j < lth2; j++)
{
if (s1[i] == s2[j])
{
att[i + 1][j + 1].sum = att[i][j].sum + 1;
att[i + 1][j + 1].derection = 0;
}
if (s1[i] != s2[j])
{
att[i + 1][j + 1].sum = maxoftwo(att[i][j + 1].sum, att[i + 1][j].sum);
att[i + 1][j + 1].derection = att[i][j + 1].sum > att[i + 1][j].sum ? -1 : 1;
}
}
}
char s[att[lth1][lth2].sum];
int k = att[lth1][lth2].sum - 1;//最长子序列数组
int i = lth1, j = lth2;
while (att[i][j].sum != 0)
{
if (att[i][j].derection == 0)
{
s[k] = s1[i - 1];
i--;
j--;
k--;
}
if (att[i][j].derection == 1)
j--;
if (att[i][j].derection == -1)
i--;
}
for (int i = 0; i < att[lth1][lth2].sum; i++)
cout << s[i];
cout << endl;
}
int main()
{
string s1, s2;
cin >> s1 >> s2;
string s;
int length1 = s1.length();
int length2 = s2.length();
LCS(s1, s2, length1, length2);
}
至此,这个问题我们就解决了,关于直接用递归的方法我也不太想去写了,太麻烦了,效率又低,算了算了,读者若有兴趣可以自己写写,可以的话可以私信发给我康康。
注:本篇文章的图片来自于:
程序员编程艺术第十一章:最长公共子序列(LCS)问题