其他分享
首页 > 其他分享> > leetcode-hot100-动态规划

leetcode-hot100-动态规划

作者:互联网

文章目录

5. 最长回文子串 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i][j]表示从[i,j]能的字符串是否能组成回文子串

  1. 子问题

dp[i][j]是否是回文子串取决于其子串是否是回文的。
即:如果dp[i][j]是回文子串,那么其子串必然是回文的。
既然是回文的,那么必然首尾相等。

  • s[i] == s[j],首尾相等,此时只需要关注子串,即判断dp[i + 1][j - 1]是否是回文的。当j - 1 - (i + 1) + 1 < 2的时候,说明[i, j]的长度为1,那么1个字符肯定是回文的,所以此时还需要对这个区间的长度进行判断
  • 不相等,直接判定为false。
  1. 状态转移方程

dp[i][j] = dp[i + 1][j - 1] s[i] == s[j]

  1. 初始化和边界

dp[i][i] = true 即:单个字符也是回文的
初始化dp的大小为dp[n][n],因为在最后的时候只需要考虑到n-1位置即可,同样第二维的长度也是n,这样就凑齐了一个矩阵,这个矩阵中对角线肯定是true的。
为什么第二维不是2?
因为二维限定死了,要么是回文要么不是回文,这样就在对角线上无法得到一个回文的意思

class Solution {
    public String longestPalindrome(String s) {
        int n = s.length();
        if(n < 2){
            return s;
        }
        // dp: dp[i][j]表示字符串s在范围i~j是否为一个回文串
        boolean[][] dp = new boolean[n][n];
        for(int i = 0; i < n; i++){
            dp[i][i] = true;
        }
        int begin = 0;
        int maxLen = 1;
        String res = "";
        char[] ss = s.toCharArray();
        for(int j = 1; j < n; j++){
            for(int i = 0; i < j; i++){
                if(ss[i] == ss[j]){
                    if(j - i < 3){
                        dp[i][j] = true;
                    }else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }else {
                    dp[i][j] = false;
                }
                if(dp[i][j] && j - i + 1 > maxLen){
                    begin = i;
                    maxLen = j - i + 1;
                }
            }
            
        }
        return s.substring(begin, begin + maxLen);
    }
}

647. 回文子串 - 力扣(LeetCode) (leetcode-cn.com)

  1. 思路基本上和上一题差不多。有一些区别。
  2. 在因为是求回文子串,本题求的是个数,所以,最左端的指针是可以移动到最右端,和最右端指针重合的,所以两指针都是从最左端移动得到的。
class Solution {
    public int countSubstrings(String s) {
        int n = s.length();
        // dp定义: dp[i][j]表示字符串在i~j范围是否是一个回文子串
        boolean[][] dp = new boolean[n][n];
        int res = 0;
        char[] ss = s.toCharArray();
        //这里移动的初始位置就是区别。
        for(int j = 0; j < n; j++){
            for(int i = 0; i <= j; i++){
                if(ss[i] == ss[j]){
                    if(j - i < 3){
                        dp[i][j] = true;
                    }else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }else {
                    dp[i][j] = false;
                }
            }
        }
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++){
                if(dp[i][j]){
                    res++;
                }
            }
        }
        return res;
    }
}

10. 正则表达式匹配 - 力扣(LeetCode) (leetcode-cn.com)

题解推荐:宫水三叶

  1. 单字符匹配:需要和s字符串的同一位置字符完全匹配。
  2. '.'匹配:可以匹配s字符串同一位置的任意字符。
  3. '*'匹配:不能单独的匹配,必须和前一个字符搭配使用。表示能匹配同一位置字符多次。
  1. 状态定义:

dp[i][j]表示字符串s中以i结尾的子串能否与p中以j结尾的子串匹配。

  1. 子问题:

