问题
求长为m的序列和长为n的序列的最长公共子序列(可以不连续),如ABCBDAB和BDCABA,BCAB和BCBA都是它们的最长公共子序列。在生物学上用来求DNA序列的匹配度。这里我们用它举例来学习动态规划方法。
先放结论:
动态规划是暴力递归的一种优化。要写出动态规划,首先要试着找出最优子结构与重叠子问题,然后写出递归式。
1.最优子结构就是一个问题的最优解包含其子问题的最优解。换句话说,我们用子问题的最优解来构造原问题的最优解。具有这个性质的问题也适用于贪心策略。我们在优化递归的时候后面的问题可以用到前面的结论。
2.重叠子问题,递归算法会重复求解相同的子问题,所以才有了我们的填表优化。查表的时间是常数级别的。
我们优化的时候可以在每一次递归时保存上一次的值,最好的是用二维表格自顶向下的记录值。下面我们看这个问题。
最长公共子序列问题
我们首先写出它的递归式
如果两个序列某两个位置i,j上的字符相等,那它们的LCS(最长公共子序列)为前面字符的LCS+1,如果不等,LCS就是i位置和j-1位置处的LCS和i-1和j位置LCS的最大值,如果用c[n]来存储每一个位置上的LCS的话,得到以下式子
我们现在可以写出暴力递归的代码分析,长为m的序列一共有2^m个子序列(把每一位上的字母用0或1表示,0代表没有,1代表有,一共有2*2*2*……*2=2^m种状态,所以有2^m个子序列),如果用暴力搜索匹配,一共要O(n*2^m),运行时间是指数级别的。
我们可以画出递归树,递归二叉树的高度是m+n,运行时间是2^(m+n)级别的,还不如暴力搜索。
动态规划
由于LCS只有m*n个子问题,我们可以用长为m+1和n+1的二维数组c[i][j]来计算,并用b[i][j]数组来保存最优解的标志,以便回溯得到最优解。表格中从第二行和第二列开始每一格的值依赖于左上角,左边和上边三个数。
代码
#include<stdio.h>
#include<string.h>
char a[500],b[500];
char num[501][501]; ///记录中间结果的数组
char flag[501][501]; ///标记数组,用于标识下标的走向,构造出公共子序列
void LCS(); ///动态规划求解
void getLCS(); ///采用倒推方式求最长公共子序列
int main()
{
int i;
strcpy(a,"ABCBDAB");
strcpy(b,"BDCABA");
memset(num,0,sizeof(num));
memset(flag,0,sizeof(flag));
LCS();
printf("%d\n",num[strlen(a)][strlen(b)]);
getLCS();
return 0;
}
void LCS()
{
int i,j;
for(i=1;i<=strlen(a);i++)
{
for(j=1;j<=strlen(b);j++)
{
if(a[i-1]==b[j-1]) ///注意这里的下标是i-1与j-1
{
num[i][j]=num[i-1][j-1]+1;
flag[i][j]=1; ///斜向下标记
}
else if(num[i][j-1]>num[i-1][j])
{
num[i][j]=num[i][j-1];
flag[i][j]=2; ///向右标记
}
else
{
num[i][j]=num[i-1][j];
flag[i][j]=3; ///向下标记
}
}
}
}
void getLCS()
{
char res[500];
int i=strlen(a);
int j=strlen(b);
int k=0; ///用于保存结果的数组标志位
while(i>0 && j>0)
{
if(flag[i][j]==1) ///如果是斜向下标记
{
res[k]=a[i-1];
k++;
i--;
j--;
}
else if(flag[i][j]==2) ///如果是斜向右标记
j--;
else if(flag[i][j]==3) ///如果是斜向下标记
i--;
}
for(i=k-1;i>=0;i--)
printf("%c",res[i]);
}
时间复杂度为O(m*n),空间复杂度O(m*n),如果只求最大长度不要求序列,我们还可以将空间优化。只用min(m+1, n+1)+3的空间就可以完成。
空间优化代码
#include<iostream>
#include<string>
#include<memory.h>
#include <stdlib.h>
using namespace std;
int LCS(string x, string y);
int main() {
string x, y;
cin>>x>>y;
LCS(x, y);
system("pause");
}
int LCS(string x, string y){
int lenx = x.length();
int leny = y.length();
int leftabove, left, above; //左上角 左 上
int *compute = new int[leny + 1]; //compute[0] 即原来的calc[i][0] for i in [0, lenx];
memset(compute, 0, (leny + 1) * sizeof(int));
for(int i = 1; i <= lenx; i++) {
leftabove = left = compute[0];
above = compute[1];
for(int t = 0; t <= leny; t++) cout<<compute[t]<<" ";
cout<<endl;
for(int j = 1; j <= leny; j++) {
//计算,并且更新这三个变量
if(x[i - 1] == y[j - 1]) compute[j] = leftabove + 1;
else if(left > above) compute[j] = left;
else compute[j] = above;
//更新此三个变量,很有技巧的哦
leftabove = above;
above = compute[j + 1];
left = compute[j];
}
}
cout<<compute[leny]<<endl;
delete[] compute;
}
注:以上代码来自网络,有错请指出。