《算法导论》动态规划—最长公共子序列(不连续)--c语言实现

1、基本概念

  一个给定序列的子序列就是该给定序列中去掉零个或者多个元素的序列。形式化来讲就是:给定一个序列X={x1,x2,……,xm},另外一个序列Z={z1、z2、……,zk},如果存在X的一个严格递增小标序列1,i2……,ik>,使得对所有j=1,2,……k,有xij = zj,则Z是X的子序列。例如:Z={B,C,D,B}是X={A,B,C,B,D,A,B}的一个子序列,相应的小标为<2,3,5,7>。从定义可以看出子序列直接的元素不一定是相邻的。

公共子序列:给定两个序列X和Y,如果Z既是X的一个子序列又是Y的一个子序列,则称序列Z是X和Y的公共子序列。例如:X={A,B,C,B,D,A,B},Y={B,D,C,A,B,A},则序列{B,C,A}是X和Y的一个公共子序列,但不不是最长公共子序列。因为它的长度等于3,而子序列{B,C,A,B}其长度等于4,所以序列{B,C,B,A}才是X和Y的一个最长公共子序列。

最长公共子序列(LCS)问题描述:给定两个序列X={x1,x2,……,xm}和Y={y1,y2,……,yn},找出X和Y的最长公共子序列。

2、动态规划解决过程

1)描述一个最长公共子序列

  如果序列比较短,可以采用蛮力法枚举出X的所有子序列,然后检查是否是Y的子序列,并记录所发现的最长子序列。如果序列比较长,这种方法需要指数级时间,不切实际。

  LCS的最优子结构定理设X={x1,x2,……,xm}和Y={y1,y2,……,yn}为两个序列,并设Z={z1、z2、……,zk}为X和Y的任意一个LCS,则:

     (1)如果xm=yn,那么zk=xm=yn,而且Zk-1是Xm-1和Yn-1的一个LCS。

  (2)如果xm≠yn,那么zk≠xm蕴含Z是是Xm-1和Yn的一个LCS。

  (3)如果xm≠yn,那么zk≠yn蕴含Z是是Xm和Yn-1的一个LCS。

  定理说明两个序列的一个LCS也包含两个序列的前缀的一个LCS,即LCS问题具有最优子结构性质。

2)一个递归解

  根据LCS的子结构可知,要找序列X和Y的LCS,根据xm与yn是否相等进行判断的,如果xm=yn则产生一个子问题,否则产生两个子问题。设C[i,j]为序列Xi和Yj的一个LCS的长度。如果i=0或者j=0,即一个序列的长度为0,则LCS的长度为0。LCS问题的最优子结构的递归式如下所示:

3)计算LCS的长度

  采用动态规划自底向上计算解。书中给出了求解过程LCS_LENGTH,以两个序列为输入。将计算序列的长度保存到一个二维数组C[M][N]中,另外引入一个二维数组B[M][N]用来保存最优解的构造过程。M和N分别表示两个序列的长度。该过程的伪代码如下所示:

复制代码
 1 LCS_LENGTH(X,Y)
 2     m = length(X);
 3     n = length(Y);
 4     for i = 1 to m
 5       c[i][0] = 0;
 6     for j = 1 to n
 7       c[0][j] = 0;
 8     for i=1 to m
 9        for j=1 to n
10            if x[i] = y[j]
11               then c[i][j] = c[i-1][j-1]+1;
12                    b[i][j] = '\';
13            else if c[i-1][j] >= c[i][j-1]
14                   then c[i][j] = c[i-1][j];
15                        b[i][j] = '|';
16                   else
17                        c[i][j] = c[i][j-1];
18                        b[i][j] = '-';
19 return c and b
复制代码

由伪代码可以看出LCS_LENGTH运行时间为O(mn)。

4)构造一个LCS

  根据第三步中保存的表b构建一个LCS序列。从b[m][n]开始,当遇到'\'时,表示xi=yj,是LCS中的一个元素。通过递归即可求出LCS的序列元素。书中给出了伪代码如下所示:

复制代码
1 PRINT_LCS(b,X,i,j)
2     if i==0 or j==0
3         then return
4     if b[i][j] == '\'
5         then PRINT_LCS(b,X,i-1,j-1)
6              print X[i]
7      else if b[i][j] == '|'
8                 then PRINT_LCS(b,X,i-1,j)
9              else PRINT_LSC(b,X,i,j-1)
复制代码

3.算法分析

时间复杂度:构建矩阵我们花费了O(MN)的时间,回溯时我们花费了O(M+N)的时间,两者相加最终我们花费了O(MN)的时间。

空间复杂度:构建矩阵我们花费了O(MN)的空间,标记函数也花费了O(MN)的空间,两者相加最终我们花费了O(MN)的空间。


4.编程实现

采用c语言实现上述过程。

