今天自学了动态规划算法,下面这是一个最长公共子序列的问题
解释可能不是很详细,大家尽力看吧。
最长公共子序列是指:给定两个序列X={x1,x2,…,xm}和Y={y1,y2,y3,…yn},找出X和Y的一个最长公共子序列。
暴力解的话需要穷举X的所有子序列,检查每个子序列是否也是Y的子序列,记录找到的最长公共子序列。X的子序列有2的m次方个,所以暴力解的时间复杂度为指数阶。我们可以用动态规划来解决这道题。
我们可以假设c[i][j]表示Xi和Yj的最长公共子序列长度。
可以得到
{ 0 i=0或者j=0
c[i][j]= {c[i-1][j-1]+1 i,j>0且xi=yj
{ max{c[i][j-1]+1} i,j>0且xi≠yj
自底向下构造最优值,并记录最优值和最有策略
i=1时:{x1}和{y1,y2,…,yn}中字符一一比较,按递归式求解并记录最长公共子序列长度。
i=2时:{x1}和{y1,y2,…,yn}中字符一一比较,按递归式求解并记录最长公共子序列长度。
…
i=m时:{x1}和{y1,y2,…,yn}中字符一一比较,按递归式求解并记录最长公共子序列长度。
上面只是求出最长公共序列的长度,并不知道最长公共子序列是什么
如果求出c[m][n]=5,表示Xm和Yn的最长公共子序列时5,那么我们可以反向追溯5是哪里来的。根据递推式
xi=yj时:c[i][j]=c[i-1][j-1]+1;
xi≠yj时:c[i][j]=max{c[i][j-1],c[i-1][j]};
那么c[i][j]来源一共有三个:c[i][j]=c[i-1][j-1]+1,c[i][j]=c[i][j-1],c[i][j]=c[i-1][j]。我们可以用一个辅助数组b[i][j]记录这3个来源:
c[i][j]=c[i-1][j-1]+1, b[i][j]=1;
c[i][j]=c[i][j-1] ,b[i][j]=2;
c[i][j]=c[i-1][j],, b[i][j]=3;
这样就可以根据b[m][n]反向追踪最长公共子序列,当b[i][j]=时,输出xi,当b[i][j]=2时,追踪c[i][j-1];当b[i][j]=3时,追踪c[i-][j],直到i=0或者j=0停止
如果不是很理解,可以试着画个二维数组的图试一遍
(1)最长公共子序列求解函数
首先计算两字符长度,然后i=1开始,s1[0]与s2中的每一个比较
如果字符相同,则公共序列长度为c[i-1][j-1]+1,并记录最优策略来源b[i][j]=1。
如果当前字符不相同,则公共序列长度为c[i][j-1] 和c[i-1][j]中的最大值,如果c[i][j-1]>=c[i-1][j],则最有策略来源b[i][j]=2;如果c[i][j-1]<c[i-1][j],则最优策略来源b[i][j]=3。直到i>len1,算法结束,这时从c[len1][len2]就是我们要的最长公共序列长度。代码如下:
for(i=1;i<=len1;i++)//控制a1序列
for(j=1;j<=len2;j++)//控制a2序列
{
if(a1[i-1]==a2[j-1])//如果当前字符相同,则公共子序列的长度为该
//字符前的最长公共序列+1;
{
c[i][j]=c[i-1][j-1]+1;
b[i][j]=1;
}
else
{
if(c[i][j-1]>=c[i-1][j])//找两者最大值,并记录最优策略
b[i][j]=2,c[i][j]=c[i][j-1];
else
{
c[i][j]=c[i-1][j];
b[i][j]=3;
}
}
}
(2)最优解函数输出
倒推法:如果b[i][j]=1,说明s1[i-1]=s2[j-1],那么我们就可以递归输出print(i-1,j-1);然后输出s1[i-1]。
如果b[i][j]=2,说明s1[i-1]≠s2[j-1]且最优解来源于c[i][j]=c[i][j-1],递归输出print(i,j-1)。
如果b[i][j]=3,说明s1[i-1]≠s2[j-1]且最优解来源于c[i][j]=c[i-1][j],递归输出print(i,j-1)。
当i0||j0时,递归结束。代码如下
void print(int i,int j)
{
if(i==0||j==0)//i=0或者j=0说明回溯完成
return ;
if(b[i][j]==1)
{
print(i-1,j-1);
printf("%c ",a1[i-1]);
}
else if(b[i][j]==2)
print(i,j-1);
else print(i-1,j);
}
总代码如下
#include<stdio.h>
#include<string.h>
int b[100][100],c[100][100];
char a1[100],a2[100];//定义两个字符串数组;
void print(int i,int j)
{
if(i==0||j==0)//i=0或者j=0说明回溯完成
return ;
if(b[i][j]==1)
{
print(i-1,j-1);
printf("%c ",a1[i-1]);
}
else if(b[i][j]==2)
print(i,j-1);
else print(i-1,j);
}
int main()
{
int len1,len2,i,j;
while(~scanf("%s%s",a1,a2))
{
len1=strlen(a1);
len2=strlen(a2);
for(i=1;i<=len1;i++)//控制a1序列
for(j=1;j<=len2;j++)//控制a2序列
{
if(a1[i-1]==a2[j-1])//如果当前字符相同,则公共子序列的长度为该
//字符前的最长公共序列+1;
{
c[i][j]=c[i-1][j-1]+1;
b[i][j]=1;
}
else
{
if(c[i][j-1]>=c[i-1][j])//找两者最大值,并记录最优策略
b[i][j]=2,c[i][j]=c[i][j-1];
else
{
c[i][j]=c[i-1][j];
b[i][j]=3;
}
}
}
print(len1,len2); //递归构成最长共共序列最优解
printf("\n");
}
return 0;
}