最长公共子序列问题(动态规划求解)

问题

      求长为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; 
}  

注:以上代码来自网络,有错请指出。




    原文作者:动态规划
    原文地址: https://blog.csdn.net/qq_26010491/article/details/50644444
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