/**********************************************************************
最大公共子序列:
最大公共子序列长度:int Lcs_length( char *str1, char *str2, int **c, int **b)
输出最大公共子序列:void Print_Lcs( char *str, int **b, int i, int j)
输出最大公共子序列长度及最大公共子序列:void Find_Lcs( char *str1, char *str2)

时间复杂度:构建矩阵花费了O(MN)的时间,回溯时花费了O(M+N)的时间,两者相加最终花费了O(MN)的时间。

空间复杂度:构建矩阵花费了O(MN)的空间,标记函数也花费了O(MN)的空间,两者相加最终花费了O(MN)的空间。
************************************************************************/

#include 
#include 
#include 

#define EQUAL	1	//EQUAL表示c[i][j]是由c[i-1][j-1]+1来的=====此时两个序列有相同的字符
#define UP		2	//UP表示c[i][j]是由c[i-1][j]来的============此时两个序列没有相同的字符
#define LEFT	3	//LEFT表示c[i][j]是由[ci][j-1]来的==========此时两个序列没有相同的字符


/**************************************************************
函数:int Lcs_length( char *str1, char *str2, int **c, int **b)
输入:	str1:	待比较字符串1
		str2:	待比较字符串2
		**c:	储存最大公共子序列长度
		**b:	储存最大公共子序列检索路径

返回值:str1和str2最大公共子序列
时间复杂度:O(mn)
空间复杂度:O(mn)
***************************************************************/
int Lcs_length( char *str1, char *str2, int **c, int **b)
{
	int len1 = strlen(str1),
		len2 = strlen(str2);
	int i,j;
	for( i = 1; i <= len1; i++)
		c[i][0] = 0;
	for ( j = 0; j <= len2; j++)
		c[0][j] = 0;
	for(  i = 1; i <= len1; i++)
		for( j = 1; j <= len2; j++)
		{	
			/*******************************
			使用i-1和j-1
			算法导论书上写的是比较str1[i]和str[j],但是算法导论书上的两个序列下标是由1开始的
			这里使用i-1以及j-1是由于数组的下标从0开始
			********************************/
			if( str1[i-1] == str2[j-1] )											
			{
				c[i][j] = c[i-1][j-1] + 1;
				b[i][j] = EQUAL; 
			}
			else if (c[i-1][j] >= c[i][j-1])
				{
					c[i][j] = c[i-1][j];
					b[i][j] = UP;
				}
			else 
				{
					c[i][j] = c[i][j-1];
					b[i][j] = LEFT;
				}
		}
		return c[len1][len2];
}

/**************************************************************
函数:void Print_Lcs( char *str, int **b, int i, int j
		str:	待比较字符串1
		**b:	储存最大公共子序列检索路径
		i:		待比较字符串1的长度
		j:		待比较字符串2的长度

返回值:无
打印值:输出字符串1和字符串2的最长公共子序列
时间复杂度:O(m+n)
空间复杂度:O(m+n)
***************************************************************/
void Print_Lcs( char *str, int **b, int i, int j)
{
	if( i == 0 || j == 0)
		return;
	if( b[i][j] == EQUAL)
	{
		Print_Lcs(str, b, i - 1, j - 1);
		printf("%c ", str[i-1]);
	}
	else if ( b[i][j] == UP )
		Print_Lcs(str, b, i - 1, j);
	else 
		Print_Lcs(str, b, i , j - 1);
}

/**************************************************************
函数:void Find_Lcs( char *str1, char *str2)
		str1:	待比较字符串1
		str2:	待比较字符串2
返回值:无
打印值:输出最大公共子序列长度以及最大公共子序列
时间复杂度:O(mn)
空间复杂度:O(mn)
***************************************************************/
void Find_Lcs( char *str1, char *str2)
{
	int i,j,length;
	int len1 = strlen(str1),
		len2 = strlen(str2);
	//申请二维数组
	int **c = (int **)malloc(sizeof(int*) * (len1 + 1));
	int **b = (int **)malloc(sizeof(int*) * (len1 + 1));
	for( i = 0; i<= len1; i++ )	这个等号之前没加,导致内存泄漏
	{
		c[i] = (int *)malloc(sizeof(int) * (len2 + 1));
		b[i] = (int *)malloc(sizeof(int) * (len2 + 1));
	}

	//将c[len1][len2]和b[len1][len2]初始化为0
	for ( i = 0; i<= len1; i++)
		for( j = 0; j <= len2; j++)
		{
			c[i][j] = 0;
			b[i][j] = 0;
		}
	
	//计算LCS的长度
	length = Lcs_length(str1, str2, c, b);
	printf("The number of the Longest-Common-Subsequence is %d\n", length);
	
	//利用数组b输出最长子序列
	printf("The Longest-Common-Subsequence is: ");
	Print_Lcs(str1, b, len1, len2);
	printf("\n");

	//动态内存释放
	for ( i = 0; i <= len1; i++)
	{
		free(c[i]);
		free(b[i]);
	}
	free(c);
	free(b);
}

int main()
{
	char x[10] = "abcdefghi";
	char y[10] = "bdegihbjk";
	Find_Lcs(x,y);
	system("pause");
}


 
  
程序测试结果如下所示:


你可能感兴趣的:(算法导论)