dp[i][j]状态取决于s[i]是否匹配p[j],若匹配,那么状态:dp[i][j] = dp[i - 1][j - 1],那么此时就涉及到了是三个匹配中的哪个匹配了。
1. 如果是字符匹配,即:普通匹配,那么:dp[i][j] = dp[i - 1][j - 1] && s[i] == p[j]
2. 如果是'.'匹配,说明:如果是符号匹配的话,那么只能是p字符串的j位置,因为是用p去匹配s字符串的,那么:dp[i][j] = dp[i - 1][j - 1] && p[j] == '.'
3. 如果p[j] == '*',表示可以匹配0个或者多个前一个位置的元素,此时就需要考虑p[j - 1]s[i]的关系,只有当p[j - 1] == s[i]才能继续向下匹配。
1. 匹配0个:即:p[j - 1] != s[i],此时就需要再向前看一个字符,即:dp[i][j] = dp[i][j - 2]
2. 匹配1个:即:p[j - 1] == s[i],此时p[j - 1]可能是字符也可能是'.',所以,此时的dp[i][j] = dp[i - 1][j - 2] && ((s[i] == p[j - 1]) || p[j - 1] == '.')
3. 匹配2及以上:说明p[j - 1] == s[i] && p[j - 1] == s[i - 1],此时dp[i][j] = dp[i - 2][j - 2] && ((s[i] == p[j - 1] && s[i - 1] == p[j - 1]) || p[j - 1] == '.')

  1. 转移方程:
  1. p[j] == s[i]dp[i][j] = dp[i - 1][j - 1]
  2. p[j] == '.'dp[i][j] = dp[i-1][j-1]
  3. p[j] == '*',那么又需要分没有匹配以及匹配:
    1. p[j - 1] != s[i]dp[i][j] = dp[i][j-2]
      2. p[j - 1] == s[i]或者p[j - 1] == '.'
      1. 空匹配:dp[i][j] = dp[i][j-2]
      2. 一个匹配:dp[i][j] = dp[i][j-1] && (s[i] == p[j - 1] || p[j - 1] == '.')
      3. 多个匹配:dp[i][j] = dp[i-2][j - 2] ((s[i] == p[j - 1] && s[i - 1] == p[j - 1]) || p[j - 1] == '.'
      4. …
      5. 表达式展开:盗一波三叶姐的图,
      在这里插入图片描述
  1. 边界以及初始化:
  • 初始默认都没有一个可以进行匹配,并且使用空串对两个字符串进行拼接一下,这样有利于后面的滚动。
  • 边界:dp[0][0]为true,表示s以0结尾的子串可以匹配p中以0结尾的子串,两个空串肯定能互相匹配。
class Solution {
    public boolean isMatch(String s, String p) {
        int n = s.length();
        int m = p.length();
        s = " " + s;
        p = " " + p;
        char[] ss = s.toCharArray();
        char[] pp = p.toCharArray();
        boolean[][] dp = new boolean[n + 1][m + 1];
        dp[0][0] = true;
        //s可能是空串
        for(int i = 0; i <= n; i++){
            for(int j = 1; j <= m; j++){
            	// 如果是'*' 的话,不能是第一个
            	// 需要和前面字符进行配合,所以是j + 1
                if(j + 1 <= m && pp[j + 1] == '*'){
                    continue;
                }

                if(i - 1 >= 0 && pp[j] != '*'){
                //字符匹配以及'.'匹配
                    dp[i][j] = (dp[i - 1][j - 1] && ss[i] == pp[j]) 
                            || (dp[i - 1][j - 1] && pp[j] == '.');
                }else if(pp[j] == '*'){
                // 这里不能直接写else
                //前面是0匹配,后面是匹配
                    dp[i][j] = (j - 2 >= 0 && dp[i][j - 2]) || 
                        	   (i - 1 >= 0 && dp[i - 1][j] && (ss[i] == pp[j - 1] || pp[j - 1] == '.'));
                }
                
            }
        }
        return dp[n][m];
    }
}

53. 最大子序和 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i]表示数组到下标位置i的最大和。

  1. 子问题:

最终问题是由上一个问题决定是否加上当前数判定的。所以子问题也是最大和,只不过是到下标i - 1位置。

  1. 状态转移方程:

dp[i] = max(dp[i - 1] + num, num)

  1. 边界和初始化:

因为最后是要返回最大和,设置一个变量用来记录,同时为了防止只有一个数,所以初始值就是nums[0]
dp的大小为n,因为最后只会计算到n-1位置的。

class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        // dp[i]:表示数组i位置的最大和
        int[] dp = new int[n];
        dp[0] = nums[0];
        int res = nums[0];
        for(int i = 1; i < n; i++){
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

62. 不同路径 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i][j]表示为走到数组(i,j)位置,总共有多少种不同的路径。

  1. 子问题:

因为每次只能向下或者向右移动,所以要移动到m * n位置,要么是从它的右边移动过去,要么就是从它的上边移动过去。

  1. 转移方程:

dp[i][j] = dp[i - 1][j] + dp[i][j - 1]

  1. 边界和初始化:

边界:如果只有一行或者一列的时候,那么只有一种走法。
初始化:dp数组大小为[m][n],因为最后实际上返回的是[m - 1][n - 1]

class Solution {
    public int uniquePaths(int m, int n) {
        // dp定义:dp[i][j]表示走到网格(i,j)位置总有有多少种不同的路径
        int[][] dp = new int[m][n];
        //在写只有一行或者一列的时候很容易写错,稍微想想就能想明白
        for(int i = 0; i < m; i++){
            dp[i][0] = 1;
        }
        for(int j = 0; j < n; j++){
            dp[0][j] = 1;
        }
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m - 1][n - 1];
    }
}

64. 最小路径和 - 力扣(LeetCode) (leetcode-cn.com)

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[][] dp = new int[m][n];
        dp[0][0] = grid[0][0];
        for(int i = 1; i < n; i++){
            dp[0][i] = dp[0][i - 1] + grid[0][i];
        }
        for(int j = 1; j < m; j++){
            dp[j][0] = dp[j - 1][0] + grid[j][0];
        }
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
            }
        }
        return dp[m - 1][n - 1];
    }
}

70. 爬楼梯 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i]表示跳到i阶楼梯有多少中方法

  1. 子问题:

因为每次要么爬1个阶梯,要么爬2个阶梯所以爬到i阶梯的时候要么前面爬了i-1个阶梯,要么前面爬了i-2个阶梯所以到i阶梯的时候,是这两个之和。

  1. 状态转移方程:

