文章目录
前言
本文主要讲解了最长公共子序列的问题,对问题进行分析后给出了动态规划的方案,并且给出了详细的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的子序列,现在有X和Y的子序列集合Z=<z1,z2,...,zk>那么集合Z里面最长的元素就是:X和Y的最长公共子序列(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>为X和Y的任意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=yn且Zk−1是Xm−1和Yn−1的一个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意味着Z是Xm−1和Y的一个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意味着Z是X和Yn−1的一个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[i−1,j−1]+1max(c[i,j−1],c[i−1,j]) if i=0或者j=0 if i,j>0且xi=yi if i,j>0且xi=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语言)