最长公共子序列
子序列:一个序列,中删除若干项得到的新的序列即为A的子序列。例1:序列1,6,2,3,7,5的一个子序列为1,2,3,5。对于一个长度为n的序列,其子序列共有2的n次方个,其非空子序列共有
个
公共子序列:同时是两个或多个序列的子序列。例2:序列1,3,5,4,2,6,8,7和序列1,8,6,7,5的一个公共子序列为1,6,7
最长公共子序列:在上面的例2中,1,5也是两个序列的公共子序列,以及1或8等等这些单个数字也是它们的一个子序列。最长子序列即指在这些子序列中最长的,1,6,7,或1,8,7,即为这两个序列的最长公共子序列(LCS)。
问题:两个序列和
的最长公共子序列
分析:
我们定义这样一个dp数组,dp[i][j]表示A中的一个子序列和B中的一个子序列
的最长公共子序列长度(其中:
)。循环枚举i和j的值,
- 当
时。
假设i=4,j=3,即在求序列与序列
的最长公共子序列时若发现
,则这两个序列的最长公共子序列长度可以由序列
与序列
的最长公共子序列的长度加1求得,即:
- 当
时
假设i=4,j=3,即在求序列与序列
时若发现
,此时序列
与序列
的最长公共子序列的长度为:
序列与序列
的最长公共子序列的长度
或序列与序列
的最长公共子序列的长度
由于是求最长子序列,自然是取最长的一个。即:dp[i][j]=Math.max(dp[i-1][j], dp[i][j-1])。
最后求出数组dp中的最大值即为这两个序列的最大公共子序列长度
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc= new Scanner(System.in);
//System.out.println("请输入序列A:");
String a=sc.nextLine();
//System.out.println("请输入序列B:");
String b=sc.nextLine();
int dp[][]=new int[a.length()+1][b.length()+1];//多开一行一列,不用0行0列以防止数组越界
int max=0;//记录最终结果
for(int i=1;i<=a.length();i++){//注意i和j都是从1开始分别到两个序列的长度结束
for(int j=1;j<=b.length();j++){
if(a.charAt(i-1)==b.charAt(j-1))//i和j都是从1开始的,所以减1防止数组越界
dp[i][j]=dp[i-1][j-1]+1;//ai=bj的情况
else
dp[i][j]=Math.max(dp[i-1][j], dp[i][j-1]);//ai!=bj,则等于前两个状态中值最大的那个
max=max>dp[i][j]?max:dp[i][j];//每更新依次dp数组中的值都比较一次,用来记录dp数组中的最大值
}
}
System.out.println(max);
}
}
到目前为止我们只是求出了最大公共子序列的长度,并没有达到求出整个子序列的要求,其实求整个子序列只需要对每个状态添加一个标记。前面说到对状态dp[i][j]有三个分支:
- 当
时
dp[i][j]=dp[i-1][j-1] //分支1
- 当
时
dp[i][j]=dp[i-1][j] //分支2
- 当
时
dp[i][j]=dp[i][j-1] //分支3
用标志数组TAG记录每个状态由哪一个分支得到的,最后根据标志数组得到最大公共子序列。例如当TAG[i][j]==1时,表示ai==bj,此时将ai记录到最终的结果字符串中
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc= new Scanner(System.in);
//System.out.println("请输入序列A:");
String a=sc.nextLine();
//System.out.println("请输入序列B:");
String b=sc.nextLine();
int dp[][]=new int[a.length()+1][b.length()+1];//多开一行一列,不用0行0列以防止数组越界
int tag[][]=new int[a.length()+1][b.length()+1];//标志数组,用来记录进行了哪种操作
for(int i=1;i<=a.length();i++){//注意i和j都是从1开始分别到两个序列的长度结束
for(int j=1;j<=b.length();j++){
if(a.charAt(i-1)==b.charAt(j-1)){//i和j都是从1开始的,所以减1防止数组越界
dp[i][j]=dp[i-1][j-1]+1;//ai=bj的情况
tag[i][j]=1;//分支1
}else{
if(dp[i-1][j]>dp[i][j-1]){
dp[i][j]=dp[i-1][j];
tag[i][j]=2;//分支2
}else{
dp[i][j]=dp[i][j-1];
tag[i][j]=3;//分支3
}
}
}
}
int lenA=a.length(),lenB=b.length();
String result=new String();//用来保存结果
while(lenA>0&&lenB>0){
if(tag[lenA][lenB]==1){//分支1,ai==bj
result+=a.charAt(lenA-1);
//lenA、lenB都进行自减
lenA--;
lenB--;
}
else if(tag[lenA][lenB]==2)//分支2,dp[i][j]继承了上一个状态a1,a2...,a(i-1)与b1,b2...,bj的最长公共子序列长度
lenA--;//只需lenA--
else //分支2,dp[i][j]继承了上一个状态a1,a2...,ai与b1,b2...,b(j-1)的最长公共子序列长度
lenB--;//只需lenB--
}
//由于是由后向前几率最长公共子序列,所以需要对result进行一次翻转(StringBuffer对象.revers:对字符缓冲区中的内容进行翻转)
System.out.println(new StringBuffer(result).reverse());
}
}
翻转:将字符串result保存到字符串缓冲区中
StringBuffer s=new StringBuffer(result);
再调用SringBuffer中的revers()方法,得到的结果即为result的翻转
s.revers();