一.题目:求两个字符序列的最长公共字符子序列。给定两个字符串,求解这两个字符串的最长公共子序列(Longest Common Sequence)。比如字符串1:BDCABA;字符串2:ABCBDAB,则这两个字符串的最长公共子序列长度为4。
二.解法1:递归解法
1.设计思路:分析两个字符串的比较规律,可以发现字符串在进行比较的时候有三种情况:A.str1[i+1]与str2[j]比较;B.str1[i]与str2[j+1]比较;C.str1[i+1]与str2[j+1]比较(两个字符匹配的情况);递归的解法,就是蛮力的去遍历两个字符串比较的所有可能解;如果str1[i]=str2[j]的时候,就需要同时移动两个数组str1[i+1],str2[j+1];如果不相等时,需要考虑去移动str1[i+1]或者str2[j+1],在两个之间选择最大的那一个;根据三种情况去递归,当两个str1[i]=str2[j+1]的时候,就把最大公共子序列的长度+1;
2.代码:
/*最长公共子序列的长度-递归*/
#include<stdio.h>
#include<string.h>
char a[30],b[30];
int lena,lenb; //分别保存数组a、b的长度
int LCS(int i,int j); //两个参数分别表示数组a的下标和数组b的下标
int main()
{
strcpy(a,"ABCBDAB");
strcpy(b,"BDCABA");
lena=strlen(a);
lenb=strlen(b);
printf("最大公共子序列的长度为:%d\n",LCS(0,0));
return 0;
}
int LCS(int i,int j)
{
bool flag=false;
if(i>=lena || j>=lenb)
return 0;
if(a[i]==b[j])
{
return 1+LCS(i+1,j+1); //相等时两个数组同时往后移动
}
else //遍历解空间树
{
return LCS(i+1,j)>LCS(i,j+1)? LCS(i+1,j):LCS(i,j+1);//遍历找到移动哪一个数组的最大收益
}
}
三.解法2:动态规划
1.设计思路:使用动态规划的思路求解,dp[i][j]数组记录最长公共子序列的长度;自底向上的思考,可以把大的问题,变成小的问题:如果A[i]=B[j],那么有序列的最大长度dp[i][j]=dp[i-1][j-1]+1;如果A[i]!=B[j],那么dp[i][j]=dp[i-1][j]或者dp[i][j]=dp[i][j-1],选择其中最大的一个;两层循环,依次把str1[i]与str2[j]中的每一个元素进行比较,得到以str1[i]为结尾的最大公共子序列的长度;最后输出dp[i][j]最优值,既最大公共子序列的长度。
2.代码:
/*动态规划之最大子序列*/
#include <stdio.h>
int main()
{
char A[7]={'A','B','C','B','D','A','B'};
char B[6]={'B','D','C','A','B','A'};
int dp[8][7]; //dp数组记录最长公共子序列的长度
for(int i=0;i<7;i++) //边界赋值为0
{
dp[i][0]=0;
}
for(int i=0;i<8;i++)
{
dp[0][i]=0;
}
printf("test1=%d\n",dp[6][7]);
for(int i=1;i<=7;i++)
{
for(int j=1;j<=6;j++)
{
if(A[i-1]==B[j-1]) //如果相等就dp[i][j]=dp[i-1][j-1]+1;
{
dp[i][j]=dp[i-1][j-1]+1;
}
else{
if(dp[i-1][j]>dp[i][j-1])
{
dp[i][j]=dp[i-1][j]; //取两者之间较大者;局部的最优值
}
else{
dp[i][j]=dp[i][j-1];
}
}
}
}
/*char str[100]; //记录公共的字符
int i=7,j=6;
int count=0;
while(i>0&&j>0)
{
if(dp[i][j]==dp[i-1][j]) //往上遍历
{
i--;
}
else if(dp[i][j]==dp[i][j-1]) //往左遍历
{
j--;
}
else{
str[count++]=A[i-1];
i--;
j--;
}
}*/
for(int i=0;i<8;i++)
{
for(int j=0;j<7;j++)
{
printf(" %d ",dp[i][j]);
}
printf("\n");
}
/*for(int i=count-1;i>=0;i--)
{
printf(" %c ",str[i]);
}
printf("\n");*/
printf("最大子序列的长度为:%d\n",dp[7][6]);
}
四.解法3:改进动态规划(空间压缩)
1.设计思路:由于只是求解最长公共子序列的长度,不需要跟踪最优值的路径,所以可以使用一个一维数组dp[i]去保存以i为结尾的最长公共子序列的长度;在动态规划规划中,dp[i]数组的更新和左边、上边、左对角相关,在使用一维数组的时候,就需要一个pre去存储左对角的值,也就是保存还未改变的dp[i]的值;然后在二维数组中的dp[i][j-1],dp[i-1][j],对应于一维数组中是dp[i-1],dp[i];两层for循环,在内部每一次循环的过程中,首先保存未更新的t=dp[j],然后把对应的值更新到dp[j],同时pre=t,保存为更新的dp[j],也就是二维数组中左对角dp[i-1][j-1]的值;最终输出数组的最后一个数dp[n]即可;
2.代码:
/*最长公共子序列的长度--空间压缩*/
#include <stdio.h>
#define N 6
int main()
{
char A[7]={'A','B','C','B','D','A','B'};
char B[6]={'B','D','C','A','B','A'};
int dp[N+1]; //dp数组记录最长公共子序列的长度 ,+1方便第一个元素赋值为0
for(int i=0;i<=N;i++) //边界赋值为0
{
dp[i]=0;
}
for(int i=1;i<=7;i++)
{
int pre=0; //保存为更新的dp[j],对应于二维dp[i-1][j-1]的值
for(int j=1;j<=6;j++)
{
int t=dp[j];
if(A[i-1]==B[j-1]) //如果相等就dp[i][j]=dp[i-1][j-1]+1;
{
dp[j]=pre+1; //此处pre=dp[i-1][j-1]
}
else{
if(dp[j]>dp[j-1])
{
dp[j]=dp[j]; //取两者之间较大者;局部的最优值
}
else{
dp[j]=dp[j-1];
}
}
pre=t;
}
}
/*char str[100]; //记录公共的字符
int i=7,j=6;
int count=0;
while(i>0&&j>0)
{
if(dp[i][j]==dp[i-1][j]) //往上遍历
{
i--;
}
else if(dp[i][j]==dp[i][j-1]) //往左遍历
{
j--;
}
else{
str[count++]=A[i-1];
i--;
j--;
}
}
for(int i=0;i<8;i++)
{
for(int j=0;j<7;j++)
{
printf(" %d ",dp[i][j]);
}
printf("\n");
}
for(int i=count-1;i>=0;i--)
{
printf(" %c ",str[i]);
}
printf("\n");*/
printf("最大子序列的长度为:%d\n",dp[N]);
}
五.解法4:转换法
1.设计思路:由于两个序列的下标是严格递增的,所以可以把str1中的每一个元素的位置记录下来,求出,str2中每一个元素在str1中出现的位置。此时str2中每个元素在str2中的出现的位置是顺序,然后把每个元素出现的位置序列,进行降序排列。进行降序的目的是防止组成的最长公共子序列中同一个元素重复出现。降序以后,把这些序列组成一个序列,求这个序列的最长递增子序列的长度,就是最长公共子序列的长度。这个过程是把str2在str1中的出现的元素找出来,然后按照一个元素选择一次,并且,递增(因为原来序列的下标是递增的,所以选择的时候,每一个的元素的下标也是递增)。组成的新的最长子序列的长度即是原来两个序列的最长公共子序列的长度。
2.代码:
/*转换发之最长公共子序列的长度*/
#include <stdio.h>
#define N 7
#define M 6
int record[N][M]; //record[i][j]存储B中元素i在A中出现的位置
int merge[1000]; //把每一个元素出现的位置降序以后,存储在merge[]中
int main()
{
char A[N]={'A','B','C','B','D','A','B'};
char B[M]={'B','D','C','A','B','A'};
int temp=0; //记录merge[]中实际元素的个数
for(int i=0;i<N;i++) //求出B中每个元素在A中出现的位置
{
int count=0;
for(int j=0;j<M;j++)
{
if(B[i]==A[j]) //A中的元素和B中的每一个元素进行比较,是否匹配
{
record[i][count++]=j;
}
}
//printf("i=%d\n",i);
record[i][count]=-1;
}
printf("B中每个元素在A中的位置如下:\n");
for(int i=0;i<N;i++) //输出每个元素位置的记录
{
int among=0;
for(int j=0;j<M&&record[i][j]!=-1;j++)
{
among++;
printf(" %d ",record[i][j]);
}
for(int x=among-1;x>=0;x--) //把元素的位置降序,并且存储在merge[]中
{
merge[temp++]=record[i][x];
}
printf("\n");
}
printf("降序排列的元素位置如下:");
for(int i=0;i<temp;i++)
{
printf(" %d ",merge[i]);
}
printf("\n");
/*接下来使用动态规划求解最长递增子序列的长度*/
int dp[100]; //记录以i为起始位置的最长不下降子序列的长度
int max; // 纪录当前元素能够构成的最大不下降子序列的长度
int count; //记录最长不下降子序列的长度和起始位置
for(int i=temp-1;i>=0;i--) //自底向上的计算每一个元素和后面元素可以构成的最长不下降子序列
{
max=0;
for(int j=i+1;j<temp;j++) //遍历array[i]后面的其他元素构成的最长不下降子序列的长度
{
if(merge[i]<merge[j]) //可以构成不下降子序列
{
if(dp[j]>max) //找到当前元素和后面元素构成的最大不下降子序列的长度
{
max=dp[j];
}
}
}
dp[i]=max+1; //当前元素与后面元素构成的最大不下降子序列的长度需要加上此元素本身
}
for(int i=0;i<N;i++) //寻找最大不下降子序列的起始位置和其长度
{
if(dp[i]>count)
{
count=dp[i];
}
}
printf("最长公共子序列的长度为:%d\n",count);
return 0;
}
六.运行结果:
七.总结:
比较三种解法,发现还是动态规划更加适合解决此题。递归的算法需要去考虑是移动数组一还是移动数组二,而在动态规划的算法中,只需要一次的去比较以当前元素为结尾的最长公共子序列的长度,因为记录了前面元素的比较结果,所以在当前进行比较的时候,可以大大的提高效率;因为此题是求解最优解,而不是最优值,所以可以把动态规划的二维数组,优化为一维数组,可以进一步的节约空间。解法四是一种奇妙的方法,将原来的问题转换成了求最长不下降子序列的长度。