下面这篇文章介绍一下在算法设计中动态规划的最长公共子序列的问题。
最长公共子序列问题所谓,也即是分别给出长度为n和m的字符串A,B,然后找出其中最长公共子序列的最优值和最优解。
所谓最优值,也就是求出这个最长公共子序列的长度;而最优解,就是要求出这个最长最长公共子序列是什么的问题。给个不太恰当的例子:我们中学数学中经常会遇到求函数最值的问题,比如说求出函数 f(x) 的最大值,那么这个最大值就是最优值了;而最优解是什么呢?就是当f(x)取到最大值的时候的 x 值了。
解决的方法其实最简单的就是使用蛮力的方法,列举某一个字符串中的所有子序列,也就是求出这个字符串子序列的幂集;然后分别与另一字符串进行比较。那么由于幂集是 2 的 n 次方。所以其时间复杂度也就是 2 的 n 次方。
那么采用动态规划,可以通过寻求一个求最长公共子序列长度(下面简称LCS)的递推公式。
假设字符串 A = a1 a2 a3 a4.....ai B = b1 b2 b3 b4......bj
L[ i , j ]表示字符串A 和 B的最长公共子序列的长度。
那么:
稍作解释:①假如字符串A B中,有一个是长度为0;那么LCS = 0;
②假如,两个字符串的最后一个字符相同,那么其长度等于,A,B前面字符的匹配结果 +1
③假如,两个字符串的最后一个字符不相同,那么其长度等于,A减去最后一个字符和B进行匹配的结果,B减去最后一个字符和A进行匹配的结果,二者的最大值。
所以对于这个问题的求解,我们可以用一个二维表来进行计算求解,利用上面的公式逐行的填表。二维表的最后一个元素即是问题的最优值。
下面给出伪代码的实现:
下面给出c语言的实现代码:
//注意数组和字符串下标都是从0开始的
int LCS(string strA,string strB)
{
int lenA = strA.length();
int lenB = strB.size(); //注意length和size的作用是一样的,都是返回字符串的长度。
int n = lenA+1;
int m = lenB+1;
//动态创建一个二维数组 n 行 m 列
int **result = new int* [n];
for(int k = 0;k < n;k++) result[k] = new int [m];
for(int i = 0; i < n; i++) result[i][0] = 0; //将第一列全部置零
for(int j = 0; j < m; j++) result[0][j] = 0; //将第一行全部置零
for(int i = 1; i< n; i++)
for(int j = 1; j < m; j++) //逐行填充表格
{
//如果两个字符相等,则result[i][j]等于表中左上角元素值+1
if(strA[i-1] == strB[j-1]) result[i][j] = result[i-1][j-1]+1;
//如果两个字符不相等,则result[i][j]等于表中左边或者是上边元素值的最大者
//result[i][j-1]表示左边元素;result[j][i-1]表示s上边元素
else result[i][j] = getMax(result[i][j-1],result[i-1][j]);
}
int returnNum = result[n-1][m-1];//最后返回的结果:最长公共子序列长度
//删除动态创建的二维数组
for(int i=0;i<n;i++)
delete[] result[i];
delete [] result;
return returnNum;
}
对于代码就不多解释了。
下面给出一个例子:
string stringA = "xyxxzxyzxy"; //10
string stringB = "zxzyyzxxyxxz"; //12
那么创建的二维表就是:注意行和列都要多一,表示空匹配。那么这个例子所创建的数组就是 [ 11 ][ 13 ] 然后根据上面的递推公式进行填表就可以了。
其实这是一种逐个用列字符串中的每一个字符去匹配行字符串中每一个字符。
分析一下这个算法:时间复杂度也就是填表的时间,也就是(n+1)*(m+1);其中n表示字符串A的长度,m表示字符串B的长度。空间复杂度也就是是这个二维表啦,那么就是 (n+1)*(m+1)。
那么考虑一下,是否可以改进一下空间复杂度呢?--- 当然可以!其只需要使用一个两行的二维数组。
怎么说呢?也就是只要利用到一个两层一维数组,然后重复使用进行填表。
注意其中的最后一行只是为了显示迭代填表的过程。
那么:再改进一下,我们可以选择字符串短的那个字符串作为行字符串,那么就可以将所用到的空间减少到极品了。
下面看一下c语言实现代码:
/*
说明:空间使用缩小到两层
*/
int LCS_1(string strA,string strB)
{
int lenA = strA.length();
int lenB = strB.size(); //注意length和size的作用是一样的,都是返回字符串的长度。
//这里为了节约空间,所以选择字符串长度短的作为行,那么总是假定strB是字符串长度短的。
//如果strB字符串长度大写,则交换两个字符串。
if(lenA>=lenB?false:true)
{
string tempStr = strA;
strA = strB;
strB = tempStr;
}
//注意:另个字符串有可能是经过交换的,所以重新计算其长度。
//这里n,m表示的是原来所需要的二维表行列数
int n = strA.length()+1;
int m = strB.length()+1;
//动态生成一个2行m列的数组
int **result = new int* [2];
for(int k = 0;k < 2;k++) result[k] = new int [m];
for(int i = 0; i < 2; i++) result[i][0] = 0; //将第一列全部置零
for(int j = 0; j < m; j++) result[0][j] = 0; //将第一行全部置零
for(int i = 1; i< n; i++)
for(int j = 1; j < m; j++) //逐行填充表格
{
if(strA[i-1] == strB[j-1])
{
if((i-1)%2 == 0) //表示偶数
{
result[1][j] = result[0][j-1]+1;
}
else result[0][j] = result[1][j-1]+1;
}
else
{
if((i-1)%2 == 0) //表示偶数
{
result[1][j] = getMax(result[1][j-1],result[0][j]); //左边,上边
}
else result[0][j] = getMax(result[0][j-1],result[1][j]);
}
}
int returnNum;//最后返回的结果:最长公共子序列长度
//注意: 前面已经假定是字符串长度短的strB作为行,那么字符串长些的strA就是作为列
//还要注意的是(n-2)得到的是字符串最后一个字符所在的下标
if((n-2)%2 == 0) returnNum = result[1][m-1];
else returnNum = result[0][m-1];
//删除动态创建的二维数组
for(int i=0;i<2;i++)
delete[] result[i];
delete [] result;
return returnNum;
}
下面给出两个测试案列:
string stringA = "xyxxzxyzxy"; //10
string stringB = "zxzyyzxxyxxz"; //12
LCS = 6
string stringA = "xyyzx"; //6
string stringB = "zxyxyz"; //7
LCS = 4
下面给出两个测试案列:
string stringA = "xyxxzxyzxy"; //10
string stringB = "zxzyyzxxyxxz"; //12
LCS = 6
string stringA = "xyyzx"; //6
string stringB = "zxyxyz"; //7
LCS = 4
下面给出两个测试案列:
string stringA = "xyxxzxyzxy"; //10
string stringB = "zxzyyzxxyxxz"; //12
LCS = 6
string stringA = "xyyzx"; //6
string stringB = "zxyxyz"; //7
LCS = 4
下面给出两个测试案列:
string stringA = "xyxxzxyzxy"; //10
string stringB = "zxzyyzxxyxxz"; //12
LCS = 6
string stringA = "xyyzx"; //6
string stringB = "zxyxyz"; //7
LCS = 4