最长公共子序列

16 篇文章 0 订阅
8 篇文章 0 订阅

关于这类问题纠结了好几天了,看书也理解不了,自己好笨啊。上网搜搜这方面的博客,借助他人智慧来理解理解,大神太多了,挑了几篇精华且易于理解的博客加上自己的理解转载了这篇博客。

推荐两篇优秀也是我的转载来源的大家可根据自己理解能力挑选或者结合理解

http://blog.csdn.net/lz161530245/article/details/76943991

1.基本概念

首先需要科普一下,最长公共子序列(longest common sequence)和最长公共子串(longest common substring)不是一回事儿。什么是子序列呢?即一个给定的序列的子序列,就是将给定序列中零个或多个元素去掉之后得到的结果。什么是子串呢?给定串中任意个连续的字符组成的子序列称为该串的子串。给一个图再解释一下:


       如上图,给定的字符序列: {a,b,c,d,e,f,g,h},它的子序列示例: {a,c,e,f} 即元素b,d,g,h被去掉后,保持原有的元素序列所得到的结果就是子序列。同理,{a,h},{c,d,e}等都是它的子序列。
       它的字串示例:{c,d,e,f} 即连续元素c,d,e,f组成的串是给定序列的字串。同理,{a,b,c,d},{g,h}等都是它的字串。

        这个问题说明白后,最长公共子序列(以下都简称LCS)就很好理解了。
给定序列s1={1,3,4,5,6,7,7,8},s2={3,5,7,4,8,6,7,8,2},s1和s2的相同子序列,且该子序列的长度最长,即是LCS。
s1和s2的其中一个最长公共子序列是 {3,4,6,7,8}

2.动态规划

       求解LCS问题,不能使用暴力搜索方法。一个长度为n的序列拥有 2的n次方个子序列,它的时间复杂度是指数阶,太恐怖了。解决LCS问题,需要借助动态规划的思想。
       动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。


3.特征分析

       解决LCS问题,需要把原问题分解成若干个子问题,所以需要刻画LCS的特征。

      LCS具有最优子结构性质:假设X=<x1 ,  x2 ,   ..... , xm>和Y=<y1 , y2 , ....... , yn>为两个序列,Z=<z1 , z2 , ...... , zk>为X和Y的任意LCS。
       1. 如果 xm = yn ,则 zk=xm=yn 且 Z(1~k-1)是X(1~m-1)和Y(1~n-1)的一个LCS。

        2.如果 xm != yn ,则zk !=xm 意味着Z是X(1~m-1)和Y的一个LCS。
        3.如果 xm != yn ,  则zk !=yn 意味着Z是X和Y(1~n-1)的一个LCS。
 

(1) xm = yn

那么它们L(xm, yn)的最后一项一定是这个元素(zk)!
如果zk != xm,因为有xm=yn,那么可以将xm=yn追加到Z的末尾,得到X和Y的一个长度为k+1的公共子序列(即公共子序列Z变为<z1 , z1 , ..... , zk , z(k+1)>),这与我们假设的Z<z1 , z2 , ...... , zk>是X和Y的最长公共子序列的假设矛盾。因此必然有zk=xm=yn。Z(1~k-1)是X(1~m-1)和Y(1~n-1)的一个长度为k-1的公共子序列。我们要希望证明它是一个LCS。利用反证法,假设存在X(1~m-1)和Y(1~n-1)的一个长度大于k-1(len>k-1)的公共子序列W,
因为xm=yn所以将xm或yn追加到W的末尾会得到一个长度大于k(len+1>k-1+1,这个w的长度大于K),这与我们的假设长度为k的Z为X和Y的LCS相矛盾,因此Z(1~k-1)是X(1~m-1)和Y(1~n-1)的一个LCS。


(2)如果zk != xm,那么Z是X(1~m-1)和Y的一个公共子序列。如果存在X(1~m-1)和Y的一个长度大于K的公共子序列W,那么W也是X和Y的公共子序列,这与长度为K的Z是X和Y的最长公共子序列的假设矛盾(W的长度大于Z,Z就不是最长公共子序列了)。
 (3)与情况(2)对称。

       有些同学,一看性质就容易晕菜,所以我给出一个图来让这些同学理解一下:


       以我在第1小节举的例子(S1={1,3,4,5,6,7,7,8}和S2={3,5,7,4,8,6,7,8,2}),并结合上图来说:

       假如S1的最后一个元素 与 S2的最后一个元素相等,那么S1和S2的LCS就等于 {S1减去最后一个元素} 与 {S2减去最后一个元素} 的 LCS  再加上 S1和S2相等的最后一个元素。

       假如S1的最后一个元素 与 S2的最后一个元素不等(本例子就是属于这种情况),那么S1和S2的LCS就等于 : {S1减去最后一个元素} 与 S2 的LCS, {S2减去最后一个元素} 与 S1 的LCS 中的最大的那个序列。

4.递归公式

        第3节说了LCS的特征,我们可以发现,在求X=<x1 , x2 , ..... , xm>和Y=<y1 , y2 , ..... , yn>的一个LCS是,我们需要求解一个或两个子问题。
