算法--- 动态规划之最长公共子序列问题

一.概念梳理

(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


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

最长公共子序列问题就是求序列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)。

 

试试使用动态规划算法!

二.动态规划

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

三.特征分析

1.最长公共子序列的结构

设序列X={Y1,Y2,...Yn}和Y={Y1,Y2,...Ym}的最长公共子序列为 Z= {Z1,Z2,...Zk},则

(1)若Xm=Yn, 则Zk = Xm = Yn ,且Zk-1 是 Xm-1 和Y n-1 的最长公共子序列。

(2)若Xm!=Yn ,且Zk!=Xm,则Z是Xm-1和Y的最长公共子序列。

(3)若Xm!=Yn ,且Zk!=Yn,则Z是X和Yn-1的最长公共子序列。

LCS问题具有最优子结构 

2.子问题的递归结构

    由最长公共子序列问题的最优子结构性质可知,要找出X={X1,X2,...Xn}和Y={Y1,Y2,...Ym}的最长公共子序列,可按以下方式递归进行。当Xm= Yn 时,找出Xm-1和Yn-1的最长公共子序列,然后在其尾部加上 Xm(=Yn)  即可得X和Y的最长公共子序列,当 Xm !=Yn时,必须解两个子问题,既找出 Xm 和 Yn 的一个最长公共子序列及 X 和 Yn-1的一个最长公共子序列。这两个公共子序列中较长者即为X和Y的最长公共子序列。

    由此递归结构容易看到,最长公共子序列问题具有子问题重叠性质。例如,在计算X和Y的最长公共子序列时,可能要计算X和Yn-1及Xm-1的最长公共子序列。而这两个子问题都包括一个公共子问题,既计算Xm-1和Yn-1的最长公共子序列。

首先建立子问题最优值的递归关系。用C[ i ][ j ] 记录序列Xi和 Yj的最长公共子序列的长度。其中,X={X1,X2,...Xn}和Y={Y1,Y2,...Ym}。当i=0 或 j=0 时,空序列是 Xi 和 Yj 的最长公共子序列。故此C[ i ][ j ] = 0. 其他情况下,由最优子结构性质可建立递归关系如下:


四.计算最优值

    直接利用递归容易写出计算C[ i ][ j ]的递归算法, 但其计算时间是随输入长度指数增长的,由于在所考虑的子问题空间中,共有0(m,n)个不同的 子问题,因此,用动态规划算法自顶向上计算最优值能提高算法效率。

    计算最长刚刚子序列长度的动态规划算法LCSlength以序列X = {X1,X2,...Xn}和Y = { Y1,Y2,... Ym} 作为输入,输入两个数组C 和B,其中C [ I ][ J ]储存Xi 和 Yj 的最长公共子序列的长度, B [ i ][ j ]记录 C [ i ][ j ]的值是由哪个子问题解得到的,这在构造最长公共子序列时要用到。问题的最优值, 既 X 和 Y 的最长公共子序列的长度记录 C [ M ] [ N  ]中。

void LCSlength(int m , int n, char *x, char *y ,int c[][MAX],int b[][MAX])
{
	int i,j;
	for(i=0;i<=m;i++)c[i][0]=0;
	for(j=0;j<=n;j++)c[0][j]=0;
	for(i =1;i<=m;i++)
	{
	for( j=1;j<=n;j++)
	{
		if(x[i-1]==y[j-1])
		{
			c[i][j]=c[i-1][j-1]+1;
			b[i][j]=1;
		}
			else if(c[i-1][j]>=c[i][j-1])
			{
				c[i][j]=c[i-1][j];
				b[i][j]=2;
			}
				else
			 {
				 c[i][j]=c[i][j-1];
			 	b[i][j]=3;
			 }
		}
		
	}
} 

 五.构造最长公共子序列

    由算法LCSlength计算得到的数组 B 可以用于快速构造序列 X = {X1,X2,...Xn}和Y = { Y1,Y2,... Ym} 的最长公共子序列。首先从      b [ m ] [ n ] 开始,依次在数组b中搜索。当在 b[ i ][ j ] = 1 时,表示Xi 和 Yj 的最长公共子序列是由 X i-1 和 Y j-1 的最长公共子序列在尾部加上 Xi 所得到的子序列。当 b [ i ] [ j ]  = 2 时,表示Xi 和 Y j  的最长公共子序列与 Xi-1 和Y i的最长公共子序列相同。

当 b [ i ] [ j ]  = 3 时,表示Xi 和 Y i  的最长公共子序列与 Xi-1 和Y j-1 的最长公共子序列相同。

    下面的算法LCS实现依据 b的内容打印出Xi 和Yj 的最长公共子序列。通过算法调用LCS 便可打印出序列X和Y的最长公共子序列。

void LCS(int i,int j , char *x,int b[][MAX])
{
	if(i==0||j==0)
		 return ;
	if(b[i][j]==1)
	{
		LCS(i-1,j-1,x,b);
		cout<<x[i-1]<<" ";
	}
	else if (b[i][j]==2)
	{
	LCS(i-1,j,x,b);
	}
		else
		 LCS(i,j-1,x,b); 
	
}
        可见我们在计算长度LCS的时候只要多记录一些信息,就可以利用这些信息恢复出一个最长公共子序列来。就好比我们在迷宫里走路,走到每个位置的时候记录下我们时从哪个方向来的,就可以从终点回到起点一样。
    

代码一:


#include<iostream>
#include<cstring>
#define MAX 50
using namespace std;
//1  x[m]==y[n]==z[k] z[k-1]是X[m-1] Y[n-1]的最长公共子序列
//2  x[m]!=y[n]  且z[k]!=x[m] Z是x[m-1]与Y的最长公共子序列	
//3  x[m]!=y[n]  且z[k]!= y[n] Z是X和y[n-1]的最长公共子序列 
void LCSlength(int m , int n, char *x, char *y ,int c[][MAX],int b[][MAX])
{
	int i,j;
	for(i=0;i<=m;i++)c[i][0]=0;
	for(j=0;j<=n;j++)c[0][j]=0;
	for(i =1;i<=m;i++)
	{
	for( j=1;j<=n;j++)
	{
		if(x[i-1]==y[j-1])
		{
			c[i][j]=c[i-1][j-1]+1;
			b[i][j]=1;
		}
			else if(c[i-1][j]>=c[i][j-1])
			{
				c[i][j]=c[i-1][j];
				b[i][j]=2;
			}
				else
			 {
				 c[i][j]=c[i][j-1];
			 	b[i][j]=3;
			 }
		}
		
	}
}
void LCS(int i,int j , char *x,int b[][MAX])
{
	if(i==0||j==0)
		 return ;
	if(b[i][j]==1)
	{
		LCS(i-1,j-1,x,b);
		cout<<x[i-1]<<" ";
	}
	else if (b[i][j]==2)
	{
	LCS(i-1,j,x,b);
	}
		else
		 LCS(i,j-1,x,b); 
	
}
int main()
{
	 char x[MAX]={"ABCBDAB"};
	 char y[MAX]={"BDCABA"} ; 
	 int b[MAX][MAX],c[MAX][MAX] ;
	 int m,n;
	 m=strlen(x);
	 n=strlen(y);
	 LCSlength(m,n,x,y,c,b);
	 LCS(m,n,x,b) ;
	return 0;
}

代码二:


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

代码一是让你在函数内定义好两个序列,然后输出最长公共子序列和长度。
代码二是让你输入两个序列,然后输出最长公共子序列和长度。
代码二是让你输入两个序列,然后输出最长公共子序列和长度。
代码三是让你输入三个序列,然后输出最长公共子序列的长度。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值