《算法导论》学习(十九)----动态规划之最长公共子序列(C语言)


前言

本文主要讲解了最长公共子序列的问题,对问题进行分析后给出了动态规划的方案,并且给出了详细的C语言代码。


一、问题描述

1.什么是公共子序列?

最长公共子串又名LCS(longest-common-subsequence)

(1)什么是子序列?

首先我们给出子序列的定义
给定一个序列 X = < x 1 , x 2 , . . . , x m > ,另一个序列 Z = < z 1 , z 2 , . . . , z k > 满足如下条件时称为 X 的子序列: 存在一个严格递增的 X 的下标序列 < i 1 , i 2 , . . . , i k > ,对所有 j = 1 , 2 , . . . , k ,满足 x i j = z j 。 例如: Z = < B , C , D , B > 是 X = < A , B , C , B , D , A , B > 的子序列 给定一个序列X=<x_1,x_2,...,x_m>,另一个序列Z=<z_1,z_2,...,z_k>满足如下条件时称为X的子序列:\\ 存在一个严格递增的X的下标序列<i_1,i_2,...,i_k>,对所有j=1,2,...,k,满足x_{i_j}=z_j。\\ 例如:Z=<B,C,D,B>是X=<A,B,C,B,D,A,B>的子序列 给定一个序列X=<x1,x2,...,xm>,另一个序列Z=<z1,z2,...,zk>满足如下条件时称为X的子序列:存在一个严格递增的X的下标序列<i1,i2,...,ik>,对所有j=1,2,...,k,满足xij=zj例如:Z=<B,C,D,B>X=<A,B,C,B,D,A,B>的子序列

(2)最长公共子序列

定义如下:
如果 z i 既是 X 的子序列,也是 Y 的子序列,现在有 X 和 Y 的子序列集合 Z = < z 1 , z 2 , . . . , z k > 那么集合 Z 里面最长的元素就是: X 和 Y 的最长公共子序列 ( L C S ) 如果z_i既是X的子序列,也是Y的子序列,现在有X和Y的子序列集合Z=<z_1,z_2,...,z_k>\\ 那么集合Z里面最长的元素就是:\\ X和Y的最长公共子序列(LCS) 如果zi既是X的子序列,也是Y的子序列,现在有XY的子序列集合Z=<z1,z2,...,zk>那么集合Z里面最长的元素就是:XY的最长公共子序列(LCS)

2.问题描述

给定两个序列:

  • X = < x 1 , x 2 , . . . , x m > X=<x_1,x_2,...,x_m> X=<x1,x2,...,xm>
  • Y = < y 1 , y 2 , . . . , y n > Y=<y_1,y_2,...,y_n> Y=<y1,y2,...,yn>

求解X和Y的最长公共子序列

二、问题分析

1.递归框架构建

(1)LCS特征分析

令 X = < x 1 , x 2 , . . . , x m > 和 Y = < y 1 , y 2 , . . . , y n > 为两个序列, Z = < z 1 , z 2 , . . . , z k > 为 X 和 Y 的任意 L C S 令X=<x_1,x_2,...,x_m>和Y=<y_1,y_2,...,y_n>为两个序列,Z=<z_1,z_2,...,z_k>为X和Y的任意LCS X=<x1,x2,...,xm>Y=<y1,y2,...,yn>为两个序列,Z=<z1,z2,...,zk>XY的任意LCS

  • 如果 x m = y n , 则 z k = x m = y n 且 Z k − 1 是 X m − 1 和 Y n − 1 的一个 L C S 如果x_m=y_n,则z_k=x_m=y_n且Z_{k-1}是X_{m-1}和Y_{n-1}的一个LCS 如果xm=yn,zk=xm=ynZk1Xm1Yn1的一个LCS
  • 如果 x m ≠ y n , 那么 z k ≠ x m 意味着 Z 是 X m − 1 和 Y 的一个 L C S 如果x_m\neq y_n,那么z_k\neq x_m意味着Z是X_{m-1}和Y的一个LCS 如果xm=yn,那么zk=xm意味着ZXm1Y的一个LCS
  • 如果 x m ≠ y n , 那么 z k ≠ y n 意味着 Z 是 X 和 Y n − 1 的一个 L C S 如果x_m\neq y_n,那么z_k\neq y_n意味着Z是X和Y_{n-1}的一个LCS 如果xm=yn,那么zk=yn意味着ZXYn1的一个LCS

(2)递归式

