动态规划–代码随想录刷题记录【JAVA】
刷题小白先挖个坑!感兴趣欢迎关注收藏!
文章目的:
- 提取出精简的解题思路
- 进一步补充不清楚的逻辑分析
- 代码部分会多加注释
- 方便本人复盘
注:以下绝大多数思路来自代码随想录网站!
文章目录
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()];
}
}
53. 最大子序和
给定一个整数数组 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;
}
392. 判断子序列
给定字符串 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;
}
}
}
115. 不同的子序列
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。
- 输入:s=“rabbbit”,t="rabbit 输出:3
583. 两个字符串的删除操作
确定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()];
}
}
72. 编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释: horse -> rorse (将 ‘h’ 替换为 ‘r’) rorse -> rose (删除 ‘r’) rose -> ros (删除 ‘e’)
确定dp数组(dp table)以及下标的含义
dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]
递推公式
情况一:
- if (word1[i - 1] == word2[j - 1])
- 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1];
情况二:
if (word1[i - 1] != word2[j - 1]),此时就需要编辑
操作一:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作
即 dp[i][j] = dp[i - 1][j] + 1
操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作即 dp[i][j] = dp[i][j - 1] + 1
情况三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增删加元素。
- 即 dp[i][j] = dp[i - 1][j - 1] + 1;
不用添加元素吗?
- word2添加一个元素,相当于word1删除一个元素
- 例如 word1 = “ad” ,word2 = “a”,word1删除元素’d’ 和 word2添加一个元素’d’,变成word1=“a”, word2=“ad”, 最终的操作数是一样!
初始化数组
- dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]。
- 那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i;
- 同理dp[0][j] = j;
ublic int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m + 1][n + 1];
// 初始化
for (int i = 1; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= n; j++) {
dp[0][j] = j;
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 因为dp数组有效位从1开始
// 所以当前遍历到的字符串的位置为i-1 | j-1
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
}
}
}
return dp[m][n];
}
回文子串
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:“abc”
输出:3
解释:三个回文子串: “a”, “b”, “c”
dp数组的含义
布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
递推公式
当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。
当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况
情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
情况二:下标i 与 j相差为1,例如aa,也是回文子串
情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
初始化
dp[i][j]初始化为false
遍历顺序
- 情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。
- dp[i + 1][j - 1] 在 dp[i][j]的左下角
- 一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。
class Solution {
public int countSubstrings(String s) {
char[] chars = s.toCharArray();
int len = chars.length;
boolean[][] dp = new boolean[len][len];
int result = 0;
for (int i = len - 1; i >= 0; i--) {
for (int j = i; j < len; j++) { //注意这个遍历顺序!
if (chars[i] == chars[j]) {
if (j - i <= 1) { // 情况一 和 情况二
result++;
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { //情况三
result++;
dp[i][j] = true;
}
}
}
}
return result;
}
}
双指针法
- 在遍历中心点的时候,要注意中心点有两种情况。
- 一个元素可以作为中心点(奇数长度),两个元素也可以作为中心点(偶数长度)。
-那么有人同学问了,三个元素还可以做中心点呢。其实三个元素就可以由一个元素左右添加元素得到,四个元素则可以由两个元素左右添加元素得到。
class Solution {
public int countSubstrings(String s) {
int result = 0;
for (int i = 0; i < s.length(); i++) {
result += extend(s, i, i);
result += extend(s, i, i + 1);
}
return result;
}
private int extend(String s, int i, int j) {
int res = 0;
while (i >= 0 && j < s.length() && s.charAt(i) == s.charAt(j)) {
i--;
j++;
res++;
}
return res;
}
}
516. 最长回文子序列
确定dp数组(dp table)以及下标的含义
dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]
确定递推公式
- 如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2
- 如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度
- 那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
- dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
初始化
- 首先要考虑当i 和j 相同的情况,从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2; 可以看出 递推公式是计算不到 i 和j相同时候的情况。
- 所以需要手动初始化一下,当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。
- 其他情况dp[i][j]初始为0就行,这样递推公式:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); 中dp[i][j]才不会被初始值覆盖。
遍历顺序
- 从递归公式中,可以看出,dp[i][j] 依赖于 dp[i + 1][j - 1] ,dp[i + 1][j] 和 dp[i][j - 1]
- 所以顺序是从下到上,从左到右
class Solution {
public int longestPalindromeSubseq(String s) {
char[] chars = s.toCharArray();
int len =s.length();
int dp[][] = new int[len][len];
for(int i=0;i<len;i++){
dp[i][i]=1;
}
for(int i=len-1;i>=0;i--){
for(int j=i+1;j<len;j++){
if(chars[i]==chars[j]){
dp[i][j]=dp[i+1][j-1]+2;
}else{
dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][len-1];
}
}
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]));
}
}