dp[i] = dp[i - 1] + dp[i - 2]

  1. 边界和初始化:
  • 边界:dp[0] = 1 ,在0阶梯,只需要1步就行;dp[1] = 1,在1阶梯,爬1个阶梯也到顶。
  • 初始化:最后返回的时候返回dp[n],因为需要计算爬到第n阶梯的位置,所以大小为n+1
class Solution {    
    public int climbStairs(int n) {        
        int[] f = new int[n + 1];        
        f[0] = 1;        
        f[1] = 1;        
        for(int i = 2; i <= n; i++){            
            f[i] = f[i - 1] + f[i - 2];        
        }        
        return f[n];    
    }
}

72. 编辑距离 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i][j]表示word1从i位置转换到word2的j位置需要的最少步数。
因为最后的时候需要将word1转换为word2

  1. 子问题:
  1. 如果word1[i] = word2[j],那么只需要关注word(i - 1)与word(j - 1)的关系,即:dp[i - 1][j - 1]
  2. 如果word1[i] != word2[j],那么此时有三个操作:记住三个操作都是针对word1的,因为要将word1转换为word2,因为有这三次操作,不管是哪一个,只要有这种操作,就要加1
    1. 插入:因为插入之后,两个字符串最后一个字符肯定相等,但是此时word1的长度是原来的长度+1,即word1(原来长度 + 1) == word2(j),此时需要看word1(i)是否等于word2(j - 1)
    2. 删除:因为删除之后,因为删除的是word1字符串的,所以删除了之后,要看word1(i-1)与word2(j)是否相等,但是此时word1的长度是原来的-1.即word1(原来长度 - 1) == word2(j),此时需要看word1(i - 1)是否等于word2(j)
    3. 替换:因为替换之后,两个字符串最后一个字符肯定相等,长度也没有发生变化,但此时word1(i) == word2(j),所以此时需要看word1(i - 1)是否等于word2(j - 1)
  1. 状态转移方程:

word1[i] == word2[j]dp[i][j] = dp[i - 1][j - 1]
word1[i] != word2[j]dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1])

  1. 边界和初始化:

边界:为了方便滚动,默认给两个字符串首尾加上一个空格,但不用实际真正加。因此dp[0][0] = 0,当只有一行和一列的时候,每次只加1。
初始化:因为加上0,所以数组的大小为(word1.length() + 1, word2.length() + 1)
在这里插入图片描述

class Solution {    
    public int minDistance(String word1, String word2) {        
        int n1 = word1.length();        
        int n2 = word2.length();        
        int[][] dp = new int[n1 + 1][n2 + 1];        
        for(int i = 1; i <= n1; i++){            
            dp[i][0] = dp[i - 1][0] + 1;        
        }        
        for(int j = 1; j <= n2; j++){            
            dp[0][j] = dp[0][j - 1] + 1;        
        }
        for(int i = 1; i <= n1; i++){            
            for(int j = 1; j <= n2; j++){            
                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[n1][n2];    
    }
}

96. 不同的二叉搜索树 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i]表示以i为根节点能生成的二叉搜索树的个数

  1. 子问题:

在[1,n]选取k作为k作为根节点,那么使用[1,k-1]构建左子树,使用[k+1,n]去构建右子树
此时总的数量就有左子树的个数 * 右子树的个数

  1. 状态转移方程:

dp[i] = ∑ \sum ∑dp[j - 1] * dp[i - j] , j∈[1,i]
左子树用了j - 1个,那么右子树只能是i-j个。
因为给定了是i,所以左+右+根= i,j - 1 + (i - j) + 1 = i

  1. 边界和初始化:

边界:n = 0/1的时候,dp[i] = 1,要么是空树,要么就是一个单节点。
初始化:因为要返回dp[n],所以初始化大小为dp[n + 1]

class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n + 1];
        dp[0] = 1;
        dp[1] = 1;
        for(int i = 2; i <= n ;i++){
            for(int j = 1; j <= i; j++){
                dp[i] += dp[j - 1] * dp[i - j];//前面是以i为根节点,i-1个左子树,后面是n-j个右子树
            }
        }
        return dp[n];
    }
}

121. 买卖股票的最佳时机 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i][j]表示到i天结束,持股的状态j,此时具有的最大利润。
dp[i][0]表示第i + 1天不持股。
dp[i][1]表示第i + 1天持股。

  1. 子问题:
  • dp[i][0]表示到第i+1天没有持股:
    1、第i天持股,到第i+1天给卖了
    2、第i天不持股,到第i+1天也不持股
  • dp[i][1]表示到第i+1天持股
    1、第i持股,到第i+1天继续持有
    2、第i天不持股,到第i+1天持股
  1. 状态转移方程:

dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])
dp[i][1] = max(-prices[i], dp[i - 1][1]); //因为可以多次买入

  1. 边界和初始化:

初始化:dp数组的大小为:dp[n][2],因为有两个持股状态
边界:dp[0][0] = 0,dp[0][1] = -prices[0]

