1.“两字符串最长公共子序列”的概念;
一个字符串的子序列,是指从该字符串中去掉任意多个字符后剩下的字符在不改变顺序的情况下组成的新字符串。这个子序列是可以不连续的。最长公共子序列,是指多个字符串可具有的长度最大的公共的子序列。举个例子,如:有两条随机序列,如 1 3 4 5 5 and 2 4 5 5 7 6,则它们的最长公共子序列便是:4 5 5。
注意最长公共子串和最长公共子序列的区别:
最长公共子串(Longest Common Substirng)和最长公共子序列(Longest Common Subsequence,LCS)的区别为:子串是串的一个连续的部分,子序列则是从不改变序列的顺序,而从序列中去掉任意的元素而获得新的序列;也就是说,子串中字符的位置必须是连续的,子序列则可以不必连续。
2.解决思路
(1)穷举法
解最长公共子序列问题时最容易想到的算法是穷举搜索法,即对X的每一个子序列,检查它是否也是Y的子序列,从而确定它是否为X和Y的公共子序列,并且在检查过程中选出最长的公共子序列。X和Y的所有子序列都检查过后即可求出X和Y的最长公共子序列。X的一个子序列相应于下标序列{1, 2, …, m}的一个子序列,因此,X共有2m个不同子序列(Y亦如此,如为2^n),从而穷举搜索法需要指数时间(2^m * 2^n)。
(2)动态规划算法
最长公共子序列问题也有最优子结构性质。记:
Xi=<x1,⋯,xi>,即X序列的前i个字符 (1≤i≤m)(前缀)
Yj=<y1,⋯,yj>,即Y序列的前j个字符 (1≤j≤n)(前缀)
假定Z=<z1,⋯,zk> ∈ LCS(X,Y),那么不难有以下结论:
(1).若xm=yn(最后一个字符相同),则不难用反证法证明:该字符必是X与Y的任一最长公共子序列Z(设长度为k)的最后一个字符,即有zk = xm = yn 且显然有Zk-1∈LCS(Xm-1 , Yn-1)即Z的前缀Zk-1是Xm-1与Yn-1的最长公共子序列。此时,问题化归成求Xm-1与Yn-1的LCS(LCS(X , Y)的长度等于LCS(Xm-1 , Yn-1)的长度加1)。
(2)若xm≠yn,则亦不难用反证法证明:要么Z∈LCS(Xm-1, Y),要么Z∈LCS(X , Yn-1)。由于zk≠xm与zk≠yn其中至少有一个必成立,若zk≠xm则有Z∈LCS(Xm-1 , Y),类似的,若zk≠yn 则有Z∈LCS(X , Yn-1)。此时,问题化归成求Xm-1与Y的LCS及X与Yn-1的LCS。LCS(X , Y)的长度为:max{LCS(Xm-1 , Y)的长度, LCS(X , Yn-1)的长度}。
从上面的分析可以看出,解决这个层次的LCS问题,要求三个方面的东西:
A:LCS(Xm-1,Yn-1)+1;
B:LCS(Xm-1,Y),LCS(X,Yn-1);
C:max{LCS(Xm-1,Y),LCS(X,Yn-1)}。
3.算法实现
(1)穷举法递归实现
穷举法是比较如意想到的方法,但是时间复杂度太高,需要比较的字符串稍微大一点就不行了。并且,这种方法只能计算最长公共子序列的长度,无法计算具体的序列。
算法实现如下:
//1.穷举法:递归实现
int LCS(string x, string y, char* result, int xlen, int ylen, int relen, int i, int j){
if(i < xlen && j < ylen){
if(x[i] == y[j]){
result[relen] = x[i];
relen++;
return 1 + LCS(x, y, result, xlen, ylen, relen, i+1, j+1);
}else{
int count_1 = LCS(x, y, result, xlen, ylen, relen, i, j + 1);
int count_2 = LCS(x, y, result, xlen, ylen, relen, i + 1, j);
return count_1 > count_2 ? count_1 : count_2;
}
}else{
return 0;
}
}
(2)动态规划算法递归实现(记忆化搜索)
相对于穷举法来说,在时间效率上要好很多。为O(m*n)级别,但是由于涉及到多层次的递归调用,一定程度上影响了效率,但基本还可接受。该过程需要用到两个辅助数组,大小为tmp_1[m][n],tmp_2[m][n]。其中一个用来记录最长的子序列长度,另一个记录两个字符串相同字符的信息,据此回溯,就可以得到公共的子序列。
(3)动态规划算法非递归实现(递推)
仍然需要用到两个辅助数组,作用同(2)中一致,不过针对第一个数组(tmp_1)可以做一个优化,因为程序对其中每一个元素的计算只和它左边、上边、左上这三个元素相关,因此只需要保留这几个元素就可以了。因为程序是一行一行计算的,因此只保留两行内容就可以了,根据上面一行计算下面一行,两行交替使用。最终程序结束时的当前行最右侧的元素就是最大的子序列长度。
4.时间复杂度分析
(1)穷举法的时间复杂度:假设字符串X、Y长度分别为m、n,在字符串X的子序列中,任何一个字符都有可能出现,也可能不出现,因此,字符串X的子序列的所有可能组合为2^m种。同理字符串Y的所有子序列的可能组合有2^n种。要取得X、Y的所有公共子序列,需要进行2^m*2^n次比较,时间复杂度为 O(2^m*2^n),然后在从中选出一个或者多个最大的子序列,复杂度为O(2^m + 2^n)。总体复杂度为2^m*2^n。
(2)动态规划法的时间复杂度:动态规划分两个过程,分别是构造公共子序列长度数组和构造最长公共子序列。构造公共子序列数组,数组中共有(m+1)* (n+1)个元素,计算他们的值时间复杂度为O(m*n);构造最长公共子序列过程复杂度为O(max(m,n))总体复杂度为O(m*n)。递归实现和非递归实现都是如此。
(3)下面是针对实际例子的测试情况,字符串都是由程序随机产生:
字符串X长度 | 字符串Y长度 | 公共子序列长度 | 递归时间(ms) | 非递归时间(ms) |
---|---|---|---|---|
20 | 30 | 7 | 10 | 0 |
200 | 300 | 75 | 82 | 2 |
500 | 1000 | 220 | 1302 | 17 |
2000 | 1000 | 441 | 11856 | 68 |
2000 | 3000 | 787 | 64898 | 191 |
(4)从实际效果来看,动态规划算法的非递归实现要比递归实现效率高很多。可能是因为递归法需要创建函数、分配栈空间等开销较大所致。非递归的时间复杂度相对稳定,递归的发挥不是很稳定。
5.完整源代码
#include <iostream>
#include <string>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <stdlib.h>
using namespace std;
const int X = 20, Y = 30;
//1.穷举法:递归实现
int LCS(string x, string y, char* result, int xlen, int ylen, int relen, int i, int j){
if(i < xlen && j < ylen){
if(x[i] == y[j]){
result[relen] = x[i];
relen++;
return 1 + LCS(x, y, result, xlen, ylen, relen, i+1, j+1);
}else{
int count_1 = LCS(x, y, result, xlen, ylen, relen, i, j + 1);
int count_2 = LCS(x, y, result, xlen, ylen, relen, i + 1, j);
return count_1 > count_2 ? count_1 : count_2;
}
}else{
return 0;
}
}
int LCS_2(string x, string y, char* result, int xlen, int ylen, int relen, int i, int j){
if(i >= 0 && j >= 0 && i < xlen && j < ylen){
if(x[i] == y[j]){
result[relen] = x[i];
relen++;
return 1 + LCS_2(x, y, result, xlen, ylen, relen, i - 1, j - 1);
}else{
int count_1 = LCS_2(x, y, result, xlen, ylen, relen, i, j - 1);
int count_2 = LCS_2(x, y, result, xlen, ylen, relen, i - 1, j);
return count_1 > count_2 ? count_1 : count_2;
}
}
return 0;
}
//2.动态规划算法:递归实现(记忆化搜索)
int LCS_length_1(string x, string y, int tmp_1[][Y+1], int tmp_2[][Y+1], int xlen, int ylen, int i, int j){
if(i >= 0 && j >= 0 && i <= xlen && j <= ylen){
if(i == 0 || j == 0 || tmp_1[i][j] > 0){
return tmp_1[i][j];
}else{
if(x[i-1] == y[j-1]){
tmp_2[i][j] = 0;
return tmp_1[i][j] = 1 + LCS_length_1(x, y, tmp_1, tmp_2, xlen, ylen, i-1, j-1);
}else if((tmp_1[i][j-1] = LCS_length_1(x, y, tmp_1, tmp_2, xlen, ylen, i, j-1)) >=
(tmp_1[i-1][j] = LCS_length_1(x, y, tmp_1, tmp_2, xlen, ylen, i-1, j))){
tmp_2[i][j] = 1;
return tmp_1[i][j] = tmp_1[i][j-1];
}else{
tmp_2[i][j] = -1;
return tmp_1[i][j] = tmp_1[i-1][j];
}
}
}
return 0;
}
//3.动态规划算法:非递归实现(递推实现,空间优化)
int LCS_length_2(string x, string y, int tmp_1[][Y+1], int tmp_2[][Y+1], int xlen, int ylen){
int i = 0, j = 0, k = 0;
//辅助数组的首行元素置为0
for(i = 0; i <= xlen; i++){
tmp_1[0][i] = 0;
}
tmp_1[1][0] = 0;
//对数组内容进行写入
for(i = 1; i <= xlen; i++){
k = i%2;
tmp_1[i%2][0] = 0;
for(j = 1; j <= ylen; j++){
if(x[i-1] == y[j-1]){
tmp_1[k][j] = tmp_1[(i-1)%2][j-1] + 1;
tmp_2[i][j] = 0; //斜向下
}else{
if(tmp_1[k][j-1] >= tmp_1[(i-1)%2][j]){
tmp_1[k][j] = tmp_1[k][j-1];
tmp_2[i][j] = 1; //向右
}else{
tmp_1[k][j] = tmp_1[(i-1)%2][j];
tmp_2[i][j] = -1; //向下
}
}
}
}
return tmp_1[(i-1)%2][j-1];
}
//最大公共子序列输出函数,非递归实现
int LCS_print_1(string x, char *result, int tmp_2[][Y+1], int xlen, int ylen){
int i = xlen, j = ylen, k = 0;
while(i > 0 && j > 0 ){
if(tmp_2[i][j] == 0){
result[k] = x[i-1]; //斜向下
k++;
i--;
j--;
}else if(tmp_2[i][j] == 1){
j--; //向右
}else if(tmp_2[i][j] == -1){
i--; //向下
}
}
return k;
}
//-------------------------以下为测试代码----------------------------------------------------
//1.测试穷举法递归实现
void test_1(string x, string y){
char result[X];
int xlen = x.length();
int ylen = y.length();
//int max_len = LCS(x, y, result, xlen, ylen, 0, 0, 0);
int max_len = LCS_2(x, y, result, xlen, ylen, 0, xlen - 1, ylen - 1);
cout << "1.穷举法递归求最长公共子序列:" << endl;
cout << " 最大公共子序列长度为:" << max_len << endl;
/*
for(int i = 0; i < max_len; i++){
cout << result[i];
}
cout << endl;
*/
}
//2.测试动态规划算法递归实现
void test_2(string x, string y){
//2.1参数处理
char result[X+1];
int xlen = x.length();
int ylen = y.length();
int tmp_1[X+1][Y+1] = {1};
int tmp_2[X+1][Y+1] = {-2};
//2.2初始化辅助数组
memset(tmp_1,-1,sizeof(tmp_1));
for(int i = 0; i <= xlen; i++){
tmp_1[i][0] = 0;
}
for(int j = 0; j <= ylen; j++){
tmp_1[0][j] = 0;
}
//2.3执行计算
int max_len = LCS_length_1(x, y, tmp_1, tmp_2, xlen, ylen, xlen, ylen);
LCS_print_1(x, result, tmp_2, xlen, ylen);
//3.3打印结果
cout << "2.动态规划法递归求最长公共子序列:" << endl;
cout << " 最大公共子序列长度为:" << max_len << " 序列为:";
for(int j = max_len - 1; j >= 0; j--){
cout << result[j];
}
cout << endl;
}
//3.测试动态规划算法非递归实现
void test_3(string x, string y){
//3.1参数处理
char result[X+1];
int xlen = x.length();
int ylen = y.length();
int tmp_1[2][Y+1] = {-2};
int tmp_2[X+1][Y+1] = {-2};
//3.2执行计算
int max_len = LCS_length_2(x, y, tmp_1, tmp_2, xlen, ylen);
LCS_print_1(x, result, tmp_2, xlen, ylen);
//3.3输出结果
cout << "3.动态规划法非递归求最长公共子序列:" << endl;
cout << " 最大公共子序列长度为:" << max_len << " 序列为:";
for(int j = max_len - 1; j >= 0; j--){
cout << result[j];
}
cout << endl;
}
char* rand_string(char* str, int len){
int i = 0;
srand((unsigned)time(NULL));
for(i = 0; i < len; i++){
str[i] = rand()%26 + 'a';
}
str[i] = '\0';
return str;
}
long getCurrentTime(){
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
int main()
{
char str_1[X+1];
rand_string(str_1,X);
string x(str_1);
sleep(1);
char str_2[Y+1];
rand_string(str_2,Y);
string y(str_2);
cout << "string x = " << x << endl;
cout << " 字符串长度为:" << x.length() << endl;
cout << "string y = " << y << endl;
cout << " 字符串长度为:" << y.length() << endl;
long time_1 = getCurrentTime();
//test_1(x, y);
long time_2 = getCurrentTime();
cout << " 用时:";
cout << time_2 - time_1 << " ms;" << endl;
long time_3 = getCurrentTime();
test_2(x, y);
long time_4 = getCurrentTime();
cout << " 用时:";
cout << time_4 - time_3 << " ms;" << endl;
long time_5 = getCurrentTime();
test_3(x, y);
long time_6 = getCurrentTime();
cout << " 用时:";
cout << time_6 - time_5 << " ms;" << endl;
return 1;
}