求字符串s1和s2的最长公共子序列的长度
提示:互联网大厂的经典动态规划题
解决动态规划问题的重要知识!暴力递归的4种经典尝试模型
最关键的就是这个递归函数,解题的核心在这。
互联网大厂的动态规划题目中的四种经典暴力递归尝试模型:
(1)DP1:从左往右的尝试模型,关注i位置结尾,或者i位置开头的情况,或者看i联合i+1,i+2的情况,填表往往是上到下,或者下到上,左到右,右到左。
(2)DP2:从L–R范围上的尝试模型,关注L和R的情况,填表格式非常固定,主对角,副对角,倒回来填
(3)DP3:多样本位置对应的尝试模型,2个样本,一个样本做行,一个样本做列,关注i和j对应位置的情况,先填边界,再填中间
(4)DP4:业务限制类的尝试模型,比如走棋盘,固定的几个方向可以走,先填边界,再填中间。
本题是——DP3:多样本位置对应的尝试模型
题目
求字符串s1和s2的最长公共子序列的长度
所谓子序列,就是字符串中不一定连续位置的字符的组合
公共子序列:是2个字符串中公共有的子序列
最长公共子序列:就是2个字符串中最长的那个子序列,其长度就是咱们要求的结果。
一、审题
示例:
s1=ab1cd2ef345gh
s2=opq123rs4tx5yz
其最长公共子序列是:12345,长度ans=5;
暴力递归尝试DP3:多样本位置对应模型
敏感度:题目,给你2字符串,今后题目给你俩数组,俩个样本,对比进行的题目,显然就是多样本,让你用一个样本做行,一个样本做列,故你要敏感地想到,这就是考你动态规划题目的DP3:多样本位置对应模型
故暴力递归,就知道咋尝试了
显然咱可以这么做:
定义暴力递归函数**f(s1,i, s2,j)**为求:从s1的0–i范围与s2的0–j范围上的最长公共子序列的长度是?
主函数咋调用?f(s1,N-1, s2,M-1),N是s1长度,M是s2长度
多样本位置模型,关注的是i和j结尾的情况如何如何?
那咱们就来分析一下,究竟f(s1,i, s2,j)是如何处理的:
(1)最长公共子序列common:不以 s1的i位置结尾,不以 s2的j位置结尾,那相当于扩展i和j没意义
所以结果ans应该是f(s1,i - 1, s2,j - 1)
(2)最长公共子序列common:不以 s1的i位置结尾,以 s2的j位置结尾,那相当于扩展i没意义
所以结果ans应该是f(s1,i - 1, s2,j)
(3)最长公共子序列common:以 s1的i位置结尾,不以 s2的j位置结尾,那相当于扩展j没意义
所以结果ans应该是f(s1,i, s2,j - 1)
(4)最长公共子序列common:以 s1的i位置结尾,以 s2的j位置结尾,那必然是当**s1[i]=s2[j]**才行(这个条件千万别漏掉!!!)
所以结果ans应该是f(s1,i - 1, s2,j - 1) + 1
也就这么几种情况,以上四种情况取最大值。
(0)最基本的情况就是当i=0,切j=0时的状况,看看俩相等与否
另外,看i=0,整行,如果s2的0–j-1范围中有一个s1的i字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
另外,看j=0,整列,如果s1的0–i-1中有一个s2的j字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
这很好理解吧?
okay,咱们逻辑已经捋清了,所以手撕暴力递归的代码岂不是非常非常非常非常简单了!!!
//定义暴力递归函数**f(s1,i, s2,j)**为求:**从s1的0--i范围与s2的0--j范围上的最长公共子序列的长度**是?
public static int f(char[] str1, char[] str2, int i, int j){
//(0)最基本的情况就是当i=0,切j=0时的状况,看看俩相等与否
if (i == 0 && j == 0) return str1[i] == str2[j] ? 1 : 0;
//另外,看i=0,整行,如果s2的0--j-1范围中有一个s1的i字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
if (i == 0 && j > 0) return Math.max(f(str1, str2, i, j - 1), str1[i] == str2[j] ? 1 : 0);
//另外,看j=0,整列,如果s1的0--i-1中有一个s2的j字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
if (j == 0 && i > 0) return Math.max(f(str1, str2, i - 1, j), str1[i] == str2[j] ? 1 : 0);
//其余ij
//(1)最长公共子序列common:**不以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展i和j没意义
//所以结果ans应该是f(s1,i - 1, s2,j - 1)
int p1 = f(str1, str2, i - 1, j - 1);
//(2)最长公共子序列common:**不以** s1的i位置结尾,**以** s2的j位置结尾,那相当于扩展i没意义
//所以结果ans应该是f(s1,i - 1, s2,j)
int p2 = f(str1, str2, i - 1, j);
//(3)最长公共子序列common:**以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展j没意义
//所以结果ans应该是f(s1,i, s2,j - 1)
int p3 = f(str1, str2, i, j - 1);
//(4)最长公共子序列common:**以** s1的i位置结尾,**以** s2的j位置结尾,那必然是**s1[i]=s2[j]**才行
//所以结果ans应该是f(s1,i - 1, s2,j - 1) **+ 1**
int p4 = -1;
if (str1[i] == str2[j]) p4 = f(str1, str2, i - 1, j - 1) + 1;
//也就这么几种情况,以上四种情况取最大值。
return Math.max(Math.max(p1, p2), Math.max(p3, p4));
}
//主函数调用:
public static int maxLenOflongestCommonSubString(String s1, String s2){
if (s1.compareTo("") == 0 || s1.length() == 0 ||
s2.compareTo("") == 0 || s2.length() == 0) return 0;
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
return f(str1, str2, str1.length - 1, str2.length - 1);
}
public static void test(){
String s1="ab1cd2ef345gh";
String s2="opq123rs4tx5yz";
// String s1="abcde";
// String s2="abcdf";
System.out.println(maxLenOflongestCommonSubString(s1, s2));
}
public static void main(String[] args) {
test();
}
测试时速度很慢的5
修改记忆化搜索法:傻缓存dp表跟随暴力递归代码
记忆化搜索法,无非就是用一个dp表,把可能的i和j的各种状态组合的最佳结果,存到表格dp[i][j]中,
如果递归过程中,发现dp[i][j]已经求过了,就不要暴力递归继续去求了,直接返回dp[i][j]省时间
咱就来准备一个dp
由于暴力递归就2个变量i和j自然就是填写一个二维的表
i取0–N-1
j取0–M-1
故dp是N×M维的矩阵
dp[i][j]的含义就是暴力递归的含义:从s1的0–i范围与s2的0–j范围上的最长公共子序列的长度是?
怎么填,完全根据暴力递归的代码来写
//改为傻缓存dp表:
// 定义暴力递归函数**f(s1,i, s2,j)**为求:
// **从s1的0--i范围与s2的0--j范围上的最长公共子序列的长度**是?
public static int fDP(char[] str1, char[] str2, int i, int j, int[][] dp){
if (dp[i][j] != -1) return dp[i][j];
//(0)最基本的情况就是当i=0,切j=0时的状况,看看俩相等与否
if (i == 0 && j == 0) {
dp[i][j] = str1[i] == str2[j] ? 1 : 0;
return dp[i][j];
}
//另外,看i=0,整行,如果s2的0--j-1范围中有一个s1的i字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
if (i == 0 && j > 0) {
dp[i][j] = Math.max(fDP(str1, str2, i, j - 1, dp), str1[i] == str2[j] ? 1 : 0);
return dp[i][j];
}
//另外,看j=0,整列,如果s1的0--i-1中有一个s2的j字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
if (j == 0 && i > 0){
dp[i][j] = Math.max(fDP(str1, str2, i - 1, j, dp), str1[i] == str2[j] ? 1 : 0);
return dp[i][j];
}
//其余ij
//(1)最长公共子序列common:**不以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展i和j没意义
//所以结果ans应该是f(s1,i - 1, s2,j - 1)
int p1 = fDP(str1, str2, i - 1, j - 1, dp);
//(2)最长公共子序列common:**不以** s1的i位置结尾,**以** s2的j位置结尾,那相当于扩展i没意义
//所以结果ans应该是f(s1,i - 1, s2,j)
int p2 = fDP(str1, str2, i - 1, j, dp);
//(3)最长公共子序列common:**以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展j没意义
//所以结果ans应该是f(s1,i, s2,j - 1)
int p3 = fDP(str1, str2, i, j - 1, dp);
//(4)最长公共子序列common:**以** s1的i位置结尾,**以** s2的j位置结尾,那必然是**s1[i]=s2[j]**才行
//所以结果ans应该是f(s1,i - 1, s2,j - 1) **+ 1**
int p4 = -1;
if (str1[i] == str2[j]) p4 = fDP(str1, str2, i - 1, j - 1, dp) + 1;
//也就这么几种情况,以上四种情况取最大值。
dp[i][j] = Math.max(Math.max(p1, p2), Math.max(p3, p4));
return dp[i][j];
}
//主函数调用:
public static int maxLenOflongestCommonSubStringDP(String s1, String s2){
if (s1.compareTo("") == 0 || s1.length() == 0 ||
s2.compareTo("") == 0 || s2.length() == 0) return 0;
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
int N = str1.length;
int M = str2.length;
int[][] dp = new int[N][M];
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
dp[i][j] = -1;//初始化
}
}
return fDP(str1, str2, str1.length - 1, str2.length - 1, dp);
}
public static void test(){
String s1="ab1cd2ef345gh";
String s2="opq123rs4tx5yz";
// String s1="abcde";
// String s2="abcdf";
// System.out.println(maxLenOflongestCommonSubString(s1, s2));
System.out.println(maxLenOflongestCommonSubStringDP(s1, s2));
}
public static void main(String[] args) {
test();
}
秒杀,瞬间出结果:5
暴力递归到精细化动态规划填表dp:转移方程
那么既然咱有了暴力递归代码
很容易就能直接改为精细化填写dp表的动态规划方法:
咱就来准备一个dp
由于暴力递归就2个变量i和j自然就是填写一个二维的表
i取0–N-1
j取0–M-1
故dp是N×M维的矩阵
dp[i][j]的含义就是暴力递归的含义:从s1的0–i范围与s2的0–j范围上的最长公共子序列的长度是?
咱们咋求每一个格子呢?
自然是根据暴力递归的代码来求
//(0)最基本的情况就是当i=0,切j=0时的状况,看看俩相等与否
if (i == 0 && j == 0) return str1[i] == str2[j] ? 1 : 0;
//另外,看i=0,整行,如果s2的0--j-1范围中有一个s1的i字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
if (i == 0 && j > 0) return Math.max(f(str1, str2, i, j - 1), str1[i] == str2[j] ? 1 : 0);
//另外,看j=0,整列,如果s1的0--i-1中有一个s2的j字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
if (j == 0 && i > 0) return Math.max(f(str1, str2, i - 1, j), str1[i] == str2[j] ? 1 : 0);
这三行代码,足够填dp表中0 0 格子,0行0列
根据其余ij的代码:
```java
//(1)最长公共子序列common:**不以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展i和j没意义
//所以结果ans应该是f(s1,i - 1, s2,j - 1)
int p1 = f(str1, str2, i - 1, j - 1);
//(2)最长公共子序列common:**不以** s1的i位置结尾,**以** s2的j位置结尾,那相当于扩展i没意义
//所以结果ans应该是f(s1,i - 1, s2,j)
int p2 = f(str1, str2, i - 1, j);
//(3)最长公共子序列common:**以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展j没意义
//所以结果ans应该是f(s1,i, s2,j - 1)
int p3 = f(str1, str2, i, j - 1);
//(4)最长公共子序列common:**以** s1的i位置结尾,**以** s2的j位置结尾,那必然是**s1[i]=s2[j]**才行
//所以结果ans应该是f(s1,i - 1, s2,j - 1) **+ 1**
int p4 = -1;
if (str1[i] == str2[j]) p4 = f(str1, str2, i - 1, j - 1) + 1;
//也就这么几种情况,以上四种情况取最大值。
return Math.max(Math.max(p1, p2), Math.max(p3, p4));
可知,其余dp[i][j](表格中粉色五角星那),其实根据暴力递归中的(1)(2)(3)(4)四种情况的最大值,发现,dp[i][j]依赖:
i - 1, j - 1格子【ij的左上角】
i - 1, j格子【ij的上方格子】
i, j - 1格子【ij的左方格子】
最后咱们要的是红色五角星那个格子为结果:ans = dp[N - 1][M - 1]
因此,我们填写dp的代码就很容易手撕了:
//精细化DP
//主函数:
public static int maxLenOflongestCommonSubStringDP2(String s1, String s2){
if (s1.compareTo("") == 0 || s1.length() == 0 ||
s2.compareTo("") == 0 || s2.length() == 0) return 0;
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
int N = str1.length;
int M = str2.length;
int[][] dp = new int[N][M];
//(0)最基本的情况就是当i=0,切j=0时的状况,看看俩相等与否
dp[0][0] = str1[0] == str2[0] ? 1 : 0;
//另外,看i=0,整行,如果s2的0--j-1范围中有一个s1的i字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
for (int j = 1; j < M; j++) {//填写0行
dp[0][j] = Math.max(dp[0][j - 1], str1[0] == str2[j] ? 1 : 0);
}
//另外,看j=0,整列,如果s1的0--i-1中有一个s2的j字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
for (int i = 1; i < N; i++) {//填写0列
dp[i][0] = Math.max(dp[i - 1][0], str1[i] == str2[0] ? 1 : 0);
}
//其余ij
for (int i = 1; i < N; i++) {
for (int j = 1; j < M; j++) {
//(1)最长公共子序列common:**不以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展i和j没意义
//所以结果ans应该是f(s1,i - 1, s2,j - 1)
int p1 = dp[i - 1][j - 1];
//(2)最长公共子序列common:**不以** s1的i位置结尾,**以** s2的j位置结尾,那相当于扩展i没意义
//所以结果ans应该是f(s1,i - 1, s2,j)
int p2 = dp[i - 1][j];
//(3)最长公共子序列common:**以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展j没意义
//所以结果ans应该是f(s1,i, s2,j - 1)
int p3 = dp[i][j - 1];
//(4)最长公共子序列common:**以** s1的i位置结尾,**以** s2的j位置结尾,那必然是**s1[i]=s2[j]**才行
//所以结果ans应该是f(s1,i - 1, s2,j - 1) **+ 1**
int p4 = -1;
if (str1[i] == str2[j]) p4 = p1 + 1;
//也就这么几种情况,以上四种情况取最大值。
dp[i][j] = Math.max(Math.max(p1, p2), Math.max(p3, p4));
}
}
return dp[N - 1][M - 1];
}
public static void test(){
String s1="ab1cd2ef345gh";
String s2="opq123rs4tx5yz";
// String s1="abcde";
// String s2="abcdf";
// System.out.println(maxLenOflongestCommonSubString(s1, s2));
// System.out.println(maxLenOflongestCommonSubStringDP(s1, s2));
System.out.println(maxLenOflongestCommonSubStringDP2(s1, s2));
}
public static void main(String[] args) {
test();
}
是不是很简单,这些转移方程都不是凭空想想出来的,完全是根据暴力递归代码改写的!
结果5
总结
提示:重要经验:
1)动态规划的关键在暴力递归的尝试,有4个模型
2)暴力递归在笔试要改为记忆化搜索方案(傻缓存dp表跟随),面试要改为精细化动态规划填写dp表,把转移方程搞出来
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。