class Solution {
    public int maxProfit(int[] prices) {
        //定义:dp[i][j]表示第i + 1天持有状态j,此时具有的最大利润
        int n = prices.length;
        int[][] dp = new int[n + 1][2];
        //dp[i][0]表示第i + 1天不持股
        //dp[i][1]表示第i + 1天持股
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for(int i = 1; i < n; i++){
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
        }
        return dp[n - 1][0];
    }
}

139. 单词拆分 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i]表示字符串si位置能否被拆分为wordDict中的字符串。

  1. 子问题:

想知道i位置能否被拆分,就需要知道前一个位置i-1能否被拆分,如果i位置可以被拆分,那么前一个位置必然可以被拆分。那么也就是说:

  1. i是从[0,n)进行扫描,j是单词尾部,从[i + 1, n + 1)
  2. dp[j]=true,那么dp[i]也必然为true
  3. 同时s的子串[i,j]组成的字符串是一定在wordDict中的。
  1. 转移方程:

dp[j] = dp[i] && wordDict.contains(s.substring(i,j))

  1. 边界和初始化:

边界:记dp[0] = true。这是为了方便后面进行滚动,默认0位置是可以被拆分到wordDict中的。
初始化:因为最后是需要返回整个字符串s的情况,即:返回dp[n],所以需要知道n位置的情况,所以数组大小为[n+1]

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        int n = s.length();
        boolean[] dp = new boolean[n + 1];
        dp[0] = true;
        for(int i = 0; i < n; i++){
            for(int j = i + 1; j < n + 1; j++){
                String subStr = s.substring(i,j);
                if(dp[i] && wordDict.contains(subStr)){
                    dp[j] = true;
                }
            }
        }
        return dp[n];
    }
}

152. 乘积最大子数组 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i][j]表示到数组i位置的最大值。为什么要设置j呢?因为通过案例发现是有负数的。所以这个j就是来判断最大值和最小值的,j=0的时候代表最小值,j=1的时候代表最大值。

  1. 子问题:

需要知道i位置就需要知道i-1位置的值。然后根据当前值的正负关系,去取前一个位置的最大值还是最小值。所以每一步都需要计算得到最大最小值
nums[i] < 0的时候,要想取得最大值,就需要乘上前一个位置的最小值和当前位置值最判断,以及最小值是当前位置的值和前一个位置的最大值*当前值。
nums[i] > 0,情况与上面相反。

  1. 转移方程:

nums[i] < 0
- dp[i][0] = Math.min(nums[i], dp[i - 1][1] * nums[i]);
- dp[i][1] = Math.max(nums[i], dp[i - 1][0] * nums[i]);

nums[i] > 0
- dp[i][0] = Math.min(nums[i], dp[i - 1][0] * nums[i]);
- dp[i][1] = Math.max(nums[i], dp[i - 1][1] * nums[i]);

  1. 边界和初始化:

边界:最后返回dp[n - 1][1],就是最后一个位置的最大值,所以大小为[n][2]
初始化:最大最小值都是第一个数。最后结果也默认是第一个数,防止给定的数组只有一个数。

class Solution {
    public int maxProduct(int[] nums) {
        int n = nums.length;
        int max = nums[0];
        int[][] dp = new int[n][2];
        dp[0][0] = nums[0];
        dp[0][1] = nums[0];
        for(int i = 1; i < n; i++){
            if(nums[i] < 0){
                dp[i][0] = Math.min(nums[i], dp[i - 1][1] * nums[i]);
                dp[i][1] = Math.max(nums[i], dp[i - 1][0] * nums[i]);
            }else {
                dp[i][0] = Math.min(nums[i], dp[i - 1][0] * nums[i]);
                dp[i][1] = Math.max(nums[i], dp[i - 1][1] * nums[i]);
            }

            max = Math.max(max,dp[i][1]);
        }
        return max;
    }
}

198. 打家劫舍 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i][j]表示到i号房间偷取到的最大价值。j同样有两个值,一个是偷,一个是不偷,那么dp[i][0]表示到i号房间不偷能取得的最大价值,dp[i][1]表示到i好房间偷能取得的最大价值。

  1. 子问题:

因为题目限制,不能连续偷两个相邻两个房间,所以:

  • 偷了i号房间就不能偷前一个房间,至于后面的房间偷不偷,那不是后面i+1号房间该考虑的事。那么此时的金额就是前面不偷的基础上加上此时偷i好房间的金额。
  • 不偷i号房间,就可以偷前面的值。那么此时金额就在前面房间的偷和不偷之间取最大值。

但是,本题要求是最大价值,所以不管怎么样金额一定要最大。也就是不管偷还是不偷i号房间,最终的结果一定的是最大的。

  1. 状态转移方程:

dp[i][0] = max(dp[i - 1][0], dp[i - 1][1])
dp[i][1] = dp[i - 1][0] + nums[i]

  1. 边界和初始化:

