目录
1.最长公共子序列问题描述
2.问题解析?
2.1为什么用动态规划来做?
2.2怎么用动态规划来做?
3.java代码实现
4.最长公共子序列追踪
1.最长公共子序列问题描述
最长公共子序列(Longest Common Subsequence),这个名词可以说是耳熟能详了。
给定两个字符串 string1 和 string2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回 0。
注意,子序列并非子字符串,子字符串要求连续,子序列并不要求连续。
如BDE是ABCDE子序列,也是BDCAE的子序列,这样BDE就是二者的公共子序列了。
2.问题解析?
由于是经典问题了,都知道用动态规划来做,因此抛出两个问题。
2.1为什么用动态规划来做?
一般的题如果有最优子结构与重叠子问题,就用动态规划来做。
最优子结构:
- 对于长度分别为a,b的字符串两个字符串来说,我们求的是其最长公共子序列。
- 若是将这两个字符串各删掉k个字符。
- 对于长度分别为a-k、b-k的两个字符串来说,我们求的还是其最长公共子序列,而a、b的一对可以根据a-k、b-k的一对推出来。
- 因此符合最优子结构的特点。
重叠子问题:
- 若求长度分别为a+k、b+k的一对序列的最长公共子序列可以从a、b的一对推出来,也可以从a-k、b-k的一对推出来。
- 而a、b的一对也可以从a-k、b-k的一对推出来,那么这两个子问题就重叠了。
2.2怎么用动态规划来做?
只需推导出递推公式然后根据递推公式初始化dp数组后就可以等着机器算出答案了。
设 X=(x1,x2,.....xn) 和 Y={y1,y2,.....ym} 是两个序列,将 X 和 Y 的最长公共子序列记为LCS(X,Y)
1)如果xn=ym,即X的最后一个元素与Y的最后一个元素相同,这说明该元素一定位于公共子序列中。因此,现在只需要找:LCS(Xn-1,Ym-1)
2)如果xn != ym,因为序列X 和 序列Y 的最后一个元素不相等,那说明最后一个元素不可能是最长公共子序列中的元素,故产生了两个子问题:LCS(Xn-1,Ym) 和 LCS(Xn,Ym-1)。求解上面两个子问题,得到的公共子序列谁最长,那谁就是 LCS(X,Y)
故递推公式:
从递推公式中可以看出只需初始化第一行与第一列即可,后面的值都可以机器推算出来。
3.java代码实现
package 最长公共子序列;
import java.util.Scanner;
public class LCS {
public static void main(String[] args) {
// 读取字符串
Scanner scanner = new Scanner(System.in);
String str1 = scanner.nextLine();
String str2 = scanner.nextLine();
scanner.close();
LCS.lcs(str1, str2);
}
public static void lcs(String str1, String str2) {
// 字符串处理
char[] chars1 = str1.toCharArray();
char[] chars2 = str2.toCharArray();
// 设置动态规划数组
int[][] dp = new int[chars1.length + 1][chars2.length + 1];
// 初始化
for (int i = 0; i < chars1.length+1; i++) {
dp[i][0] = 0;
}
for (int i = 0; i < chars2.length+1; i++) {
dp[0][i] = 0;
}
// 根据递推方程求解dp数组
for (int i = 1; i < chars1.length+1; i++) {
for (int j = 1; j < chars2.length+1; j++) {
if(chars1[i-1] == chars2[j-1]) {
dp[i][j] = dp[i-1][j-1]+1;
}else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
System.out.println("长度为" + dp[chars1.length][chars2.length]);
}
}
运行结果:?
4.最长公共子序列追踪
对于之前的代码求的是最长公共子序列的长度,其实我们可以根据动态规划的递推公式再逆推出最长公共子序列的值是什么。
思路:想象一下,回溯的过程其实是把两个序列分别削短:
- 对于两个序列的各自最后一个字符,如果他们相同,那肯定是配对的(递推公式那里说过),那就把这最后一个字符保存起来,然后同时削掉一个字符。
- 对于两个序列的各自最后一个字符,如果他们不相同,那肯定是其中一个序列的最后一个字符是不需要的(看dp数组中的值是横向传下来(dp[i][j]=dp[i-1][j])的还是纵向传下来的(dp[i][j]=dp[i][j-1])),只需把不需要的那个字符削掉就可以继续把两个序列接着比较了。
- 当某一个序列全部被削掉时结束。得到最长公共子序列。
在LCS类中的lcs方法,加入溯源部分的代码:
package 最长公共子序列;
import java.util.Scanner;
public class LCS {
public static void main(String[] args) {
// 读取字符串
Scanner scanner = new Scanner(System.in);
String str1 = scanner.nextLine();
String str2 = scanner.nextLine();
scanner.close();
LCS.lcs(str1, str2);
}
public static void lcs(String str1, String str2) {
// 字符串处理
char[] chars1 = str1.toCharArray();
char[] chars2 = str2.toCharArray();
// 设置动态规划数组
int[][] dp = new int[chars1.length + 1][chars2.length + 1];
// 初始化
for (int i = 0; i < chars1.length+1; i++) {
dp[i][0] = 0;
}
for (int i = 0; i < chars2.length+1; i++) {
dp[0][i] = 0;
}
// 根据递推方程求解dp数组
for (int i = 1; i < chars1.length+1; i++) {
for (int j = 1; j < chars2.length+1; j++) {
if(chars1[i-1] == chars2[j-1]) {
dp[i][j] = dp[i-1][j-1]+1;
}else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
// dp数组溯源
int rowIndex = chars1.length;
int colIndex = chars2.length;
String temp = "";
while(rowIndex>=1 && colIndex>=1) {
if(dp[rowIndex][colIndex] == dp[rowIndex-1][colIndex-1]+1
&& chars1[rowIndex-1]==chars2[colIndex-1]) {
temp += chars1[rowIndex-1];
rowIndex--;
colIndex--;
}else {
if(dp[rowIndex-1][colIndex] > dp[rowIndex][colIndex-1]) {
rowIndex--;
}else {
colIndex--;
}
}
}
StringBuffer sub = new StringBuffer(temp);
sub.reverse();
System.out.println(sub);
System.out.println("长度为" + dp[chars1.length][chars2.length]);
}
}
运行结果:
?
如果觉得有帮助,点个赞或者收藏鼓励一下作者^。^
|