这个世界上根本就不存在“不会做”这回事,当你失去了所有的依靠的时候,自然就什么都会了。
0. 前言
最长公共子序列的问题常用于解决字符串的相似度,是一个非常实用的算法,作为码农,此算法是我们的必备基本功。最长公共子串(Longest Common Substirng)和最长公共子序列(Longest Common Subsequence,LCS)的区别为:子串是串的一个连续的部分,子序列则是从不改变序列的顺序,而从序列中去掉任意的元素而获得新的序列;也就是说,子串中字符的位置必须是连续的,子序列则可以不必连续。
1.最长公共子序列概述
问题描述: 字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列
2. 动态规划求解最长公共子序列
2.1 刻画最长公共子序列的最优子结构的特征
如果使用暴力搜索求解LCS问题,需要穷举X的所有子序列,对每一个子序列检查它是否是Y的子序列,记录找到的最长的子序列。X的每个子序列对应的X的下标集合是(1,2,3,.....m)的一个子集,所以有2m个子序列。但是最长公共子序列有最优子结构的性质。子问题的自然分类对应两个输入序列的“前缀”对。
定理: 考虑最长公共子序列问题如何分解成子问题,设A=“a1,…,am”,B=“b1,…,bn”,并Z=“z1,…,zk”为它们的最长公共子序列。不难证明有以下性质:
(1) 如果am=bn,则zk=am=bn,且“z1,…,zk-1”是“a1,…,am-1”和“b1,…,bn-1”的一个最长公共子序列;
(2) 如果am!=bn,则若zk!=am,蕴涵“z1,…,zk”是“a1,…,am-1”和“b1,…,bn”的一个最长公共子序列;
(3) 如果am!=bn,则若zk!=bn,蕴涵“z1,…,zk”是“a1,…,am”和“b1,…,bn-1”的一个最长公共子序列。
上面的定理告诉我们:两个序列的LCS包含两个序列前缀的LCS,因此LCS问题具有最优子结构的性质。
2.2 构建递归解
通过上面的定理我们可以发现在求A=“a1,…,am”,B=“b1,…,bn”的一个公共的LCS的时候我们需要求解一个到两个子问题。、
【1】如果am=bn,我们应该求Am−1和Bn−1的LCS;
【2】如果am!=bn,若zk!=am,我们应该求Am−1和Bn的LCS。如果am!=bn,则若zk!=bn,我们应该求Am和Bn−1的LCS。两者LCS比较长的称为A和B的LCS。
我们可以很容易的发现子问题重叠的性质。在求A和B的LCS的过程中,我们的2种情况,都存在Am−1和Bn−1的LCS的重叠子问题。设计LCS的递归算法首先需要建立最优解的递归式,C[i,j]表示Ai和Bj的LCS长度,如果i=j=0,那么c[i,j]=0.根据LCS问题最优子结构的性质得出以下公式:
在上面的问题中我们通过限制条件限定了需要求解哪些子问题。在前面的算法中我们没有限定排除任何子问题,在这里我们需要根据限制条件排除相应的子问题。
2.3 计算LCS的长度(求最优解)
根据2.2的递归公式,我们可以很痛以的写出一个指数时间的递归算法。但是,由于LCS问题只有Θ(mn)个不同的子问题,我们可以使用DP来自底向上的计算。
算法伪代码:
LCS-Length(X[],Y[]){
int m=x.length;
int n=Y.length;
int[][] c = new int[m + 1][n + 1];
char[][] b = new char[m + 1][n + 1];
for(i=1 to m){
c[i,0]=0;//
}
for(j=0 to n){
c[0,j]=0;
}
/**
i是行 j是列
*/
for(i=1 to m){
for(j=1 to n){
if(xi==yj){
c[i,j]=c[i-1,j-1]+1;
b[i,j]='\';
}
elseif(c[i-1,j]>=c[i,j-1]){
c[i,j]=c[i-1,j];
b[i,j]='|';
}else{
c[i,j]=c[i,j-1];
b[i,j]='--';
}
}
}
return c and b;
}
2.4 构造LCS(构造最优解)
我们可以使用辅助表b来快速构造x和y的LCS,只需要从b[m,n]开始按照箭头的方向追踪即可.递归算法如下:
PRINT_LCS(X,b[][],i,j){
if(i==0 || j==0){
return;
}
if(b[i,j]=="\"){
PRINT_LCS(X,b[][],i-1,j-1);
print xi;
}else if(b[i,j]=="|"){
PRINT_LCS(X,b[][],i-1,j)
}else{
PRINT_LCS(X,b[][],i,j-1)
}
}
3. 动态规划Java实现
算法实现类
package lbz.ch15.dp.ins3;
/**
* @author LbZhang
* @version 创建时间:2016年3月9日 下午9:51:22
* @description 最大公共子序列
*/
public class LCS {
/***
* * 这一部分我们使用辅助表,从左上角开始计算每一个位置上LCS的长度 判断算法:
*/
public static int[][] lcsLength(Object[] x, Object[] y) {
int m = x.length;
int n = y.length;
int[][] c = new int[m + 1][n + 1];
char[][] b = new char[m + 1][n + 1];
int i, j;
for (i = 1; i <= m; i++) {
c[i][0] = 0;
}
for (j = 0; j <= n; j++) {
c[0][j] = 0;
}
for (i = 1; i <= m; i++) {
for (j = 1; j <= n; j++) {
if (x[i - 1].equals(y[j - 1])) {
c[i][j] = c[i - 1][j - 1] + 1;
b[i][j]='\\';
} else if (c[i - 1][j] >= c[i][j - 1]) {
c[i][j] = c[i - 1][j];
b[i][j]='|';
} else {
c[i][j] = c[i][j - 1];
b[i][j]='-';
}
}
}
return c;
}
/**
* 为了输出最长公共子序列,改进的输出
* @param x
* @param y
* @return
*/
public static char[][] lcsPrint(Object[] x, Object[] y) {
int m = x.length;
int n = y.length;
int[][] c = new int[m + 1][n + 1];
char[][] b = new char[m + 1][n + 1];
int i, j;
for (i = 1; i <= m; i++) {
c[i][0] = 0;
}
for (j = 0; j <= n; j++) {
c[0][j] = 0;
}
for (i = 1; i <= m; i++) {
for (j = 1; j <= n; j++) {
if (x[i - 1].equals(y[j - 1])) {
c[i][j] = c[i - 1][j - 1] + 1;
b[i][j]='\\';
} else if (c[i - 1][j] >= c[i][j - 1]) {
c[i][j] = c[i - 1][j];
b[i][j]='|';
} else {
c[i][j] = c[i][j - 1];
b[i][j]='-';
}
}
}
return b;
}
// ///print the lcs
// //采用递归的方式将结果打印出来
public static void printLcs(int[][] c, Object[] x, Object[] y, int i, int j) {
if (i == 0 || j == 0) {
return;
}
if (x[i - 1].equals(y[j - 1])) {
printLcs(c, x, y, i - 1, j - 1);
System.out.print(x[i - 1] + " ");
} else if (c[i - 1][j] >= c[i][j - 1]) {
printLcs(c, x, y, i - 1, j);
} else {
printLcs(c, x, y, i, j - 1);
}
}
public static void printBySignalLcs(char[][] b, Object[] x, int i, int j) {
if (i == 0 || j == 0) {
return;
}
if (b[i][j]=='\\') {
printBySignalLcs(b,x,i-1,j-1);
System.out.print(x[i - 1] + " ");
} else if (b[i][j]=='|') {
printBySignalLcs(b,x,i-1,j);
} else {
printBySignalLcs(b,x,i,j-1);
}
}
}
测试类
package lbz.ch15.dp.ins3;
/**
* @author LbZhang
* @version 创建时间:2016年3月10日 下午3:19:57
* @description 最长公共子序列测试类
*/
public class Test {
public static void main(String[] args) {
Character[] x = { 'A', 'C', 'C', 'G', 'G', 'T', 'C', 'G', 'A', 'G',
'T', 'G', 'C', 'G', 'C', 'G', 'G', 'A', 'A', 'G', 'C', 'C',
'G', 'G', 'C', 'C', 'G', 'A', 'A' },
y = { 'G', 'T', 'C', 'G',
'T', 'T', 'C', 'G', 'G', 'A', 'A', 'T', 'G', 'C', 'C', 'G',
'T', 'T', 'G', 'C', 'T', 'C', 'T', 'G', 'T', 'A', 'A', 'A' };
Integer[] a = { 389, 207, 155, 300, 299, 170, 158, 65 }, b = { 389,
300, 299, 207, 170, 158, 155, 65 };
int[][] c;
char[][] p;
c = LCS.lcsLength(x, y);
//p=LCS.lcsPrint(x, y);
LCS.printLcs(c, x, y, 29, 28);
System.out.println();
c = LCS.lcsLength(a, b);//二维表
LCS.printLcs(c, a, b, 8, 8);
System.out.println();
System.out.println("--------******改进******---------");
p=LCS.lcsPrint(x, y);
LCS.printBySignalLcs(p, x, 29, 28);
System.out.println();
p=LCS.lcsPrint(a, b);
LCS.printBySignalLcs(p, a, 8, 8);
}
}
实验结果