边界:dp[0][0] = 0dp[0][1]=nums[0]
初始化:因为最后要知道给定数组的偷取最大金额,所以,最后返回的是dp[n - 1],至于偷还是不偷,看哪个金额大。数组大小为[n][2]

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n == 1){
            return nums[0];
        }
        int[][] dp = new int[n][2];
        dp[0][0] = 0;
        dp[0][1] = nums[0];
        for(int i = 1; i < n; i++){
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
            dp[i][1] = dp[i - 1][0] + nums[i];
        }
        return Math.max(dp[n - 1][0], dp[n - 1][1]);
    }
}

221. 最大正方形 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i][j]表示以(i,j)为右下角,并且包含1的正方形的边长最大值。

为什么是右下角?
因为最后是需要判断完整个二维数组的,数组从(0,0)出发,到(row - 1, col - 1),所以是需要右下角进行判断。

  1. 子问题:

要判断是正方形,那么也就是四个方向必须全部为1, 那么只有当左上、右上、左下都为1时候,此时右下是否为1,就很关键
如果该位置上值是0,那么dp[i][j] = 0,因为当前位置不可能在由1组成的正方形中。
如果该位置上是1,那么dp[i][j]的值是由左上、右上、左下三个相邻位置的dp值决定的,当前位置的值是取决于三个相邻位置的最小值+1

  1. 转移方程:
    dp[i][j] = min(dp(i-1)(j-1),dp(i-1)(j),dp(i)(j-1)) + 1
    因为此时位置上的值是1,那么当前位置的值就要+1。也就是只包含1的正方形的边长最大值+1。
  2. 边界和初始化:
    边界:全部初始化为0,即:没有能够可以全部由1组成的正方形。
    初始化:最后需要[row] - 1[col - 1]位置的值,所以,大小为[row][col]
class Solution {
    public int maximalSquare(char[][] matrix) {
        if(matrix == null || matrix.length == 0 || matrix[0].length == 0){
            return 0;
        }
        int row = matrix.length;
        int col = matrix[0].length;
        int maxSide = 0;
        int[][] dp = new int[row + 1][col + 1];
        for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                if(matrix[i][j] == '1'){
                    if(i == 0 || j == 0){
                        dp[i][j] = 1;
                    }else {
                        dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i][j - 1]) + 1;
                    }
                    maxSide = Math.max(maxSide, dp[i][j]);
                }
            }
        }
        return maxSide * maxSide;
    }
}

279. 完全平方数 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i]表示最少使用多少个数的平方就可以组成i。

  1. 子问题:

要知道最少使用多少个数的平方可以组成i,假设最坏的情况是一个都没有,那么最差的情况就是:n个1。如果有平方数可以组成,那么就要满足扫描数的平方≤i,(一旦超过了,那么说明就凑不齐),即:dp[i - j * j]表示如果正好差数j的平方就可以组成i,那么前一个位置所需要的最少个数,再加上这个j就可以组成dp[i]所需的最少平方数。

  1. 转移方程:

min = min(i,dp[i - j * j])
dp[i] = min + 1
min表示数字i最少能够使用多少个平方数组成。其初值为i,最差的情况就是全是1组成的,但不管是由全1组成或者有平方数组成,每次计算完一个数之后,都要+1,这样让其最少的完全平方数+1。

  1. 边界和初始化:
    边界:dp[0] = 0
    初始化:因为最后需要计算n的情况,即:dp[n],所以dp数组大小为n+1
class Solution {
    public int numSquares(int n) {
        //dp[i]表示最少使用多少个平方数就可以组成i
        int[] dp = new int[n + 1];
        dp[0] = 0;
        for(int i = 1; i < n + 1; i++){
            int min = i;
            for(int j = 1; j * j <= i; j++){
                min = Math.min(min, dp[i - j * j]);
            }
            dp[i] = min + 1;
        }
        return dp[n];
    }
}

300. 最长递增子序列 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i]表示到数组i位置的递增子序列的长度。

  1. 子问题:
  1. 要想知道i位置递增子序列的长度,就需要知道前面一个位置的递增子序列的长度,如果前面也是,同时当前位置的值大于前一个位置,那么加1即可,同样是递增子序列。
  1. 转移方程:

dp[i] = max(dp[j] + 1, dp[i]);j表示前一个位置,i表示当前位置。
j∈[0,i)、i∈[1,n-1]

  1. 边界和初始化:

边界:给出了数组大小之后,先默认没有递增的,即:每个位置上的递增子序列的长度都是1,就是自己。
初始化:因为需要算得最后的位置,即:下标n-1,所以数组大小为n

class Solution {
    public int lengthOfLIS(int[] nums) {
        //定义:dp[i]表示到i位置递增子序列的长度
        int n = nums.length;
        int max = 1;
        int[] dp = new int[n];
        for(int i = 0; i < n; i++){
            dp[i] = 1;
        }
        for(int i = 1; i < n; i++){
            for(int j = 0; j < i; j++){
                if(nums[i] > nums[j]){
                    dp[i] = Math.max(dp[j] + 1, dp[i]);
                }
                max = Math.max(max, dp[i]);
            }
        }
        return max;
    }
}

309. 最佳买卖股票时机含冷冻期 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i][j]表示到第i天持有股票状态j能收获的最大收益
根据题意可知,可以多次的买卖股票,卖出股票之后有冷冻期,过了冷冻期才可以买入。所以:
j有三种状态:0、持股;1、不持股但处于冷冻期;2、不持股且没有处于冷冻期。
:冷冻期为一天。最后返回的时候一定是不持股的状态能取得的最大值

  1. 子问题:

