【动态规划】最长公共子序列
最长公共子序列
给出两个序列text1和text2,要求求出这两个序列的最长公共子序列和最长公共子序列长度。
**最长公共子序列的含义:**这里的子序列可以不是连续的,只要相对位置是符合的就算,比如abdfs和adef的最长公共子序列是adf。
例题:求出acfsc(text1)和acs(text2)的最长公共子序列。
1️⃣.划分子问题
对于求text1和text2的最长公共子序列,从只有一个字符时候开始求解,求出只有一个字符的时候的最长公共序列,然后扩大问题,求出前两个字符的最长公共子序列,不断扩大问题规模,直到达到最大规模。
2️⃣.确定动态规划方程
这里先列出求长度时的模拟过程,通过模拟的过程分析出如何递推,找出动态规划方程。
dp表格,最长公共子序列长度表格
text1|text2 | a | c | s | |
---|---|---|---|---|
0 | 0 | 0 | 0 | |
a | 0 | 1 | 1 | 1 |
c | 0 | 1 | 2 | 2 |
f | 0 | 1 | 2 | 2 |
s | 0 | 1 | 2 | 3 |
c | 0 | 1 | 2 | 3 |
第二行:t1只有a和t2取a,这时候相等,那么长度就是1,t1不变,由于t1只有一个元素a,所以第二行都为1;
第三行:t1取ac,t2取a,这个时候t1的c和t2的c不相等,但是t1的前一个a和t2的a相等,所以取dp[i] [j]的上方的值dp[i-1] [j]填充到当前值。
第三行:t1取ac,t2取acs,这个时候c和s不等,当前值的左方值是更大,t1的ac和t2的ac,所以取左方dp[i] [j-1]填充到dp[i] [j]。
通过观察表格不难发现,当前值dp[i] [j]是由三个方向推出来的,左上方,左方,上方,进一步可以确定动态规划方程。
- t1[i]==t2[j]
d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 , t e x t 1 [ i ] = = t e x t 2 [ j dp[i][j] = dp[i-1][j-1]+1,text1[i]==text2[j dp[i][j]=dp[i−1][j−1]+1,text1[i]==text2[j
- t1[i]!=t2[j]
d p [ i ] [ j ] = M a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) , t e x t 1 [ i ] ! = t e x t 2 [ j ] dp[i][j]=Max(dp[i-1][j],dp[i][j-1]),text1[i]!=text2[j] dp[i][j]=Max(dp[i−1][j],dp[i][j−1]),text1[i]!=text2[j]
为了方便递推,dp数组的第一行和第一列设置为0,也就是t1和t2是空串的时候,同样,由于dp数组对应的字符的i和j不是从0开始,所以t1和t2数组使用i和j索引的时候要注意避免越界。
3️⃣:递推填写表格
确定动态规划方程后,通过双重for循环便可以实现表格的填写,dp数组的右下角的值即为t1和t2的最长公共子序列长度。
4️⃣:回溯求最长公共子序列
填写表格求出最长公共子序列后还需要求出是那些字符组成,为了实现这个功能,还需要再用一个二维数组state来存当前dp数组求解中每个值是如何推出的状态,这里定为1(t1[i]==t2[j] 从左上角推出),2(从左边推出),3(从上方推出),当然标志的定义是随意的,只要可以达到区分的目的即可。
5️⃣:注意
明确dp数组的含义:dp[i] [j]代表t1[i]和t2[j]所组成的最长公共子序列长度。
💻:代码实现
package com.dp;
import java.util.Scanner;
/**
* @Author Lunau
* @Create 2022-04-21 16:07
* @Description
* @Result
*/
public class CommonOrder {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入序列1:");
char[] ach = sc.nextLine().toCharArray();
System.out.println("请输入序列2");
char[] bch = sc.nextLine().toCharArray();
System.out.println("最长公共序列长度为:"+getCommonOrder(ach,bch));
}
public static int getCommonOrder(char[] ach,char[] bch) {
//最长公共子序列数组 第一行第一列初始化为0
int[][] dp = new int[ach.length+1][bch.length+1];
//序列状态数组
int[][] state = new int[ach.length+1][bch.length+1];
for(int i=1;i<=ach.length;i++) {
for(int j=1;j<=bch.length;j++) {
//相等就在上一个基础上加一
if(ach[i-1]==bch[j-1]) {
dp[i][j] = dp[i-1][j-1]+1;
state[i][j] = 1; //dp[i][j]从左上方推出
} else if(dp[i][j-1]>=dp[i-1][j]) {
//不相等就从dp[i-1][j]或dp[i][j-1]取较大者
dp[i][j] = dp[i][j-1];
state[i][j] = 2; //dp[i][j]从左边推出
} else{
dp[i][j] = dp[i-1][j];
state[i][j] = 3; //dp[i][j]从上方推出
}
}
}
//求最长公共序列,dp数组右下角为最长公共子序列长度
int p=ach.length,q=bch.length,k=dp[p][q];
char[] res = new char[k]; //防止越界
while(p>0&&q>0) {
if(state[p][q]==1) {
res[k-1] = ach[p-1]; //ach最后一个索引是ach长度减一,res是从0开始索引
k--;p--;q--;
} else if(state[p][q]==2) {
q--; //bch指针左移
} else {
p--; //ach指针左移动
}
}
for(int i=0;i<res.length;i++) {
System.out.print(res[i]+" ");
}
return dp[ach.length][bch.length];
}
}