如果xm=yn,我们应求解X(1~m-1)和Y(1~n-1)的一个LCS。将xm = yn追加到这个LCS的末尾,就达到X和Y的一个LCS。

         假设我们用c[i,j]表示Xi和Yj的LCS的长度(直接保存最长公共子序列的中间结果不现实,需要先借助LCS的长度)。其中X = {x1 ... xm},Y ={y1...yn}。可得递归公式如下:递归公式如下:

         

5.计算LCS的长度

       这里我不打算贴出相应的代码,只想把这个过程说明白。还是以s1={1,3,4,5,6,7,7,8},s2={3,5,7,4,8,6,7,8,2}为例。我们借用《算法导论》中的推导图:


         图中的空白格子需要填上相应的数字(这个数字就是c[i,j]的定义,记录的LCS的长度值)。填的规则依据递归公式,简单来说:如果横竖(i,j)对应的两个元素相等,该格子的值 = c[i-1,j-1] + 1。如果不等,取c[i-1,j] 和 c[i,j-1]的最大值。首先初始化该表:

         

          然后,一行一行地从上往下填:

         

          S1的元素3 与 S2的元素3 相等,所以 c[2,1] = c[1,0] + 1。继续填充:

          

            S1的元素3 与 S2的元素5 不等,c[2,2] =max(c[1,2],c[2,1]),图中c[1,2] 和 c[2,1] 背景色为浅黄色。

            继续填充:

            

            

             

               中间几行填写规则不变,直接跳到最后一行:

              

                至此,该表填完。根据性质,c[8,9] = S1 和 S2 的 LCS的长度,即为5。

#include<iostream>
#include<cstring>
using namespace std;
int c[1005][1005];
int main()
{
	int n,len1,len2;
	string str1,str2;
	cin>>n;
	while(n--)
	{
		cin>>str1>>str2;
		len1=str1.size();
		len2=str2.size();
		memset(c,0,sizeof(c));
		for(int i=1;i<=len1;i++)
		for(int j=1;j<=len2;j++)
		{
			if(str1[i-1]==str2[j-1])
			c[i][j]=c[i-1][j-1]+1;
			else
			{
				c[i][j]=max(c[i-1][j],c[i][j-1]);
			}
		}
		cout<<c[len1][len2]<<endl;
	}
 } 

下面两个是使用多指针申请动态数组,头一次见待日后研究研究

#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;  
}  

6.构造LCS

       本文S1和S2的最LCS并不是只有1个,本文并不是着重讲输出两个序列的所有LCS,只是介绍如何通过上表,输出其中一个LCS。

       我们根据递归公式构建了上表,我们将从最后一个元素c[8][9]倒推出S1和S2的LCS。

       c[8][9] = 5,且S1[8] != S2[9],所以倒推回去,c[8][9]的值来源于c[8][8]的值(因为c[8][8] > c[7][9])。

       c[8][8] = 5,  且S1[8] = S2[8], 所以倒推回去,c[8][8]的值来源于 c[7][7]。

       以此类推,如果遇到S1[i] != S2[j] ,且c[i-1][j] = c[i][j-1] 这种存在分支的情况,这里请都选择一个方向(之后遇到这样的情况,也选择相同的方向)。

       第一种结果为:

       

          这就是倒推回去的路径,棕色方格为相等元素,即LCS = {3,4,6,7,8},这是其中一个结果。

          如果如果遇到S1[i] != S2[j] ,且c[i-1][j] = c[i][j-1] 这种存在分支的情况,选择另一个方向,会得到另一个结果。

          

           即LCS ={3,5,7,7,8}。

#include<iostream>
#include<cstring>
using namespace std;
int c[1005][1005],b[1005][1005];
string str1,str2;
void LCS(int len1,int len2)
{
	for(int i=1;i<=len1;i++)
		for(int j=1;j<=len2;j++)
		{
			if(str1[i-1]==str2[j-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;
			}
		}
}
}
void printLCS(int i,int j)
{
	if(i==0||j==0)
	return ;
	if(b[i][j]==0) 
	{
	printLCS(i-1,j-1);
	cout<<str1[i-1];	
	}
	else
	{
		if(b[i][j]==1)
		printLCS(i-1,j); 
		else
		printLCS(i,j-1);
	}

}
int main()
{
	int n,len1,len2;
	cin>>n;
	while(n--)
	{
		cin>>str1>>str2;
		len1=str1.size();
		len2=str2.size();
		memset(c,0,sizeof(c));
		memset(b,0,sizeof(b));
		LCS(len1,len2);
		printLCS(len1,len2);
		cout<<endl;
		cout<<c[len1][len2]<<endl;
	}
 } 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【1】项目代码完整且功能都验证ok,确保稳定可靠运行后才上传。欢迎下载使用!在使用过程中,如有问题或建议,请及时私信沟通,帮助解答。 【2】项目主要针对各个计算机相关专业,包括计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网等领域的在校学生、专业教师或企业员工使用。 【3】项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 【4】如果基础还行,或热爱钻研,可基于此项目进行二次开发,DIY其他不同功能,欢迎交流学习。 【注意】 项目下载解压后,项目名字和项目路径不要用中文,否则可能会出现解析不了的错误,建议解压重命名为英文名字后再运行!有问题私信沟通,祝顺利! 基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值