dp[i][0]:表示第i天持有股票能取得的最大收益,此时要么就是前一天继续持股,要么就是前一天不持股且没有处于冷冻期今天买入。(如果前一天不持股且处于冷冻期,今天必然是无法买入的)
dp[i][1]:表示第i天不持有股票且处于冷冻期的最大收益。此时因为处于冷冻期,那么必然前一天是卖掉股票的且前一天不持股的,因为卖掉了所以不持股。
dp[i][1]:表示处于第i天且没有处于冷冻期的最大收益。此时没有处于冷冻期要么就是前一天处于冷冻期的,今天解冻了,要么就是前一天也没有持股并且处于冷冻期的。

  1. 转移方程:

经过分析可知:
dp[i][0] = max(dp[i - 1][0], dp[i - 1][2] - prices[i])

dp[i][1] = dp[i - 1][0] + prices[i]

dp[i][2] = max(dp[i - 1][1], dp[i - 1][2])

  1. 边界和初始化:

边界:
dp[0][0] = -price[0]
dp[0][1] = 0
dp[0][2] = 0
初始化:因为要计算到最后一天,并且由三种状态,所以初始化的时候是dp[n][3]

class Solution {
    public int maxProfit(int[] prices) {
        //0表示持股,1表示不持股处于冷冻期,2表示不持股也不处于冷冻期
        int n = prices.length;
        int[][] dp = new int[n][3];
        if(n == 0){
            return 0;
        }
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        dp[0][2] = 0;
        for(int i = 1; i < n; i ++){
            //要么前一天持股,要么前一天不持股也没有处于冷冻期今天买入
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2] - prices[i]);
            //前一天持股今天卖掉
            dp[i][1] = dp[i - 1][0] + prices[i];
            //前一天不持股也不处于冷冻期,要么前一天持股处于冷冻
            dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1]);
        }
        return Math.max(dp[n - 1][1], dp[n - 1][2]);
    }
}

312. 戳气球 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i][j]表示数组[i,j]的积分值,i控制左边界,j控制右边界。

  1. 子问题:

此时的子问题是在[i,j]这个区间内,选择不同的气球扎破能得到的最大积分值,那么就将区间划分为了[i,k),k,(k,j]
如果最后只有三个气球了,那么此时只能扎破中间的,那么此时得到的金币数量就是i * k * j,还要加上之前在区间[i,k)以及(k,j]位置扎破的金币
注:为什么要加dp[i][k]和dp[k][j]?
因为k永远都是最后一个被扎破的气球,(i,j)区间中的k两边的东西必然是先各自被戳破。两边是互不干扰
重点:
k永远都是最后一个被戳破的,并且始终都是找金币数最大的那个。

  1. 转移方程:

sum = i指向的气球拥有金币数 * j指向的气球拥有金币数 * k指向的气球拥有金币数
sum += dp[i][k] + dp[k][j]
dp[i][j] = max(dp[i][j], sum)

  1. 边界和初始化:

初始化:根据题意是可以在给定数组的两端加上数字1,这样大小就是n+2,此时dp数组的大小为:[n+2][n+2],但是最后实际上是返回的dp[0][n+2]。返回0~(n + 1)能扎破出的最大金币数
边界:dp数组都初始化为0

class Solution {
    public int maxCoins(int[] nums) {
        int n = nums.length;
        int[][] dp = new int[n + 2][n + 2];
        int[] temp = new int[n + 2];
        temp[0] = 1;
        temp[n + 1] = 1;
        for(int i = 1; i <= n; i++){
            temp[i] = nums[i - 1];
        }
        //倒着处理, 因为是(i,j)区间,让j先指向末尾,i比j小两个位置,k永远在这个区间进行移动,选择最大值
        for(int i = n - 1; i >= 0; i--){
            for(int j = i + 2; j <= n + 1; j++){
                for(int k = i + 1; k < j; k++){
                    int sum = temp[i] * temp[j] * temp[k];
                    sum += dp[i][k] + dp[k][j];
                    dp[i][j] = Math.max(sum, dp[i][j]);
                }
            }
        }
        return dp[0][n + 1];
    }
}

322. 零钱兑换 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i]表示使用最少的硬币就可以总金额i。

  1. 子问题:

要想知道dp[i],首先看它的前一个问题:就是最少可以用多少硬币组成dp[i - coins[j]],j∈[0,n)。在不知道选择的面值大小的时候,假设选择使用面值为ak,那么ak∈[coins[i]],那么要拼出金额i,子问题就是用最少的硬币拼出i - ak,那么最后一个问题的时候,可以选择或者不选面值为ak的。选择ak之后,就可以正好组成i硬币数+1.不选择ak,硬币数就是正无穷,就永远无法凑齐。

  1. 转移方程:

dp[i] = min(dp[i], dp[i - coins[j]] + 1),j∈[0,n)

  1. 边界和初始化

