前言
鸡爸爸对自家的三儿子产生了怀疑,因为他没有继承自己优良的基因,于是鸡爸爸带着他去找了鉴定中心,小鸡内心忐忑不安,因为爸爸看上去很生气,对他没有了以往的温柔。此时一切只能交给鉴定结果了…
经过缴费抽样后,有了以下结果,S1是鸡爸爸的基因序列,S2是小鸡的基因序列。
S1=ACCGGTCGAGTGCGCGGAAGCCGGCCGAA...
S2=GTCGTTCGGAATGCCGTTGCTCTGTAAA...
肉眼还是无法给出结果,于是,鉴定人员拿出来 神秘的“计算机”。求出如下的最长公共子序列,经过计算,相似度居然高达99.99%。听到医生的鉴定结果,小鸡留下了争气的眼泪,鸡爸爸也逐渐平息了怒火,最后鸡爸爸牵着小鸡开心的走上了回家的路…
S3=GTCGTCGGAAGCCGGCCGAA...
在测序之前,问题是生物学问题
测序之后,问题变成了计算机问题
那最长公共子序列我们应该怎么来算呢!
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
一、问题描述
★ 一个给定序列的子序列是在该序列中删去若干元素后得到的序列
★给定2个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列
★ 公共子序列中长度最长的公共子序列叫做最长公共子序列
★ 最长公共子序列(Longest Common Subsequence, LCS)问题可 以叙述为:
- 给定2个序列X={x1,…,xm}和Y={y1,…,yn},要求找出X和Y的一个最长公共子序列
例:
X = {A, B, C, B, D, A, B}
Y = {B, D, C, A, B, A}
LCS = ﹛B D A B﹜ 还有﹛ B C A B ﹜和﹛B C B A﹜
二、问题解决
1.穷举搜索法
❆ 假设x的长度为m,y的长度为y,对于x的每一个子序列,检查他是否是y的子序列,即为两者的公共子序列
❆ 选出所有结果的最长子序列
❆ x={1,2,…,m} 共有 2m个公共子序列
❆ 穷举法的时间复杂度为: T(n)=(2m*n)
2.动态规划法(填表法)
使用动态规划必须满足两个条件,最优子结构与重叠子问题。
最优子结构:
- 两个序列的最长公共子序列包含了两个序列的前缀的最长公共子序列
- 最长公共子序列包含最优子结构性质
由最长公共子序列问题的最优子结构性质可知,要找出X={x1,…,xm}和Y={y1,…,yn}的一个最长公共子序列,可按以下方式递归地进行:
- 当Xm = Yn时,找出X(m-1)和Y(n-1)的最长公共子序列,然后在其尾部加上Xm(=Yn)即可得X和Y的一个最长公共子序列
- Xm ≠ Yn,即找出X(m-1)和Y的最长公共子序列或者X和Y(n-1)的最长公共子序列,较长者即为两个序列的LCS
综上有了如下的递归公式:
重叠子问题:
分析上面的递归公式,不难看出问题存在重叠子问题,并且递归高度为X序列的长度和Y序列的长度之和m+n
三、算法实现
1.递归实现
根据上面的递归方程,得到如下的递归代码:
private static int LCS(char[] x, char[] y, int i, int j) {
if(i>=0&&j>=0) {//递归结束条件
if(x[i]==y[j]) {
c[i][j] = LCS(x,y,i-1,j-1)+1;
}else {
c[i][j] = Math.max(LCS(x,y,i-1,j),LCS(x,y,i,j-1));
}
return c[i][j];
}else {
return 0;
}
}
}
如上面递归树分析得到的一样,递归中同一问题求解了多次,我们可以用一个二维数组保存我们求解过的问题,事实上我们的 c数组已经保存下来了,我们直接使用就可以了
由此可得下面的备忘录法:
2.备忘录法
private static int LCS(char[] x, char[] y, int i, int j) {
if(i>=0&&j>=0) {//递归结束条件
if(c[i][j]==0) {//计算之前先判断是否已经计算过
if(x[i]==y[j]) {
c[i][j] = LCS(x,y,i-1,j-1)+1;
}else {
c[i][j] = Math.max(LCS(x,y,i-1,j),LCS(x,y,i,j-1));
}
}
return c[i][j];
}else {
return 0;
}
}
}
备忘录的时间复杂度为 O(m×n);比暴力求解O(n×2m)好很多了,n和m分别为Y序列和X序列的长度。指数增长还是过于恐怖了。
3.动态规划法
填一张表(二维数组),然后输出c数组最右下位置的值,即最大公共子序列的长度。
X = {A, B, C, B, D, A, B}
Y = {B, D, C, A, B, A}
首先初始化数组c
for(int i=0;i<=m;i++) c[i][0]=0;
for(int j=0;j<=n;j++) c[0][j]=0;
从1,1位置开始比较:
- 如果X序列的第一个位置和Y序列的第一个位置相等,c[1][1] = c[0][0]+1;
- 如果不相等,那么c[1][1]等于他上边或左边最大的值,可以看到c[1][1]上面的c[0][1]和左边的c[1][0]都等于0,所以c[1][1]=0;
不相等时为什么要取左边或上边最大的值,看c[3][4,],此时X[3]不等于Y[4] 如果我们取c[3][4]等于上面的1,意思是X1 = {A, B, C, B} ,Y1 = {B, D} ,Y1加上{C} 后其最大公共子序列长度为1,显然X2 = {A, B, C},Y2 = {B, D,C}有最大公共子序列长度为2。
所以应该取左边或上边最大的值。
填表的过程:
private static void lcsLength(char[] x, char[] y,int[][] b) {
int m = x.length;
int n = y.length;
//c = new int[m+1][n+1];
for(int i=0;i<=m;i++) c[i][0]=0;
for(int j=0;j<=n;j++) c[0][j]=0;
for(int i=1;i<=m;i++) {
for(int j=1;j<=n;j++) {
if(x[i-1]==y[j-1]) {c和b数组的i,0和0,j位置为0,从1,1位置开始用来记录,x从0到m-1,y从0到n-1
c[i][j] = c[i-1][j-1]+1;
b[i][j] = 1;
}
else if(c[i-1][j]>c[i][j-1]) {
c[i][j] = c[i-1][j];
b[i][j] = 2;
}else {
c[i][j] = c[i][j-1];
b[i][j] = 3;
}
}
}
}
输出最长子序列:
private static void printLcs(int i, int j ,char[] x) {
if(i==0||j==0) {
return ;
}else if(b[i][j]==1) {
printLcs(i-1,j-1,x);
System.out.print(x[i-1]);
}else if(b[i][j]==2) {
printLcs(i-1,j,x);
}else {
printLcs(i,j-1,x);
}
}
综合:
import java.util.Scanner;
public class Oj1_DP_LCS {
static int[][] c;
static int[][] b;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while(sc.hasNext()) {
char[] X = sc.nextLine().toCharArray();
char[] Y = sc.nextLine().toCharArray();
int m=X.length,n=Y.length;
c = new int[m+1][n+1];
b = new int[m+1][n+1];
lcsLength(X,Y,b);
System.out.println(c[m][n]);
printLcs(m,n,X);
}
}
//输出LCS
private static void printLcs(int i, int j ,char[] x) {
if(i==0||j==0) {
return ;
}else if(b[i][j]==1) {
printLCS(i-1,j-1,x);
System.out.print(x[i-1]);
}else if(b[i][j]==2) {
printLCS(i-1,j,x);
}else {
printLCS(i,j-1,x);
}
}
//填表
private static void lcsLength(char[] x, char[] y,int[][] b) {
int m = x.length;
int n = y.length;
//c = new int[m+1][n+1];
for(int i=0;i<=m;i++) c[i][0]=0;
for(int j=0;j<=n;j++) c[0][j]=0;
for(int i=1;i<=m;i++) {
for(int j=1;j<=n;j++) {
if(x[i-1]==y[j-1]) {c和b数组的i,0和0,j位置为0,从1,1位置开始用来记录,x从0到m-1,y从0到n-1
c[i][j] = c[i-1][j-1]+1;
b[i][j] = 1;
}
else if(c[i-1][j]>c[i][j-1]) {
c[i][j] = c[i-1][j];
b[i][j] = 2;
}else {
c[i][j] = c[i][j-1];
b[i][j] = 3;
}
}
}
}
}
四、优化
1.不使用b数组
因为c[ i ][ j ]由 c[ i-1 ][ j-1 ], c[ i-1 ][ j ],c[ i ][ j-1 ]三个数确定,在输出LCS时同样可以用这三个数在O(1)的时间里来确定c[i][j]由三个数的哪一个数得来,从而节省θ(m*n)的空间。
private static void printLCS(int i, int j ,char[] x) {
if(i==0||j==0) {
return ;
}else if(c[i][j]==c[i-1][j-1]+1) {
printLCS(i-1,j-1,x);
System.out.print(x[i-1]);
}else if(c[i][j]==c[i-1][j]) {
printLCS(i-1,j,x);
}else {
printLCS(i,j-1,x);
}
}
2.滚动数组
如果只想得到最长公共子序列的长度,那么算法的空间需求将大大减少,在计算c[i][j]时,我们只用到了c的第i行和第i-1行。用这两行就可以算出最长公共子序列。最后输出c[0][n]即结果。
参考代码:
private static void lcsLength(char[] x, char[] y,int[][] b) {
int m = x.length;
int n = y.length;
//c = new int[m+1][n+1];
//for(int i=0;i<=m;i++) c[i][0]=0;
for(int j=0;j<=n;j++) c[0][j]=0;
for(int i=1;i<=m;i++) {
for(int j=1;j<=n;j++) {
if(x[i-1]==y[j-1]) {
c[1][j] = c[0][j-1]+1;
}
else if(c[0][j]>c[1][j-1]) {
c[1][j] = c[0][j];
}else {
c[1][j] = c[1][j-1];
}
}
for(int j=1;j<=n;j++) {
c[0][j] = c[1][j];
c[1][j] = 0;
}
}
}