动态规划-最长公共子序列
最长公共子序列
问题描述
- 概念解释
- 子序列:给定一个序列,另一个序列满足如下条件时称为的子序列,即存在一个严格递增的的下标序列,对所有,满足。
- 公共子序列:给定两个序列和,如果既是的子序列,又是的子序列,则称之为和的公共子序列。
- 最长公共子序列问题(longest-common-subsequence problem ,简称LCS问题)
给定两个序列和,求和长度最大的公共子序列。
问题求解
- 是否可以使用动态规划?
- 是否满足最优子结构?
- 给出一个定义:前缀—给定一个序列,对于,定义的第前缀为。
- 给出一个定理:(LCS的最优子结构),令和为两个序列,为和的任意LCS。
a. 如果,则且是和的一个LCS。
b.如果,那么如果,则是和的一个LCS。
c. 如果,那么如果,则是和的一个LCS。
由此,我们可以看出,LCS问题满足最优子结构。
- 是否满足子问题重叠?
显然,在求解和的一个LCS时,需要求解器子问题,且子问题存在重复的部分,所以满足子问题重叠的特征。
- 是否满足最优子结构?
刻画最长公共子序列特征
- 由上述定理中最优子结构性质,可得递归公式:
计算LCS长度
- 从上述公式可以看出,可以使用递归方式解决问题,但是因为子问题个数只有个,所以可以采用自底向上的动态规划问题。以下为自底向上的动态规划问题的伪代码,其中过程LCS-LENGTH接受两个序列和为输入。它将的值保存在表中,并按行主次序计算表项。过程还维护一个表,帮助构造最优解。指向的表项对应计算时所选择的子问题的最优解。过程返回表b和表c,保存了和的LCS的长度。
LCS-LENGTH(X,Y)
m=X.length // 获得m的长度
n=Y.length
let b[1..m,1..n] and c[0..m,0..n] be new tables//新建二维数组b和c,b用于保存构建最长公共子序列的线索,c用于记录最长公共子序列的长度
for i = 0 to m//若其中一个子序列长度为0,则显然最长子序列长度为0
c[i,0] = 0
for j = 0 to n
c[0,j] = 0
for i = 1 to m//自底向上,按行主次序计算表项c,并找出最长公共子序列
for j =1 to n
if xi == yj//若序列对应位置相同,则子序列长度+1
c[i,j] = c[i-1,j-1]+1
b[i,j] = "↖"//保存重构子序列的线索,“↖”表示对应的两个序列i和j的位置为公共序列
elseif c[i-1,j]≥c[i,j-1]//找出最长公共子序列
c[i,j] = c[i-1,j]
b[i,j] = "↑"//“↑”表示公共元素要想上寻找,即i-1
else c[i,j] = c[i,j-1]//“←”表示公共元素要向左寻找,即j-1
b[i,j] = "←"
return c and b
举例来说,如图所示:
构造LCS
- 利用LCS-LENGTH返回的表b快速构造和的LCS,只需要按照如上所描述的方案即可逆序一次构造出LCS的所有元素(因为在找最大公共子序列时逻辑上就是逆序寻找的),以下给出递归函数,按正确的顺序打印出和的一个LCS。
PRINT-LCS(b,X,i,j)
if i==0 or j ==0
return
if b[i,j] == "左上"
PRINT-LCS(b,X,i-1,j-1)
print Xi
elseif b[i,j] == "↑"
PRINT-LCS(b,X,i-1,j)
else PRINT-LCS(b,X,i,j-1)
- 算法改进:我们发现可以直接通过s所依赖的判断出计算时使用了这三项中的哪一项。因此我们可以用一个类似PRINT-LCS的过程在时间内完成重构LCS的工作,而不必使用表b。以下给出该方法的伪代码:
ENHANCE-PRINT-LCS(c,X,i,j)
if c[i,j] > c[i-1,j] and c[i,j] > c[i,j-1]
print Xi
ENHANCE-PRINT-LCS(c,X,i-1,j-1)
elseif c[i,j] == c[i-1,j]
ENHANCE-PRINT-LCS(c,X,i-1,j)
else
ENHANCE-PRINT-LCS(c,X,i,j-1)
以下给出java实现的具体代码:
import java.util.Scanner;
public class LCS_LENGTH {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int keep = 1;//用于探测是否循环计算
do{
System.out.println("请输入第一个序列A");
String A = scanner.next();
char[] a1 = A.toCharArray();//将字符串变成数组
System.out.println("请输入第二个序列B");
String B = scanner.next();
char[] b1 = B.toCharArray();
//为了阅读方便,将输入的两个数组的长度都+1,且数组的第一个值为'0'
int m = a1.length+1;
int n = b1.length+1;
char[] a = new char[m];
char[] b = new char[n];
a[0] ='0';
b[0]='0';
//数组的复制
for(int i =1;i<m;i++){
a[i]= a1[i-1];
}
for(int i =1;i<n;i++){
b[i]= b1[i-1];
}
int[][] c = new int[m][n];//c用于保存当两个序列的长度为i和j时,最大子序列长度
//设置当其中一个序列为空序列时,最大子序列长度为0
for(int i = 0;i<m;i++){
c[i][0] = 0;
}
for(int i = 0;i<n;i++){
c[0][i] = 0;
}
//自底向上计算最大子序列长度
for(int i = 1;i<m;i++){//按行求解
for(int j = 1;j<n;j++){
if(a[i]==b[j]){//若当前元素相等,则最大子序列长度+1
c[i][j] = c[i-1][j-1]+1;
}
//否则,选择较长的那一组序列
else if(c[i-1][j]>=c[i][j-1]){
c[i][j] = c[i-1][j];
}
else{
c[i][j] = c[i][j-1];
}
}
}
System.out.println("A与B的一个最大公共子序列长度: "+c[m-1][n-1]+" ,且最大公共子序列为:");
ENHANCE_PRINT_LCS(c,a,m-1,n-1);//重构最长子序列
System.out.println("\n继续输入请输入1");
keep = scanner.nextInt();
}while( keep == 1);
}
//递归输出最长子序列
public static void ENHANCE_PRINT_LCS(int[][] c,char[] a,int i,int j){
if(i>=1&&j>=1&&c[i][j]> c[i-1][j]&&c[i][j]>c[i][j-1]){
ENHANCE_PRINT_LCS(c,a,i-1,j-1);
System.out.print(a[i]+" ");
}else if(i>=1&&c[i][j]==c[i-1][j]){
ENHANCE_PRINT_LCS(c,a,i-1,j);
}else if(j>=1){
ENHANCE_PRINT_LCS(c,a,i,j-1);
}
}
}
以下为测试部分: