动态规划专题 最长公共子序列

一些概念:

(1)子序列: 一个序列A = a1,a2,……an,中任意删除若干项,剩余的序列叫做A的一个子序列。也可以认为是从序列A按原顺序保留任意若干项得到的序列。

例如:

对序列 1,3,5,4,2,6,8,7来说,序列3,4,8,7 是它的一个子序列。
对于一个长度为n的序列,它一共有2^n 个子序列,有(2^n – 1)个非空子序列。

请注意:子序列不是子集,它和原始序列的元素顺序是相关的。

(2)公共子序列 : 顾名思义,如果序列C既是序列A的子序列,同时也是序列B的子序列,则称它为序列A和序列B的公共子序列。

例如:

对序列 1,3,5,4,2,6,8,7和序列 1,4,8,6,7,5 来说

序列1,8,7是它们的一个公共子序列。

请注意: 空序列是任何两个序列的公共子序列。
例如: 序列1,2,3和序列4,5,6的公共子序列只有空序列。

(3)最长公共子序列

A和B的公共子序列中长度最长的(包含元素最多的)叫做A和B的公共子序列。
仍然用序列1,3,5,4,2,6,8,7和序列1,4,8,6,7,5

它们的最长公共子序列是:

1,4,8,7
1,4,6,7

最长公共子序列的长度是4 。
请注意: 最长公共子序列不唯一。

请大家用集合的观点来理解这些概念,子序列、公共子序列以及最长公共子序列都不唯一,所以我们通常说一个最长公共子序列,但显然最长公共子序列的长度是一定的。

最长公共子序列问题就是求序列A= a1,a2,……an, 和B = b1,b2,……bm,的一个最长公共子序列。

因为最长公共子序列不唯一,让我们把问题简化,如何求出两个序列的最长公共子序列长度呢?

你首先能想到的恐怕是暴力枚举?那我们先来看看:序列A有 2^n 个子序列,序列B有 2^m 个子序列,如果任意两个子序列一一比较,比较的子序列高达 2^(n+m) 对,这还没有算具体比较的复杂度。

或许你说,只有长度相同的子序列才会真正进行比较。那么忽略空序列,我们来看看:对于A长度为1的子序列有C(n,1)个,长度为2的子序列有C(n,2)个,……长度为n的子序列有C(n,n)个。对于B也可以做类似分析,即使只对序列A和序列B长度相同的子序列做比较,那么总的比较次数高达:

C(n,1)*C(m,1)*1 + C(n,2) * C(m,2) * 2+ …+C(n,p) * C(m,p)*p

其中p = min(m, n)。

吓着了吧?怎么办?试试使用动态规划算法!

我们用Ax表示序列A的连续前x项构成的子序列,即Ax= a1,a2,……ax, By= b1,b2,……by, 我们用LCS(x, y)表示它们的最长公共子序列长度,那原问题等价于求LCS(m,n)。为了方便我们用L(x, y)表示Ax和By的一个最长公共子序列。

让我们来看看如何求LCS(x, y)。我们令x表示子序列考虑最后一项

(1) Ax = By

那么它们L(Ax, By)的最后一项一定是这个元素!

为什么呢?为了方便,我们令t = Ax = By, 我们用反证法:假设L(x,y)最后一项不是t,

则要么L(x,y)为空序列(别忘了这个),要么L(x,y)的最后一项是Aa=Bb ≠ t, 且显然有a < x, b < y。无论是哪种情况我们都可以把t接到这个L(x,y)后面,从而得到一个更长的公共子序列。矛盾!

如果我们从序列Ax中删掉最后一项ax得到Ax-1,从序列By中也删掉最后一项by得到By-1,(多说一句角标为0时,认为子序列是空序列),则我们从L(x,y)也删掉最后一项t得到的序列是L(x – 1, y - 1)。为什么呢?和上面的道理相同,如果得到的序列不是L(x - 1, y - 1),则它一定比L(x - 1, y - 1)短(注意L(,)是个集合!),那么它后面接上元素t得到的子序列L(x,y)也比L(x - 1, y - 1)接上元素t得到的子序列短,这与L(x, y)是最长公共子序列矛盾。

