目录
简介
题目:
思路:
递归版本:
根据递归 分析推导
动态规划版本:
简介
前面刷了几道题目,都是从暴力递归到递归+动态规划的版本,最后演变成纯动态规划的版本。接下来的题目,将会跳过 递归 + 动态规划的版本。而是转头直接分享递归版本和纯动态规划版本。其实,动态规划是有技巧的,只要递归版本写的好,直接改成动态规划是手到擒来的一件事情,非常的简单。而动态规划,也是分模型的。即从左到右模型,范围模型,样本模型,业务模型。以后刷题的时候,可以直接套用这些模型,思路会清晰很多。
今天,我们来分析 样本模型。而样本模型的套路,就是直接套路样本数据的最后一个元素的无限可能,即存在的各种各样的可能性。以样本数据的最后一个元素即基础,进行分析。
题目:
https://leetcode.com/problems/longest-common-subsequence/
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
思路:
1. 如果样本数据都不存在,即无效的样本。
2. 如果一个样本长度为1,另一个样本很长,则长度最多为1,也有可能为0;
3. 如果2个样本数据都很长,那就逐层讨论。每次递归,都以当前样本的最后一个元素,讨论样本存在的可能性。最后把所有的样本数据拿出来比较,找到符合条件的数据即可,
递归版本:
package code03.动态规划_07;
/**
* 链接:https://leetcode.com/problems/longest-common-subsequence/
*
* 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
*
* 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
*
* 给你一些样本数据,要求找出共性的,就属于样本模型。 而样本模型的核心,就是以最后一个字符为基础进行讨论
*/
public class LongestCommonSubsequence_04_opt {
public static int longestCommonSubsequence(String text1, String text2)
{
if (text1 == null || text2 == null || text1.isEmpty() || text2.isEmpty()) {
return 0;
}
char[] s1 = text1.toCharArray();
char[] s2 = text2.toCharArray();
//以s1做行,s2做列
int[][] dp = new int[ s1.length][s2.length];
int index1 = s1.length - 1;
int index2 = s2.length - 1;
//根据递归 if (index1 == 0 && index2 == 0) 而来
dp[0][0] = s1[0] == s2[0] ? 1 : 0;
//根据递归 else if (index1 == 0) 而来, 此处代表先处理 列
for (int i = 1; i <= index2; i++) {
dp[0][i] = s1[0] == s2[i] ? 1 : dp[0][i-1];
}
//根据递归 else if (index2 == 0) 而来, 此处代表先处理 行
for (int j = 1; j <= index1; j++) {
dp[j][0] = s1[j] == s2[0] ? 1 : dp[j-1][0];
}
//通用case 根据递归中最后一个else而来
for (int row = 1; row <= index1; row++) {
for (int col = 1; col <= index2; col++) {
//根据 int p1 = process(s1, s2, index1, index2-1) 改写
int p1 = dp[row][col - 1];
//根据 int p2 = process(s1, s2, index1-1, index2) 改写
int p2 = dp[row -1][col];
//int p3 = s1[index1] == s2[index2] ? (process(s1, s2, index1-1, index2-1) + 1) : 0;
int p3 = s1[row] == s2[col] ? (dp[row -1][col -1] + 1) : 0;
dp[row][col] = Math.max(p1, Math.max(p2, p3));
}
}
//返回值对应递归中的下标
return dp[index1][index2];
}
public static void main(String[] args) {
String s1 = "abcde";
String s2 = "ace";
System.out.println(longestCommonSubsequence(s1,s2));
}
}
递归好不好,是动态规划的关键。如果递归写不好,那么动态规划也就会非常的复杂,甚至完全没有版本写出来。因此,尝试写好一个递归,才是动态规划的关键所在。
根据递归 分析推导
1. 假设样本1数据为 ace 样本2数据为abcde得到的结果应该是3
2. 以样本1作行,样本2作列绘制二维数组。可得到以下表 (样本1或2,即可作行,也可左列)
a(0) | b(1) | c(2) | d(3) | e(4) | |
a(0) | |||||
b(1) | |||||
c(2) |
3. 根据递归内部
if (index1 == 0 && index2 == 0) { return s1[index1] == s2[index2] ? 1 : 0; }
可知数组dp[0][0] 为1或者0. 而样本开头都是a,因此此位置为 1
a(0) | b(1) | c(2) | d(3) | e(4) | |
a(0) | 1 | ||||
b(1) | |||||
c(2) |
4. 根据递归内部
else if (index1 == 0) { //如果数组1以当前下标结尾,而数组2不确定 对应解题思路2. 如果一个样本长度为1,另一个样本很长,则长度最多为1,也有可能为0; //如果当前下标的字符数组相等,此时text1已经结束了并且找到了和text2相等的字符,直接返回1. //如果没找到,继续找text2的字符 return s1[index1] == s2[index2] ? 1 : process(s1, s2, index1, index2-1);
由于index1对应的是样本1的数据,即行数据。而index2对应的是样本2数据,即列数据。此处列的下标对应的为0,也就是说此处可以推导出行下标为0对应的所有的列。如果相等则为1, 不相等则继续递归,依次类推。
0行 0 列已经推导出为 1,即 dp[0][0] = 1;
a != e 依赖index2 - 1
a != d 依赖 index2 - 1
a != c 依赖 index2 - 1
a != b 依赖 index2 - 1
a == a 得到1,所以第一行都为1
a(0) | b(1) | c(2) | d(3) | e(4) | |
a(0) | 1 | 1 | 1 | 1 | 1 |
b(1) | |||||
c(2) |
5. 根据递归内部:
else if (index2 == 0) { //数组2来到结尾,数组1不一定。 对应解题思路2. 如果一个样本长度为1,另一个样本很长,则长度最多为1,也有可能为0; //原理同上。此时继续往下找的是字符数组text1 return s1[index1] == s2[index2] ? 1 :process(s1, s2, index1-1, index2); }
推导方法一样:
a != c 依赖上一行 index1 - 1
a != b 依赖上一行 index1 - 1
a == a 之前推导过的是1, 因此,此列都为1
a(0) | b(1) | c(2) | d(3) | e(4) | |
a(0) | 1 | 1 | 1 | 1 | 1 |
b(1) | 1 | ||||
c(2) | 1 |
6. 最后推导
//以第一个数组结尾结束,第二个不确定。 就是上方的index1==0的过程 int p1 = process(s1, s2, index1, index2-1); //以第二个数组结尾结束,第一个不确定。 index2==0的过程 int p2 = process(s1, s2, index1-1, index2); //同时以数组1的当前下标index1 和 数组2的当前下标index2结尾。 //数组1为 ab123c45d //数组2位 123abc45d //最长公共子序为 12345 //等价与上方的case index1 == 0 && index2 == 0 int p3 = s1[index1] == s2[index2] ? (process(s1, s2, index1-1, index2-1) + 1) : 0; return Math.max(p1, Math.max(p2, p3));
a(0) | b(1) | c(2) | d(3) | e(4) | |
a(0) | 1 | 1 | 1 | 1 | 1 |
c(1) | 1 | 1 | 2 | ||
e(2) | 1 |
a(0) | b(1) | c(2) | d(3) | e(4) | |
a(0) | 1 | 1 | 1 | 1 | 1 |
c(1) | 1 | 1 | 2 | 2 | 2 |
e(2) | 1 | 1 | 2 | 2 |
最后一步: p1 依赖上一列 p2 依赖前一列,p3是判断是否相等。如果相等,则是获取
上一行和上一列的max值,并且加 1。 即得到 2 + 1 = 3;
a(0) | b(1) | c(2) | d(3) | e(4) | |
a(0) | 1 | 1 | 1 | 1 | 1 |
c(1) | 1 | 1 | 2 | 2 | 2 |
e(2) | 1 | 1 | 2 | 2 | 3 |
由于递归传入的参数是: process(s1, s2, text1.length()-1, text2.length()-1); 所以最终得到的数据是dp[2][4], 即3. 由此,我们设计动态规划的代码:
动态规划版本:
package code03.动态规划_07;
/**
* 链接:https://leetcode.com/problems/longest-common-subsequence/
*
* 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
*
* 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
*
* 给你一些样本数据,要求找出共性的,就属于样本模型。 而样本模型的核心,就是以最后一个字符为基础进行讨论
*/
public class LongestCommonSubsequence_04_opt {
public static int longestCommonSubsequence(String text1, String text2)
{
if (text1 == null || text2 == null || text1.isEmpty() || text2.isEmpty()) {
return 0;
}
char[] s1 = text1.toCharArray();
char[] s2 = text2.toCharArray();
//以s1做行,s2做列
int[][] dp = new int[ s1.length][s2.length];
int index1 = s1.length - 1;
int index2 = s2.length - 1;
//根据递归 if (index1 == 0 && index2 == 0) 而来
dp[0][0] = s1[0] == s2[0] ? 1 : 0;
//根据递归 else if (index1 == 0) 而来, 此处代表先处理 第一行的所有列
for (int i = 1; i <= index2; i++) {
dp[0][i] = s1[0] == s2[i] ? 1 : dp[0][i-1];
}
//根据递归 else if (index2 == 0) 而来, 此处代表先处理 第一列的所有行
for (int j = 1; j <= index1; j++) {
dp[j][0] = s1[j] == s2[0] ? 1 : dp[j-1][0];
}
//通用case 根据递归中最后一个else而来
for (int row = 1; row <= index1; row++) {
for (int col = 1; col <= index2; col++) {
//根据 int p1 = process(s1, s2, index1, index2-1) 改写
int p1 = dp[row][col - 1];
//根据 int p2 = process(s1, s2, index1-1, index2) 改写
int p2 = dp[row -1][col];
//int p3 = s1[index1] == s2[index2] ? (process(s1, s2, index1-1, index2-1) + 1) : 0;
int p3 = s1[row] == s2[col] ? (dp[row -1][col -1] + 1) : 0;
dp[row][col] = Math.max(p1, Math.max(p2, p3));
}
}
//返回值对应递归中的下标
return dp[index1][index2];
}
public static void main(String[] args) {
String s1 = "ace";
String s2 = "abcde";
System.out.println(longestCommonSubsequence(s1,s2));
}
}
试想一下,如果没有递归做辅助,这些动态规划的代码该如何写。甚至写好了,我们完全搞不清楚这些代码的意思。
为啥
dp[0][0] = s1[index1] == s2[index1] ? 1 : 0;
为啥要求以下三种情况的最大值
//根据 int p1 = process(s1, s2, index1, index2-1) 改写 int p1 = dp[row][col - 1]; //根据 int p2 = process(s1, s2, index1-1, index2) 改写 int p2 = dp[row -1][col]; //int p3 = s1[index1] == s2[index2] ? (process(s1, s2, index1-1, index2-1) + 1) : 0; int p3 = s1[row] == s2[col] ? (dp[row -1][col -1] + 1) : 0;
dp[row][col] = Math.max(p1, Math.max(p2, p3));
为啥最后要返回的值 下标 是下列这样的
return dp[index1 - 1][index2 - 1];
没有递归做辅助,这一些的为啥可能根本就解释不清楚。更别谈写出动态规划的代码了。
由于这是一道力扣原题,结果是动态规划版本可以顺利通过,而递归版本一直是超时,这也能够说明不同的业务场景,不用的写法,是非常大重要的。