动态规划--[自用]代码随想录刷题记录【JAVA】

动态规划–代码随想录刷题记录【JAVA】

刷题小白先挖个坑!感兴趣欢迎关注收藏!
文章目的:

  1. 提取出精简的解题思路
  2. 进一步补充不清楚的逻辑分析
  3. 代码部分会多加注释
  4. 方便本人复盘

注:以下绝大多数思路来自代码随想录网站!

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
  1. dp[i]的定义

dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度

  1. 状态转移方程

if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1)

  1. dp[i]的初始化

每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1.

  1. 确定遍历顺序
  • dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来,那么遍历i一定是从前向后遍历。

  • j其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要吧 0 到 i-1 的元素都遍历了就行了。 所以默认习惯 从前向后遍历。

  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]));
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值