因此L(x, y) = L(x - 1, y - 1) 最后接上元素t

LCS(Ax, By) = LCS(x - 1, y - 1) + 1

(2)  Ax ≠ By

仍然设t = L(Ax, By), 或者L(Ax, By)是空序列(这时t是未定义值不等于任何值)。

则t  ≠ Ax和t  ≠ By至少有一个成立,因为t不能同时等于两个不同的值嘛!

(2.1) 如果t  ≠ Ax,则有L(x, y)= L(x - 1, y),因为根本没Ax的事嘛。

LCS(x,y) = LCS(x – 1, y)

(2.2) 如果t  ≠ By,l类似L(x, y)= L(x , y - 1)

LCS(x,y) = LCS(x, y – 1)

可是,我们事先并不知道t,由定义,我们取最大的一个,因此这种情况下,有LCS(x,y) = max(LCS(x – 1, y) , LCS(x, y – 1))。
看看目前我们已经得到了什么结论:

LCS(x,y) = 
(1) LCS(x - 1,y - 1) + 1 如果Ax = By
(2) max(LCS(x – 1, y) , LCS(x, y – 1)) 如果Ax ≠ By

这时一个显然的递推式,光有递推可不行,初值是什么呢?

显然,一个空序列和任何序列的最长公共子序列都是空序列!所以我们有:

LCS(x,y) = 
(1) LCS(x - 1,y - 1) + 1 如果Ax = By
(2) max(LCS(x – 1, y) , LCS(x, y – 1)) 如果Ax ≠ By
(3) 0 如果x = 0或者y = 0

到此我们求出了计算最长公共子序列长度的递推公式。我们实际上计算了一个(n + 1)行(m + 1)列的表格(行是0..n,列是0..m),也就这个二维度数组LCS(,)。

大概的伪代码如下:
输入序列A, B长度分别为n,m,计算二维表 LCS(int,int):

for x = 0 to n do
    for y = 0 to m do
        if (x == 0 || y == 0) then
            LCS(x, y) = 0
        else if (Ax == By) then
            LCS(x, y) =  LCS(x - 1,y - 1) + 1
        else
            LCS(x, y) = ) max(LCS(x – 1, y) , LCS(x, y – 1))
        endif
    endfor
endfor

注意: 我们这里使用了循环计算表格里的元素值,而不是递归,如果使用递归需要已经记录计算过的元素,防止子问题被重复计算。

现在问题来了,我们如何得到一个最长公共子序列而仅仅不是简单的长度呢?其实我们离真正的答案只有一步之遥!

仍然考虑那个递推式,我们LCS(x,y)的值来源的三种情况:

(1) LCS(x – 1,  y – 1) + 1如果Ax = By
这对应L(x,y) = L(x,- 1 y- 1)末尾接上Ax

(2.1) LCS(x – 1, y)  如果Ax ≠ By且LCS(x – 1, y) ≥LCS(x, y – 1)
这对应L(x,y)= L(x – 1, y)
(2.2) LCS(x, y – 1)  如果Ax ≠ By且LCS(x – 1, y) <LCS(x, y – 1)
这对应L(x,y) = L(x, y – 1)

(3) 0 如果 x =0或者y = 0
这对应L(x,y)=空序列

注意(2.1)和(2.2) ,当LCS(x – 1, y) = LCS(x, y – 1)时,其实走哪个分支都一样,虽然长度时一样的,但是可能对应不同的子序列,所以最长公共子序列并不唯一。
神奇吧?又一个类似的递推公式。可见我们在计算长度LCS(x,y)的时候只要多记录一些信息,就可以利用这些信息恢复出一个最长公共子序列来。就好比我们在迷宫里走路,走到每个位置的时候记录下我们时从哪个方向来的,就可以从终点回到起点一样。

