一 问题引入
二 问题描述
给定两个字符串X[A,B,C,B,D,A,B]和Y[B,D,C,A,B,A],求X和Y的最长公共子序列LCS
三 思路
1 采用暴力破解法: 扫描X所有的可能子串, 取其中一个字符时候,存在 取或不取两种情况, 假设X字符串有M个字符,则子串个数为2的M次方,
假设Y有N个字符,将M中取出的子串与Y中进行对比,最后的时间复杂度为 N*2^M, 简直是龟速啊。。。
2 简化方案
2.1 从题目描述中 X = [A,B,C,B,D,A,B] , Y[B,D,C,A,B,A] ,目测最大公共子串为 BDAB , BCAB ,最长为4.
2.2 假设从一开始就知道X,Y的最大长度为4 ,那么只要关心X为4的子串与Y进行对比,这样效率就提高了.
2.3 进一步假设X字符串转为char数组后坐标为{0,1,2,3,4.....M} , 同理Y为{0,1,2,3...N}
假设 i <= M , j <= N , c[i][j] =LCS( X,Y) . // i ,j 为一个int整数 , c[i][j]为 X的char数组{0,1,2,3....i} 和Y的char数组{0,1,2,3...j} 最大公共子串的长度
// LCS 为 longestcommon subsequence ( 最长公共子串 ) 的缩写 .
那么对于X{0,1,2,3.....M} , Y{0,1,2,3....N}的最大公共子串长度为
c[i][j] + LCS(X1,Y1)
其中 X1 的坐标{ i+1,i+2,.....M } ,Y1坐标为{j+1,j+2....N}.
求最大公共子串长度总结 : 一部分长度( 假设已知) + 另外一部长度( 需要求的 )
四 公式
最大公共子串公式1
if (X[i] == Y[j] ) c[i][j] = c[i-1][j-1] + 1
证明 :
假设X , Y (见图1) , 在X {0,1,2,...i} , Y {0,1,2,3...j} 中最长公共子串长度为 k ,那么 在X{0,1,2,3...,i-1} ,Y{0,1,2,3,...,j-1}(见图2)中最长公共子串长度为k-1
图1
图2
反证法 : X{0,1,2,3...,i-1} ,Y{0,1,2,3,...,j-1}中最长公共子串长度为k-1不成立, 即存在一个w ( w > k-1 ) ,那么 如上图2 , 当 X[i] == Y[j] 时候, 即在X {0,1,2,...i} , Y {0,1,2,3...j} 中, 最大公共子串长度为 w+1 > ( k-1 ) +1 > k ,这与原命题矛盾,所以不存在这样一个w值.
即 if (X[i] == Y[j] ) c[i][j] = c[i-1][j-1] + 1
最大公共子串公式2
if (X[i] != Y[j] ) c[i][j] = max( c[i][j-1] , c[i-1][j] ) // max (a ,b) 返回a,b两个数中最大的一个 ,如果相等,则返回a
理解 : c[i][j] 对应图3 , c[i-1][j] 对应图4, c[i][j-1]对应图5, 那么最大公共子串长度一定是c[i-1][j] 和c[i][j-1]中的一个 ,用上面反证法即可证明.
图3
图4
图5
五 递归调用图
假设X{0,1,2,3..M},Y{0,1,2,3,...N} 中M =7 , N = 6,那么它的调用图如下.
从这幅图中看出
1 树的高度为 M+N = 7+6 = 15
2 时间复杂度 2^(M+N) = 2^13 (比暴力破解时间N*2^M=6*2^7还要多很多, 假设不采用备忘录法和没有重复情况下, 动态规划时间复杂度比暴力破解复杂度高很多,但 经测试在M=29,N=28,时候 ,暴力破解法时间为 300万次 ,动态规划调用1000多次,后面有代码,可自行测试)
3 有少量重复调用,比如红框1和红框2,都是递归6,5这种情况
六 动态规划的特征
1 最优子结构 (包含问题最优解),如 最大公共子串公式2 .
2 重复子问题,如 五 递归调用图
七 自顶向下+备忘录法 java代码
伪代码
LCS(x,y,i,j)
if(x[i]==y[j]) c[i][j] = LCS(x,y,i-1,j-1)+1
else c[i][j] = max(LCS(x,y,i-1,j) , LCS(x,y,i,j-1))
return c[i][j]
LCS.java
public class LCS {
private static int[][] c;
private static int counter;
/**
* 判断是否为空
* */
private static boolean isNullOrBlank(String source){
if(source==null||"".equals(source.replace(" ", ""))) return true;
return false;
}
/**
* 得到最长公共子串
* */
public static int getLcs(String xStr,String yStr){
if( isNullOrBlank(xStr) || isNullOrBlank(yStr) ) return 0;
char[] xChar = xStr.toCharArray();
char[] yChar = yStr.toCharArray();
c = new int[xChar.length][yChar.length];
int lcsMaxNumber = getLcsMaxNumber(xChar,yChar,xChar.length-1,yChar.length-1);
return lcsMaxNumber;
}
private static int max(int x,int y){
if(x>y) return x;
return y;
}
// /**
// * 不采用备忘录法
// * 得到LCS最长公共子串的长度
// * */
// private static int getLcsMaxNumber(char[] x,char[] y,int i,int j){
// counter++;
// if(i>=0&&j>=0){ // c[i][j]初始化为0,当不为0时,表示已经遍历过
// if(x[i]==y[j]){
// c[i][j] = getLcsMaxNumber(x,y,i-1,j-1) + 1;
// }else{
// c[i][j] = max(getLcsMaxNumber(x,y,i-1,j),getLcsMaxNumber(x,y,i,j-1));
// }
// System.out.println("counter = "+counter+" c["+i+"]["+j+"] = " + c[i][j]);
// return c[i][j];
// }
//
// return 0;
// }
/**
* 备忘录法
* 得到LCS最长公共子串的长度
* */
private static int getLcsMaxNumber(char[] x,char[] y,int i,int j){
counter++;
if(i>=0&&j>=0){
if(c[i][j]==0){
if(x[i]==y[j]){ // c[i][j]初始化为0,当不为0时,表示已经遍历过
c[i][j] = getLcsMaxNumber(x,y,i-1,j-1) + 1;
}else{
c[i][j] = max(getLcsMaxNumber(x,y,i-1,j),getLcsMaxNumber(x,y,i,j-1));
}
}
System.out.println("counter = "+counter+" c["+i+"]["+j+"] = " + c[i][j]);
return c[i][j];
}
return 0;
}
public static void main(String[] args) {
// String x = "ABCBDAB";
// String y = "BDCABA";
String x = "ACCGGTCGAGTGCGCGGAAGCCGGCCGAA";
String y = "GTCGTTCGGAATGCCGTTGCTCTGTAAA";
int number = LCS.getLcs(x, y);
System.out.println(number);
}
}
对于前面提到的DNA的输出最大公共子串长度为20
八 回溯法求最大公共子串
回溯法原理
假设 X = "ABCBDAB"; Y= "BDCABA",它们生成一副下面的图
public class LCS2 {
private static int[][] c; //保存长度
private static char[][] b; //保存特殊符号 ↖ ↑ ←
/**
* 判断是否为空
* */
private static boolean isNullOrBlank(String source){
if(source==null||"".equals(source.replace(" ", ""))) return true;
return false;
}
/**
* 得到最长公共子串
* */
public static String getLCS(String x,String y){
//判断x,y是否为空
if( isNullOrBlank(x) || isNullOrBlank(y) ) return null;
//初始化变量
int m = x.length();
int n = y.length();
b = new char[m][n];
c = new int[m][n];
char[] xChar = x.toCharArray();
char[] yChar = y.toCharArray();
//调用获取最大公共子串长度方法
getLcsMaxNumber(xChar,yChar,xChar.length-1,yChar.length-1);
//回溯法得到最长公共子串
StringBuffer sb = new StringBuffer();
PRINT_LCS(sb,xChar,yChar,m-1,n-1);
return sb.toString();
}
private static int max(int x,int y){
if(x>y) return x;
return y;
}
/**
* 备忘录法
* 得到LCS最长公共子串的长度
* */
private static int getLcsMaxNumber(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]初始化为0,当不为0时,表示已经遍历过
c[i][j] = getLcsMaxNumber(x,y,i-1,j-1) + 1;
b[i][j] = '↖';
}else{
int i_1 = getLcsMaxNumber(x,y,i-1,j);
int j_1 = getLcsMaxNumber(x,y,i,j-1);
if(i_1>j_1){
c[i][j] = i_1;
b[i][j] = '↑';
}else{
c[i][j] = j_1;
b[i][j] = '←';
}
}
}
return c[i][j];
}
return 0;
}
//回溯法得到最长公共子串
private static void PRINT_LCS(StringBuffer sb,char[] x,char[] y,int i,int j){
if(i==0 || j==0 ){
if(b[i][j]=='↖'){
sb.append(x[i]);
}
return ;
}
if(b[i][j]=='↖'){
PRINT_LCS(sb,x,y,i-1,j-1);
sb.append(x[i]);
}else if(b[i][j]=='↑'){
PRINT_LCS(sb,x,y,i-1,j);
}else if(b[i][j]=='←'){
PRINT_LCS(sb,x,y,i,j-1);
}
}
public static void main(String[] args) {
// String x = "ABCBDAB";
// String y = "BDCABA";
String x = "ACCGGTCGAGTGCGCGGAAGCCGGCCGAA";
String y = "GTCGTTCGGAATGCCGTTGCTCTGTAAA";
String lcs = LCS2.getLCS(x, y);
System.out.println("最大公共子串为 = " + lcs);
}
}
问题引入中
DNA序列为:S1=
ACCGGTCGAGTGCGCGGAAGCCGGCCGAA
,S2=
GTCGTTCGGAATGCCGTTGCTCTGTAAA的,