最大公共子序列及最大公共子串问题是动态规划算法中比较经典,而且经常在一起提到。其主要的不同点就是公共子序列不要求元素的下标值连续,而公共字串则要求下标连续。
动态规划算法重点是要发现题目的最优解的结构特征,尽可能用一个递归的方程式进行描述从而实现自底向上计算最优值。一般这里的最优值还不是动态规划题目的最终解,最优值所构建的是动态规划问题的解空间,我们需要根据这个解空间来最终构造出最优解。
最大公共子序列问题的最优解结构用递归方程描述如下, 表示的是字符序列 和字符序列的最大公共子序列元素个数。注意,公式条件中 中的下标代指实际问题中字符序列X和字符序列Y的实际元素个数。在程序实现时,由于字符数组下标是从0开始,所以实际指代的是字符数组中的元素 。
这是问题的关键,有了它,我们可以构建出问题的最优值解空间。下图是以给定的字符数组 a, b,构建的最优值解空间
char[] a = {'A','C','D','E','H','L'};
char[] b = {'A','B','C','I','F','H','L'};
有了最优值解空间后,我们可以通过回溯把问题的最优解求出。
上图中着色的单元格是求解过程中所经过的路径,黄色的为最优解的元素,即最大公共子序列,绿色的部分为非公共子序列元素。
代码实现
public class CLS {
private void commonCLS(char[] a,char[] b){
int i,j;
int[][] c = new int[a.length + 1][b.length + 1]; //构建最优值解空间。
for ( i = 0 ;i <= a.length ;i++) c[i][0] = 0;
for ( j = 0 ;j <= b.length; j++) c[0][j] = 0;
for (i = 1 ;i <= a.length ;i++)
for (j = 1;j <=b.length; j++){
if (a[i - 1] == b[j - 1]) //由于数组元素下标从零开始,故实际需要查看数组中的a[i-1] 和 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];
}
}
constructSolution(a.length,b.length,a,b,c);
}
private void constructSolution(int i,int j,char[] a,char[] b,int[][] c){
if ( i == 0 || j == 0) return;
if (c[i][j] == c[i-1][j-1] + 1 && a[i-1] == b[j-1]){ //由于数组元素下标从零开始,故实际需要查看数组中的a[i-1] 和 b[j-1]。
constructSolution(i-1,j-1,a,b,c);
System.out.print(a[i-1] + " ");
}else
{
if (c[i - 1][j] >= c[i][j-1])
constructSolution(i-1,j,a,b,c);
else
constructSolution(i,j-1,a,b,c);
}
}
public static void main(String[] args) {
char[] a = {'A','C','D','E','H','L'};
char[] b = {'A','B','C','I','F','H','L'};
CLS cls = new CLS();
cls.commonCLS(a, b);
}
}
输出结果
A C H L
对于最大公共字串,其最优解空间要相对简单,由于其要求子串的小标在两个字符数组中都是连续的,那么问题的最优解就会在二维矩阵解空间的斜对角线上。其最优解结构的递归方程式可描述如下
同样以最大公共子序列用到的字符数组 a, b为例,构建的最优值解空间如下
char[] a = {'A','C','D','E','H','L'};
char[] b = {'A','B','C','I','F','H','L'};
不难看出上述最优值解空间中构造出的最优解如下图
另外 ,我们通过上述最优值解空间去求最优解的话,我们还需要一个坐标点位置,就是从那里开始回溯来最优解。因为对于给定的不同字符串,生成的最优值解空间也会不一样,我们要找到所有斜对角线上最长且连续的一串自然数,就需要记录下其一端的坐标 点p (x,y)。对于当前给定的a,b字符数组,坐标点p的位置为x=6,y=7。
代码如下
public class SequentialLCS {
private void sLCS(char[] a,char[] b){
int sum = 0;
int m=0,n=0;
int[][] c = new int[a.length + 1][b.length + 1];
for (int i=0;i<=a.length;i++) c[i][0] = 0;
for (int j=0;j<=a.length;j++) c[0][j] = 0;
for (int i=1;i<=a.length;i++)
for (int j=1;j<=b.length;j++){
if (a[i-1] == b[j-1]) //由于数组元素下标从零开始,故实际需要查看数组中的a[i-1] 和 b[j-1]。
c[i][j] = c[i-1][j-1] + 1;
else
c[i][j] = 0;
if (sum < c[i][j]){
sum = c[i][j];
m = i;
n = j;
}
}
constructSolution(c,a,m,n);
}
private void constructSolution(int[][] c, char[] a,int m, int n) {
if ((m ==0 || n==0) ||(c[m][n] == 0)) return;
constructSolution(c,a,m-1,n-1);
System.out.print( a[m-1] + " ");
}
public static void main(String[] args){
char[] a = {'A','C','D','E','H','L'};
char[] b = {'A','B','C','I','F','H','L'};
SequentialLCS sLCS = new SequentialLCS();
sLCS.sLCS(a, b);
}
}
运行结果如下
H L