另外,说一下复杂度?

时间复杂度时O(n * m),空间也是O(n * m)
今天对LCS的讲解就到这里,聪明的你是不是已经蠢蠢欲动要AC问题啦? 心动不如行动,赶快吧。

在这里,本宝宝找到了两个模板,先收藏着,等过段在细细琢磨琢磨,有新的收获再补充。
代码一是让你输入两个序列,然后输出最长公共子序列和长度。
代码二是让你输入三个序列,然后输出最长公共子序列的长度。

代码一:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int LCSLength(char* str1, char* str2, int **b)
{
    int i,j,length1,length2,len;
    length1 = strlen(str1);
    length2 = strlen(str2);

    //双指针的方法申请动态二维数组
    int **c = new int*[length1+1]; //共有length1+1行
    for(i = 0; i < length1+1; i++)
        c[i] = new int[length2+1];//共有length2+1列

    for(i = 0; i < length1+1; i++)
        c[i][0]=0;        //第0列都初始化为0
    for(j = 0; j < length2+1; j++)
        c[0][j]=0;        //第0行都初始化为0

    for(i = 1; i < length1+1; i++)
    {
        for(j = 1; j < length2+1; j++)
        {
            if(str1[i-1]==str2[j-1])//由于c[][]的0行0列没有使用,c[][]的第i行元素对应str1的第i-1个元素
            {
                c[i][j]=c[i-1][j-1]+1;
                b[i][j]=0;          //输出公共子串时的搜索方向
            }
            else if(c[i-1][j]>c[i][j-1])
            {
                c[i][j]=c[i-1][j];
                b[i][j]=1;
            }
            else
            {
                c[i][j]=c[i][j-1];
                b[i][j]=-1;
            }
        }
    }
    /*
    for(i= 0; i < length1+1; i++)
    {
    for(j = 0; j < length2+1; j++)
    printf("%d ",c[i][j]);
    printf("\n");
    }
    */
    len=c[length1][length2];
    for(i = 0; i < length1+1; i++)    //释放动态申请的二维数组
        delete[] c[i];
    delete[] c;
    return len;
}
void PrintLCS(int **b, char *str1, int i, int j)
{
    if(i==0 || j==0)
        return ;
    if(b[i][j]==0)
    {
        PrintLCS(b, str1, i-1, j-1);//从后面开始递归,所以要先递归到子串的前面,然后从前往后开始输出子串
        printf("%c",str1[i-1]);//c[][]的第i行元素对应str1的第i-1个元素
    }
    else if(b[i][j]==1)
        PrintLCS(b, str1, i-1, j);
    else
        PrintLCS(b, str1, i, j-1);
}

int main(void)
{
    char str1[100],str2[100];
    int i,length1,length2,len;
    printf("请输入第一个字符串:");
    gets(str1);
    printf("请输入第二个字符串:");
    gets(str2);
    length1 = strlen(str1);
    length2 = strlen(str2);
    //双指针的方法申请动态二维数组
    int **b = new int*[length1+1];
    for(i= 0; i < length1+1; i++)
        b[i] = new int[length2+1];
    len=LCSLength(str1,str2,b);
    printf("最长公共子序列的长度为:%d\n",len);
    printf("最长公共子序列为:");
    PrintLCS(b,str1,length1,length2);
    printf("\n");
    for(i = 0; i < length1+1; i++)//释放动态申请的二维数组
        delete[] b[i];
    delete[] b;
    system("pause");
    return 0;
}

