实验要求
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回 0 。
一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的公共子序列是这两个字符串所共同拥有的子序列。输入:text1 = "abcde", text2 = "ace" 输出:3 解释:最长公共子序列是 "ace" ,它的长度为 3 。
目录
1.实验内容
给定两个字符串 text1 和 text2 ,返回这两个字符串的最长公共子序列。如果不存在公共子序列,返回null 。一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是"abcde" 的子序列,但"aec" 不是"abcde" 的子序列。
两个字符串的公共子序列是这两个字符串所共同拥有的子序列。
2.实验目的
用动态规划的方法求解最长公共子序列问题。
3.源码 + 注释
假设字符串text1 和text2 的长度分别为m 和n ,创建m+1 行n+1 列的二维数组dp ,其中dp[i][j] 表示text1[0:i] 和text2[0:j] 的最长公共子序列的长度。创建一个m 行n 列的二维数组path ,其中path[i][j] 表示计算dp[i][j] 时所选择的子问题的最优解。
int[][] path = new int[str1.length()][str2.length()]; //记录路径
int[][] dp = new int[str1.length() + 1][str2.length() + 1];
考虑动态规划的边界情况,当i=0 时,text1[0:i] 为空,空字符串和任何字符串的最长公共子序列的长度都是0,因此对任意0≤j≤n ,有dp[0][j]=0 ;当j=0 时,text2[0:j] 为空,同理可得对任意0≤i≤m ,有dp[i][0]=0 。因此动态规划的边界条件是:当i=0 或j=0 时,dp[i][j]=0 。
//初始化:第0行和第0列都是0
for (int i = 0; i < dp.length; i++)
{
dp[i][0] = 0;
}
for (int i = 0; i < dp[0].length; i++)
{
dp[0][i] = 0;
}
考虑dp[i][j] 的计算:当text1[i-1]=text2[j-1] 时,将这两个相同的字符称为公共字符,考虑text1[0:i-1] 和text2[0:j-1] 的最长公共子序列,再增加一个字符即可得到text1[0:i] 和text2[0:j] 的最长公共子序列,因此dp[i][j]= dp[i-1][j-1]+1 。当text1[i-1]≠text2[j-1] 时,考虑以下两项text1[0:i] 和text2[0:j-1] 的最长公共子序列和text1[0:i-1] 和text2[0:j] 的最长公共子序列,若要得到text1[0:i] 和text2[0:j] 的最长公共子序列,应取两项中长度较大的一项,因此dp[i][j]=max(dp[i][j-1],dp[i-1][j]) 。最终计算得到的dp[m][n] 即为text1 和text2 的最长公共子序列的长度。
在计算dp[i][j] 的过程中,若dp[i][j]= dp[i-1][j-1]+1 ,则path[i-1][j-1]=0 ;若dp[i][j]= dp[i][j-1] ,则path[i-1][j-1]=-1 ;若dp[i][j]= dp[i-1][j] ,则path[i-1][j-1]=1 。
for (int i = 1; i < dp.length; i++)
{
for (int j = 1; j < dp[0].length; j++)
{
//若对应位置字符串相等
if (str1.charAt(i - 1) == str2.charAt(j - 1))
{
dp[i][j] = dp[i - 1][j - 1] + 1;
}
//若对应位置字符串不相等
else if (dp[i - 1][j] >= dp[i][j - 1]) //上面大 path = 1
{
dp[i][j] = dp[i - 1][j];
path[i - 1][j - 1] = 1;
}
else //左面大 path = -1
{
dp[i][j] = dp[i][j - 1];
path[i - 1][j - 1] = -1;
}
}
}
循环结束之后,dp[m][n] 保存了两个字符串的最长公共子序列的长度,path 中保存了最长公共子序列的路径信息。调用printLCS 方法打印最长公共子序列。在该方法中,如果path[i][j]=0 ,则向左上角走;若path[i][j]=1 ,则向上走;若path[i][j]=-1 ,则向左走。在递归返回的时候打印字符,即可得到正序的字符串输出。
private void printLCS(int[][] path, String str, int i, int j)
{
if (i == -1 || j == -1)
{
return;
}
if (path[i][j] == 0) //左上角
{
printLCS(path, str, i - 1, j - 1);
System.out.printf("%c", str.charAt(i));
}
else if (path[i][j] == 1) //向上走
{
printLCS(path, str, i - 1, j);
}
else //向左走
{
printLCS(path, str, i, j - 1);
}
}
4.算法测试结果
令text1=”abcde”,text2=”ace” ,算法的输出为
5.实验过程中遇到的困难及收获
通过本实验加深了对动态规划算法的理解,总结了动态规划算法的一般步骤
1.刻画一个最优解的结构特征
2.递归的定义最优解的值
3.计算最优解的值,通常采用自底向上的方法
4.利用计算出的信息构造一个最优解
对于LCS问题还有优化的空间:由于每个dp[i][j] 项只依赖于dpi-1j、dpij-1、dp[i-1][j-1] 三项,给定一个dp[i][j] ,可以在常数时间内判断出在计算dp[i][j] 时使用了三项中的哪一项,因此可以去掉path 数组,而不会影响重构最优解的工作。
6.实验源代码
.LCS.java
/*
* @Title:
* @Package
* @Description:最长公共子序列
* @author yangf257
* @date 2021/11/16 19:06
*/
public class LCS
{
public void longestCommonSequence(String str1, String str2)
{
int[][] path = new int[str1.length()][str2.length()]; //记录路径
int[][] dp = new int[str1.length() + 1][str2.length() + 1];
//初始化:第0行和第0列都是0
for (int i = 0; i < dp.length; i++)
{
dp[i][0] = 0;
}
for (int i = 0; i < dp[0].length; i++)
{
dp[0][i] = 0;
}
for (int i = 1; i < dp.length; i++)
{
for (int j = 1; j < dp[0].length; j++)
{
//若对应位置字符串相等
if (str1.charAt(i - 1) == str2.charAt(j - 1))
{
dp[i][j] = dp[i - 1][j - 1] + 1;
}
//若对应位置字符串不相等
else if (dp[i - 1][j] >= dp[i][j - 1]) //上面大 path = 1
{
dp[i][j] = dp[i - 1][j];
path[i - 1][j - 1] = 1;
}
else //左面大 path = -1
{
dp[i][j] = dp[i][j - 1];
path[i - 1][j - 1] = -1;
}
}
}
System.out.println("最长长度为" + dp[str1.length()][str2.length()]);
System.out.print("最长公共子序列为");
printLCS(path, str1, str1.length() - 1, str2.length() - 1);
}
private void printLCS(int[][] path, String str, int i, int j)
{
if (i == -1 || j == -1)
{
return;
}
if (path[i][j] == 0) //左上角
{
printLCS(path, str, i - 1, j - 1);
System.out.printf("%c", str.charAt(i));
}
else if (path[i][j] == 1) //向上走
{
printLCS(path, str, i - 1, j);
}
else //向左走
{
printLCS(path, str, i, j - 1);
}
}
}
Test.java
/*
* @Title:
* @Package
* @Description:测试类
* @author yangf257
* @date 2021/11/16 19:35
*/
public class Test
{
public static void main(String[] args)
{
String str1 = "abcde";
String str2 = "ace";
new LCS().longestCommonSequence(str1, str2);
}
}