动态规划–代码随想录刷题记录【JAVA】
刷题小白先挖个坑!感兴趣欢迎关注收藏!
文章目的:
- 提取出精简的解题思路
- 进一步补充不清楚的逻辑分析
- 代码部分会多加注释
- 方便本人复盘
注:以下绝大多数思路来自代码随想录网站!
文章目录
- 动态规划--代码随想录刷题记录【JAVA】
- 41. 最长递增子序列
- 674. 最长连续递增序列
- 718.最长重复子数组
- 1143.最长公共子序列
- 最大子序和
- 判断子序列
- 不同的子序列
- 两个字符串的删除操作
- 编辑距离 Todo
- 回文子串 Todo
- 最长回文子序列Todo
- 309.最佳买卖股票时机含冷冻期
41. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 :
- 输入:nums = [10,9,2,5,3,7,101,18]
- 输出:4
- 解释:最长递增子序列是 [2,3,7,101],因此长度为 4
- dp[i]的定义
dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
- 状态转移方程
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1)
- dp[i]的初始化
每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1.
- 确定遍历顺序
dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来,那么遍历i一定是从前向后遍历。
j其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要吧 0 到 i-1 的元素都遍历了就行了。 所以默认习惯 从前向后遍历。
- 为何要进行 dp[i] = Math.max(dp[i], dp[j] + 1) 的比较?
- 这个比较的目的就是要决定,是否可以通过把 nums[i] 加入到 nums[j] 结尾的递增子序列中,从而扩展这个子序列的长度。
- 前提条件:nums[i] > nums[j],这意味着 nums[i] 可以接在 nums[j] 后面,形成一个新的递增子序列。
- 因此,dp[i] 要么保持原值(没有发现能加上 nums[i] 的递增子序列),要么更新为新的最大值 dp[j] + 1。
class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length <= 1) return nums.length; //记得可以提前返回!
int[] dp = new int[nums.length];
int res = 1;
Arrays.fill(dp, 1); //数组全部初始化为1
for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
}
674. 最长连续递增序列
class Solution {
public int findLengthOfLCIS(int[] nums) {
if (nums.length <= 1) return nums.length;
int[] dp = new int[nums.length];
Arrays.fill(dp,1);
int result = 0;
for (int i = 1; i < nums.length; i++) { //注意 i从1开始 不然数组会越界
if(nums[i]>nums[i-1]) dp[i]=dp[i-1]+1;
result=Math.max(result,dp[i]);
}
return result;
}
}
718.最长重复子数组
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
- 输入: A: [1,2,3,2,1] B: [3,2,1,4,7]
- 输出:3
- 解释:长度最长的公共子数组是 [3, 2, 1] 。
确定dp数组(dp table)以及下标的含义
- dp[i][j] :以下标i - 1为结尾的A的子串,和以下标j - 1为结尾的B的子串,最长重复子数组长度为dp[i][j]。
- 此时细心的同学应该发现,那dp[0][0]是什么含义呢?总不能是以下标-1为结尾的A数组吧。
- 其实dp[i][j]的定义也就决定着,我们在遍历dp[i][j]的时候i 和 j都要从1开始。
确定递推公式
即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
根据递推公式可以看出,遍历i 和 j 要从1开始!
dp数组如何初始化
根据dp[i][j]的定义,dp[i][0] 和dp[0][j]其实都是没有意义的!
但dp[i][0] 和dp[0][j]要初始值,所以dp[i][0] 和dp[0][j]初始化为0。
dp[1][1] = dp[0][0] +1,只有dp[0][0]初始为0,正好符合递推公式逐步累加起来。
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int result = 0;
int[][] dp = new int[nums1.length + 1][nums2.length + 1];
for (int i = 1; i < nums1.length + 1; i++) {
for (int j = 1; j < nums2.length + 1; j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
result = Math.max(result, dp[i][j]);
}
}
}
return result;
}
}
1143.最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
- 输入:text1 = “abcde”, text2 = “ace”
- 输出:3
- 解释:最长公共子序列是 “ace”,它的长度为 3。
确定dp数组(dp table)以及下标的含义
dp[i][j]:长度为i - 1为结束下标的的字符串text1与长度为 j - 1为结束下标的字符串text2的最长公共子序列为dp[i][j]
确定递推公式
- 如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1
- 如果text1[i - 1] 与 text2[j - 1]不相同,dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
dp数组如何初始化
所以dp[i][0] = dp[0][j]=0
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int[][] dp = new int[text1.length() + 1][text2.length() + 1]; // 先对dp数组做初始化操作
for (int i = 1 ; i <= text1.length() ; i++) {
char char1 = text1.charAt(i - 1);
for (int j = 1; j <= text2.length(); j++) {
char char2 = text2.charAt(j - 1);
if (char1 == char2) { // 开始列出状态转移方程
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[text1.length()][text2.length()];
}
}
最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
- 示例:
- 输入: [-2,1,-3,4,-1,2,1,-5,4]
- 输出: 6
- 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6
确定dp数组(dp table)以及下标的含义
dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]
确定递推公式
- dp[i]只有两个方向可以推出来:
- dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和nums[i]
- 所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);
public static int maxSubArray(int[] nums) {
if (nums.length == 0) {
return 0;
}
int res = nums[0]; // dp初始化
int[] dp = new int[nums.length];
dp[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
res = res > dp[i] ? res : dp[i];
}
return res;
}
判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
- 示例 1:
输入:s = “abc”, t = “ahbgdc” 输出:true - 示例 2:
输入:s = “axc”, t = “ahbgdc” 输出:false
确定dp数组(dp table)以及下标的含义
- dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]
确定递推公式
- if (s[i - 1] == t[j - 1]),那么dp[i][j] = dp[i - 1][j - 1] + 1;,因为找到了一个相同的字符,相同子序列长度自然要在dp[i-1][j-1]的基础上加1
- if (s[i - 1] != t[j - 1]),此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了,即:dp[i][j] = dp[i][j - 1]
结果判断
如果dp[s.size()][t.size()] 与 字符串s的长度相同说明:s与t的最长相同子序列就是s,那么s 就是 t 的子序列
class Solution {
public boolean isSubsequence(String s, String t) {
int length1 = s.length(); int length2 = t.length();
int[][] dp = new int[length1+1][length2+1];
for(int i = 1; i <= length1; i++){
for(int j = 1; j <= length2; j++){
if(s.charAt(i-1) == t.charAt(j-1)){
dp[i][j] = dp[i-1][j-1] + 1;
}else{
dp[i][j] = dp[i][j-1];
}
}
}
if(dp[length1][length2] == length1){
return true;
}else{
return false;
}
}
}
不同的子序列
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。
- 输入:s=“rabbbit”,t="rabbit 输出:3
两个字符串的删除操作
确定dp数组(dp table)以及下标的含义
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
确定递推公式
- 在这一类问题中,我们需要分析两种情况
- 1.s[i - 1] 与 t[j - 1] 相等
- (1) 使用 s[i-1] 来匹配 t[j-1]:这意味着我们已经决定了将 s[i-1] 和 t[j-1] 配对上了,因此剩下的问题是:在 s[0…i-2] 中找到 t[0…j-2] 的子序列匹配,答案是 dp[i-1][j-1]。
- (2) 不使用 s[i-1] 来匹配 t[j-1]:此时,我们相当于跳过了 s[i-1],只考虑 s[0…i-2] 和 t[0…j-1] 的匹配,答案是 dp[i-1][j]
- 比如说: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,即用s[0]s[1]s[2]组成的bag或者s[0]s[1]s[3]组成的bag都可以
- 2.s[i - 1] 与 t[j - 1] 不相等
- 我们不能使用 s[i-1] 来匹配 t[j-1],因此问题转化为在 s[0…i-2] 中找到 t[0…j-1] 的匹配
数组初始化
- dp[i][0]表示什么呢?
- dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数。
- 那么dp[i][0]一定都是1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1。
- 再来看dp[0][j],dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。
那么dp[0][j]一定都是0,s如论如何也变成不了t。- dp[0][0]应该是1,空字符串s,可以删除0个元素,变成空字符串t
class Solution {
public int numDistinct(String s, String t) {
int[][] dp = new int[s.length() + 1][t.length() + 1];
for (int i = 0; i < s.length() + 1; i++) {
dp[i][0] = 1; //只需要初始化为1的部分
}
for (int i = 1; i < s.length() + 1; i++) {
for (int j = 1; j < t.length() + 1; j++) {
if (s.charAt(i - 1) == t.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
}else{
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[s.length()][t.length()];
}
}
编辑距离 Todo
回文子串 Todo
最长回文子序列Todo
309.最佳买卖股票时机含冷冻期
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
- 输入: [1,2,3,0,2]
- 输出: 3
- 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出
状态解读
状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)
不持有股票状态,这里就有两种卖出股票状态
状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
状态三:今天卖出股票
状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
疑惑
问:「今天卖出股票」我是没有单独列出一个状态的归类为「不持有股票的状态」,而本题为什么要单独列出「今天卖出股票」 一个状态呢?
答: 因为本题我们有冷冻期,而冷冻期的前一天,只能是 「今天卖出股票」状态,如果是 「不持有股票状态」那么就很模糊,因为不一定是 卖出股票的操作。
最后的结果
取 状态二,状态三,和状态四的最大值,不少同学会把状态四忘了,状态四是冷冻期,最后一天如果是冷冻期也可能是最大值。
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
if (n == 0) return 0;
// 初始化dp数组
int[][] dp = new int[n][4];
dp[0][0] = -prices[0]; // 持有股票
for (int i = 1; i < n; i++) {
/*
状态一: dp[i][0] 持有股票:
1.维持状态,继续持有 dp[i - 1][0]
2.前一天是冷冻期, dp[i - 1][3] - prices[i]
3.前一天是保持卖出股票的状态 dp[i - 1][1] - prices[i]
*/
dp[i][0] = Math.max(dp[i - 1][0], Math.max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]));
/*
状态二: dp[i][1] 保持卖出股票:
1.保持前一天的状态
2.前一天是冷冻期
*/
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][3]);
/*
状态三: dp[i][2] 今天卖出股票:
1.保持前一天持有股票
*/
dp[i][2] = dp[i - 1][0] + prices[i];
/*
状态四: dp[i][3] 冷冻期:
1.前一天卖出了股票
*/
dp[i][3] = dp[i - 1][2];
}
return Math.max(dp[n - 1][3], Math.max(dp[n - 1][1], dp[n - 1][2]));
}
}