代码二:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int max1(int m,int n)
{
    if(m>n)
        return m;
    else
        return n;
}
int max2(int x,int y,int z,int k,int m,int n)
{
    int max=-1;
    if(x>max)
        max=x;
    if(y>max)
        max=y;
    if(z>max)
        max=z;
    if(k>max)
        max=k;
    if(m>max)
        max=m;
    if(n>max)
        max=n;
    return max;
}
int LCSLength(char* str1, char* str2, char* str3) //求得三个字符串的最大公共子序列长度并输出公共子序列
{
    int i,j,k,length1,length2,length3,len;
    length1 = strlen(str1);
    length2 = strlen(str2);
    length3 = strlen(str3);

    //申请动态三维数组
    int ***c = new int**[length1+1];      //共有length1+1行
    for(i = 0; i < length1+1; i++)
    {
        c[i] = new int*[length2+1];      //共有length2+1列
        for(j = 0; j<length2+1; j++)
            c[i][j] = new int[length3+1];
    }

    for(i = 0; i < length1+1; i++)
    {
        for(j = 0; j < length2+1; j++)
            c[i][j][0]=0;
    }
    for(i = 0; i < length2+1; i++)
    {
        for(j = 0; j < length3+1; j++)
            c[0][i][j]=0;
    }
    for(i = 0; i < length1+1; i++)
    {
        for(j = 0; j < length3+1; j++)
            c[i][0][j]=0;
    }

    for(i = 1; i < length1+1; i++)
    {
        for(j = 1; j < length2+1; j++)
        {
            for(k = 1; k < length3+1; k++)
            {
                if(str1[i-1]==str2[j-1] && str2[j-1]==str3[k-1])
                    c[i][j][k]=c[i-1][j-1][k-1]+1;
                else if(str1[i-1]==str2[j-1] && str1[i-1]!=str3[k-1])
                    c[i][j][k]=max1(c[i][j][k-1],c[i-1][j-1][k]);
                else if(str1[i-1]==str3[k-1] && str1[i-1]!=str2[j-1])
                    c[i][j][k]=max1(c[i][j-1][k],c[i-1][j][k-1]);
                else if(str2[j-1]==str3[k-1] && str1[i-1]!=str2[j-1])
                    c[i][j][k]=max1(c[i-1][j][k],c[i][j-1][k-1]);
                else
                {
                    c[i][j][k]=max2(c[i-1][j][k],c[i][j-1][k],c[i][j][k-1],c[i-1][j-1][k],c[i-1][j][k-1],c[i][j-1][k-1]);
                }
            }
        }
    }
    len=c[length1][length2][length3];
    for(i = 1; i < length1+1; i++) //释放动态申请的三维数组
    {
        for(j = 1; j < length2+1; j++)
            delete[] c[i][j];
        delete[] c[i];
    }
    delete[] c;
    return len;
}

int main(void)
{
    char str1[100],str2[100],str3[100];
    int len;

    printf("请输入第一个字符串:");
    gets(str1);
    printf("请输入第二个字符串:");
    gets(str2);
    printf("请输入第三个字符串:");
    gets(str3);
    len=LCSLength(str1,str2,str3);
    printf("最长公共子序列的长度为:%d\n",len);
    system("pause");
    return 0;
}

原文地址:https://www.cnblogs.com/seamusopen/p/9919430.html

时间: 2024-10-13 12:04:58

动态规划专题 最长公共子序列的相关文章

算法——动态规划篇——最长公共子序列

问题描述      最长公共子序列,英文缩写为LCS(Longest Common Subsequence).其定义是,一个序列 S ,如果分别是两个或多个已知序列的子序列,且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列.       解决最长公共子序列,一种常用的办法,就是穷举法,组合出所有的情况,但是这样对于长序列的情况来说,是非常不实际.. 假设现在有两个序列,x[]={'A','B','C','B','D','A','B'};y[]={'B','D','C','A'

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

原文链接 动态规划法 经常会遇到复杂问题不能简单地分解成几个子问题,而会分解出一系列的子问题.简单地采用把大问题分解成子问题,并综合子问题的解导出大问题的解的方法,问题求解耗时会按问题规模呈幂级数增加. 解决思想: 为了节约重复求相同子问题的时间,引入一个数组,不管它们是否对最终解有用,把所有子问题的解存于该数组中,这就是动态规划法所采用的基本方法. [问题] 求两字符序列的最长公共字符子序列 问题描述:字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后

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

 动态规划法 经常会遇到复杂问题不能简单地分解成几个子问题,而会分解出一系列的子问题.简单地采用把大问题分解成子问题,并综合子问题的解导出大问题的解的方法,问题求解耗时会按问题规模呈幂级数增加. 为了节约重复求相同子问题的时间,引入一个数组,不管它们是否对最终解有用,把所有子问题的解存于该数组中,这就是动态规划法所采用的基本方法. [问题] 求两字符序列的最长公共字符子序列 问题描述:字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列

【算法导论之七】动态规划求解最长公共子序列

一.动态规划的概念 动态规划(Dynamic Programming)是通过组合子问题的解而解决整个问题的.分治算法是指将问题划分成一些独立的子问题,递归地求解各子问题,然后合并子问题的解而得到原始问题的解,与此不同,动态规划适用于子问题不是独立的情况,也就是各个子问题包含公共的子问题.在这种情况下,采用分治法会做许多不必要的工作,即重复地求解公共地子问题.动态规划算法对每个子问题只求解一次,将其结果保存在一张表中,从而避免每次遇到各个子问题时重新计算答案. 动态规划通常应用于最优化问题.此类问

算法导论--动态规划(最长公共子序列)

最长公共子序列问题(LCS) 给定两个序列X=?x1,x2,x3...xm?和Y=?y1,y2,y3...xn?,求X和Y的最长公共子序列. 例如:X=?A,B,C,B,D,A,B?,和Y=?B,D,C,A,B,A?,的最长公共子序列为?B,C,B,A?,长度为4: 对于此问题,可以采用暴力求解的方式来比对,即穷举出X的所有子序列,用每个子序列与y做一 一比较.假如X序列共有m个元素,对每个元素可以决定选或不选,则X的子序列个数共有2m个,可见与长度m呈指数阶,这种方法效率会很低. 动态规划 前

动态规划之最长公共子序列(LCS)

tips : 本文内容是参考了很多著名博客和自己的思考得出的,如有不当欢迎拍砖. 先简单说一下动态规划 通俗地说:动态规划就是将一个可以划分为子问题的问题进行递归求解,不过动态规划将大量的中间结果保存起来, 不管它们是否会用得到,从而在后面的递归求解过程中可以快速求解.由此可以看得出来动态规划是一个以牺牲空间 为代价换取时间的算法. 对于最长公共子序列的题目不用多说,现在来分析一下LCS的动态规划解决思路: 一.首先先观察问题是否符合动态规划最明显的两个特征:最优子结构和重叠子问题 方便起见,以

动态规划求解最长公共子序列

动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解.与分治法不同的是,适合于用动态规划求解的问题,经分解得到的子问题往往不是互相独立的.若用分治法来解决这类问题,则分解得到的子问题数目太多,以至于最后解决原问题需要耗费指数时间.然而,不同子问题的数目常常只有多项式量级.在用分治法求解时,有些子问题被重复计算了许多次.如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,从而得到多项式

九度OJ 1042 Coincidence (动态规划求最长公共子序列)

题目1042:Coincidence 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:1689 解决:898 题目描述: Find a longest common subsequence of two strings. 输入: First and second line of each input case contain two strings of lowercase character a-z. There are no spaces before, inside or aft

【动态规划】最长公共子序列问题

题目描述: 给定两个字符串s1s2--sn和t1t2--tn.求出这两个字符串最长的公共子序列的长度.字符串s1s2--sn的子序列指可以表示为si1si2--sim(i1<i2<--<im)的序列.(n>=1,m<=1000) 输入: n=4 m=4 s="abcd" t="becd" 输出: 3 分析: 阶段就是n=0,1,2,3,4--时,m=0,1,2,3--1000时:状态就是对应字符相等还是不相等. 看该题是否符合,每个阶段