那么我们可以构建出递归式
c [ i , j ] = { 0  if  i = 0 或者 j = 0 c [ i − 1 , j − 1 ] + 1  if  i , j > 0 且 x i = y i m a x ( c [ i , j − 1 ] , c [ i − 1 , j ] )  if  i , j > 0 且 x i ≠ y i c[i,j]=\begin{cases} 0& \text{ if } i=0或者j=0 \\ c[i-1,j-1]+1& \text{ if } i,j>0且x_i=y_i \\ max(c[i,j-1],c[i-1,j])& \text{ if } i,j>0且x_i\neq y_i \end{cases} c[i,j]= 0c[i1,j1]+1max(c[i,j1],c[i1,j]) if i=0或者j=0 if i,j>0xi=yi if i,j>0xi=yi
其中c[i,j]代表长度i的X序列与长度为j的Y序列的LCS(最长公共子序列)
可以明显的发现:这个递归公式是有一层一层的子问题来构成的,前面的子问题的解对后面的子问题的解有帮助,同时子问题之间的关系是包含与被包含的关系,有子问题重叠的问题

2.动态规划方案

如果采用暴力求解方案,整个程序的运行时间会达到指数级别。因此我们采用动态规划方案。
采用一种自底向上的解决方案,先解决最底层的问题,解决之后将结果存储起来,之后进入到更上一层问题进行解决。而某一层问题的解决会用到第一层问题的解

这个过程中的关键是一个存储子问题解的一个矩阵。

3.最优解的构造

我们可以通过用一个存储矩阵来构造最优解

我们这个矩阵可以用来标记问题A的解决用了哪一个子问题的解,然后每一次问题都被记录。
最后我们可以从最上层的问题,也就是总问题开始,沿着记录一直得到每一层的具体解决方案。
最后得到最优解。

三、C语言代码

1.代码

#include<stdio.h>
#include<stdlib.h>
#include<time.h>



//XSIZE的大小是X序列的大小
#define XSIZE 6
//YSIZE的大小是y序列的大小
#define YSIZE 5
//这个是随机数的范围,代表0~25.
//那么'a'+LIM就是26个英文字母的随机了
#define LIM 26



int LCS_LENGTH(char *x,char *y,char loc[][YSIZE],char val[][YSIZE+1],int xsize,int ysize)
{
    
    
	//初始化循环变量
	int i=0;
	int j=0;
	int z=0;
	//初始换中间存储变量
	for(i=0;i<xsize;i++)
	{
    
    
		val[i][0]=0;
	}
	for(i=0;i<ysize;i++)
	{
    
    
		val[0][i]=0;
	}
	//开始自底向上的动态规划
	//按照递归公式进行逻辑控制
	for(i=1;i<=xsize;i++)
	{
    
    
		for(j=1;j<=ysize;j++)
		{
    
    
			if(x[i-1]==y[j-1])
			{
    
    
				val[i][j]=val[i-1][j-1]+1;
				loc[i-1][j-1]='d';
			}
			else if(val[i-1][j]>=val[i][j-1])
			{
    
    
				val[i][j]=val[i-1][j];
				loc[i-1][j-1]='u';
			}
			else
			{
    
    
				val[i][j]=val[i][j-1];
				loc[i-1][j-1]='l';
			}
		}
	}
	//返回最优解
	return val[xsize][ysize];
}



void print_LCS(char loc[][YSIZE],char *x,int xsize,int ysize)
{
    
    
	int i=xsize-1;
	int j=ysize-1;
	if(xsize==0||ysize==0)
	{
    
    
		return;
	}
	if(loc[i][j]=='d')
	{
    
    
		print_LCS(loc,x,i,j);
		printf("%c",x[i]);
	}
	else if(loc[i][j]=='u')
	{
    
    
		print_LCS(loc,x,i,j+1);
	} 
	else
	{
    
    
		print_LCS(loc,x,i+1,j);
	}
}



int main()
{
    
    
	int i=0;
	int j=0;
	int ML=0;
	char X[XSIZE];
	char Y[YSIZE];
	srand((unsigned)time(NULL));
	//随机生成x和y两个指定大小的序列
	for(i=0;i<XSIZE;i++)
	{
    
    
		X[i]=rand()%LIM+'a';
	}
	for(i=0;i<YSIZE;i++)
	{
    
    
		Y[i]=rand()%LIM+'a';
	}
	char b[XSIZE][YSIZE];
	char c[XSIZE+1][YSIZE+1];
		
	printf("X string is:\n \t%s\n",X);
	printf("Y string is:\n \t%s\n",Y);
	ML=LCS_LENGTH(X,Y,b,c,XSIZE,YSIZE);
//	该部分是对于存储矩阵的一个测试,因此注释掉
//	如有需要可以使用
//	for(i=0;i<XSIZE;i++)
//	{
    
    
//		for(j=0;j<YSIZE;j++)
//		{
    
    
//			printf("%5c",b[i][j]);
//		}
//		printf("\n");
//	}
//	for(i=0;i<=XSIZE;i++)
//	{
    
    
//		for(j=0;j<=YSIZE;j++)
//		{
    
    
//			printf("%5d",c[i][j]);
//		}
//		printf("\n");
//	}
	//打印出最长公共子序列
	print_LCS(b,X,XSIZE,YSIZE);
	printf("\n");
	//打印出LCS的长度
	printf("max length is %d\n",ML);
	return 0;
} 

总结

文章的不妥之处请各位读者包涵指正。
其它动态规划问题可以参考:
《算法导论》学习(十八)----动态规划之矩阵链乘(C语言)
《算法导论》学习(十七)----动态规划之钢条切割(C语言)

猜你喜欢

转载自blog.csdn.net/weixin_52042488/article/details/127193792