边界:dp[0] = 0;
初始化:因为最后要返回使用多少硬币组成amount,所以需要dp[amount],那么大小就为dp[amount+1]。

class Solution {
    public int coinChange(int[] coins, int amount) {
        int n = coins.length;
        //定义:dp[i]表示最少使用多少硬币可以组成i
        int[] dp = new int[amount + 1];
        dp[0] = 0;
        for(int i = 1; i < amount + 1; i++){
            //先默认是没有能凑齐
            dp[i] = Integer.MAX_VALUE;
            //j代表硬币种类
            for(int j = 0; j < n; j++){
                //此时虽然有转移方程,但是还要满足下标大于等于0,并且前面一个位置是可以被凑齐的
                if(i - coins[j] >= 0 && dp[i - coins[j]] != Integer.MAX_VALUE){
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        if(dp[amount] != Integer.MAX_VALUE){
            return dp[amount];
        }else {
        //等于默认值就是无法被凑齐,返回-1即可。
            return -1;
        }
        
    }
}

337. 打家劫舍 III - 力扣(LeetCode) (leetcode-cn.com)

如果偷了根节点,那么左右子树就不能偷;如果偷了根节点,那么就可以选择左右子树。

  1. 状态定义:

dp[i]表示选择i节点偷与不偷的情况下能取得的最大值。
dp[0]表示不选择根节点
dp[1]表示选择根节点

  1. 子问题:

因为相邻节点不能一起偷,就是说不能偷根的时候继续偷左右子树结点。
当前结点偷了左右两个孩子节点就不能再偷了
金额 = 当前结点的钱 + 左孩子选择自己不偷的时候可以得到的最大的钱 + 右孩子选择不偷可以得到最大的钱数
当前根节点不偷两个孩子节点需要拿出最多的钱即可
金额= 左孩子能够偷到的钱 + 右孩子能偷到的钱

  1. 转移方程:

dp[1]= root.val + left[0] + right[0] 只能选择根节点的,左右节点都不能选的情况下的最大节点和
dp[0] = max(left[0], left[1]) + max(right[0], right[1]) 不选择根节点的情况下,偷左右节点可以偷的最大窃取

  1. 边界和初始化:

边界:如果dfs到了叶子结点,那么返回{0,0},向上级报告,从这里再向下能偷取到的是{0,0}。
初始化:dp只有两个状态,一个是偷当前结点,一个是不偷当前结点,所以dp[2]

class Solution {
    public int rob(TreeNode root) {
        int[] res = dfs(root);
        return Math.max(res[0], res[1]);
    }
    private int[] dfs(TreeNode root){
        if(root == null){
            return new int[]{0,0};
        }
        int[] left = dfs(root.left);
        int[] right = dfs(root.right);
        int[] dp = new int[2];
        dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        dp[1] = root.val + left[0] + right[0];
        return dp;
    }
}

338. 比特位计数 - 力扣(LeetCode) (leetcode-cn.com)

  1. 状态定义:

dp[i]表示数字i的二进制数中1的个数。

  1. 子问题:

要想得知i的二进制数中1的个数,那么就需要知道它前一个数的二进制数中的1的个数,因为后一个数比大前一个数大1这个1,就是多出来的1的个数
奇数的时候,二进制最后一位肯定是1;为偶数二进制数最后一位肯定是0
为奇数的时候,因为二进制最后一位是1,就比前一个数多了一个1.
偶数的时候,1的个数和自己除2之后的二进制数中的1的个数一样(eg: 2(01) 4(10) 8(100))

  1. 转移方程:

dp[i] = dp[i - 1] + 1 i为奇数
= dp[i / 2] i为偶数

  1. 边界和初始化:

边界:dp[0] = 0
初始化:因为返回的n数字的二进制1的情况,那么一定要计算到dp[n],初始化大小为n+1

class Solution {
    public int[] countBits(int n) {
        int[] dp = new int[n + 1];
        dp[0] = 0;
        for(int i = 1; i < n + 1; i++){
            if(i % 2 == 1){
                dp[i] = dp[i - 1] + 1;
            }else {
                dp[i] = dp[i / 2];
            }
        }
        return dp;
    }
}

416. 分割等和子集 - 力扣(LeetCode) (leetcode-cn.com)

leetcode官方视频题解
本题就是一个0-1背包问题,有兴趣的朋友可以了解一下

  1. 状态定义:

dp[i][j]表示数组从[0,i]是否能有一种方案,使得选取正整数之和为j

  1. 子问题:

要想知道[0,i]是否能有一种方案使得选取的正整数之和为j,那么就需要看dp[i-1][x],那么就是否选择nums[i],以及选择之后,剩余数的大小与nums[i]的大小关系。

  1. 如果不选择nums[i],那么dp[i][j] = dp[i - 1][j]
  2. 如果选择,那么又需要看nums[i]j的大小关系
    1. 如果正好相等,那么dp[i][j]=true
    2. 如果当前数小于j,那么dp[i][j]=dp[i-1][j-nums[i]]
    3. 如果大于j的话,就肯定不能选。
  1. 状态转移方程:

dp[i][j] = dp[i - 1][j] j < num[i]
= dp[i - 1][j] || dp[i - 1][j - nums[i]] j ≥ nums[i]
不选取nums[i] || 选取nums[i]

  1. 边界和初始化:

边界:
1.如果不选择任何正整数,那么表示被选取的正整数等于0.即:dp[i][0]=true;
2. 如果i为0,且只有一个正整数nums[0]可以被选取,那么dp[0][nums[0]]=true
初始化:因为最后需要判断最后一个数[n - 1],那么设置dp[n][target],这个target就是要求的数之和。target一定是数组之和的一半。

class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;
        if(n < 2){
            return false;
        }
        int maxNum = 0, sum = 0;
        for(int num : nums){
            sum += num;
            maxNum = Math.max(maxNum, num);
        }
        if(sum % 2 != 0){
            return false;
        }
        int target = sum / 2;
        if(maxNum > target){
            return false;
        }

        //dp[i][j]表示从0-i是否能凑齐j
        boolean[][] dp = new boolean[n][target + 1];
        //都不选
        for(int i = 0; i < n; i++){
            dp[i][0] = true;
        }
        for(int i = 1; i < n; i++){
            int num = nums[i];
            for(int j = 1; j < target + 1; j++){
                if(j >= num){
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num];
                }else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[n - 1][target];
    }
}

494. 目标和 - 力扣(LeetCode) (leetcode-cn.com)

推荐题解

  1. 状态定义:

dp[i][j]表示从数组[0,i]的元素进行加减可以在[-sum,sum]得到值j的方法数。
为什么这样定义可以看一下下面的初始化部分。

  1. 子问题:

要想知道dp[i][j]的情况,就要对dp[i - 1][x]进行讨论,那么关键就是nums[i]选取过后的加减问题。
选取nums[i],那么此时就需要讨论x是加上当前数凑齐j,还是x减去当前数凑齐j,分别为:[i - 1][j - nums[i]]、[i - 1][j + nums[i]]

  1. 转移方程:

dp[i][j]= dp[i - 1][j + nums[i]] + dp[i - 1][j - nums[i]](分别表示减或者加上当前数就能凑齐j)

  1. 边界和初始化:

初始化:因为最后需要得到dp[n -1][target],所以在初始化的时候是dp[n][target]。但经过打表之后就能分析出问题,因为本题是可以通过加、减得到的, 所以,应该每一行的长度为2 * sum + 1。(1就是中间0的位置)。所以真正的dp大小为[len - 1][2 * sum + 1]。最后的返回是dp[n - 1][sum + target]
边界定值:确定好了数组的大小之后,进行打表,如果给定数组第一个数为0,那么dp[0][sum] = 2(加减);否则dp[0][sum ± nums[0]] = 1(要么加,要么减)。(可以看打表过程,也可以自己画,以[1,1][0,1]为例)

在下表中,因为数组下标不能为负,所以将负数和0进行了右移,所以下图dp[0][sum]实际上是用黄色标出来的2。
题解的截图

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int n = nums.length;
        int sum = 0;
        for(int num : nums){
            sum += num;
        }
        //如果给定目标值都比和还大,肯定是凑不齐的,返回0
        if(Math.abs(target) > Math.abs(sum)){
            return 0;
        }
        //j的范围是[-sum, sum]之间,中间还有一个0。
        int[][] dp = new int[n][2 * sum + 1];
        //第一个值为0,那么得到最后和一定会有两个方案(加或者减)
        //数组[0,1]可以证明,最后和为1,那么第一个位置可正可负
        if(nums[0] == 0){
            dp[0][sum] = 2;
        }else {
        //否则第一个数只能分别组成(+、-)nums[i]的情况
        //[1,1]为例.
        //dp[0][3]表示在数组在[0,0]可以在范围[-2,2]得到值为3-2的方案数为1
        //dp[0][1]表示数组在[0,0]可以在范围[-2,2]得到值为1-2的方案数为1.
            dp[0][sum + nums[0]] = 1;
            dp[0][sum - nums[0]] = 1;
        }
        for(int i = 1; i < n; i++){
            for(int j = 0; j < 2 * sum + 1; j++){
            	//负数部分要确保下标大于等于0
                int left = (j - nums[i]) >= 0 ? j - nums[i] : 0;
                //正数部分要确保不能超过上限
                int right = (j + nums[i]) < 2 * sum + 1 ? j + nums[i] : 0;
                dp[i][j] = dp[i - 1][left] + dp[i - 1][right];
            }
        }
        // System.out.println(Arrays.deepToString(dp));
        return dp[n - 1][sum + target];
    }
}
class Solution {
    private int count = 0;
    public int findTargetSumWays(int[] nums, int target) {
        dfs(nums, target, 0, 0);
        return count;
    }
    private void dfs(int[] nums, int target, int idx, int sum){
        if(idx == nums.length){
            if(target == sum){
                count++;
            }
        }else {
            dfs(nums, target, idx + 1, sum - nums[idx]);
            dfs(nums, target, idx + 1, sum + nums[idx]);
        }
    }
}

标签:cn,nums,int,hot100,动态,com,leetcode,dp
来源: https://blog.csdn.net/qq_41784433/article/details/120918254