【Java】HOT100+代码随想录 动态规划(上)背包问题

目录

理论基础

 一、基础题目

LeetCode509:斐波那契数

LeetCode70:爬楼梯

LeetCode746:使用最小花费爬楼梯

LeetCode62:不同路径

 LeetCode63:不同路径ii

 LeetCode343:整数拆分

 LeetCode96:不同的二叉搜索树

二、背包问题

2.1 01背包

01背包理论基础1:二维dp数组

01背包理论基础2:滚动/一维dp数组

LeetCode416:分割等和子集(求是否能正好装满)

LeetCode1049:最后一块石头的重量ii(最多装多少)

 LeetCode493:目标和(求装满背包有多少种方法)

LeetCode474:一和零(求装满背包最多有多少个物品)

2.2 完全背包

理论基础

LeetCode518:零钱兑换ii(求组合数)

 LeetCode377:组合总和iv(求排列)

 LeetCode70:爬楼梯(求排列数)

LeetCode322:零钱兑换(求最小数)

LeetCode279:完全平方数(求最小数)

LeetCode139:单词拆分

2.3 多重背包

理论基础


理论基础

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

所以动态规划中每一个状态一定是由上一个状态推导出来的;

举例:

有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。

这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的

  • 状态转移公式(递推公式)在动态规划中相当重要
  • 动态规划五部曲如下:
  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化(很多时候递推公式决定了dp数组要如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

在debug时把dp数组打印出来,看看究竟是不是按照自己思路推导的


 一、基础题目

LeetCode509:斐波那契数

思路:每一个数字等于前两项之和,给定n求F(n)。动规五部曲——

①dp数组和下标的含义:dp[i]即第i个数的斐波那契数

②递归公式:题干中就给出了,dp[i] = dp[i - 1] + dp[i - 2];

③初始化:题干中也给出了,dp[0] = 0,dp[1] = 1;

④遍历顺序:从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历

⑤举例推导:按照递推公式,n为10时,dp数组应该是如下的:

0 1 1 2 3 5 8 13 21 34 55(如果结果不对,就打印dp数组)

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

LeetCode70:爬楼梯

思路:每次你可以爬 1 或 2 个台阶

①dp数组和下标的含义:爬到第i阶,有dp[i]种方法

②递归公式:爬到i-1阶有dp[i-1]种方法,然后再爬一阶就是dp[i];同理,爬到i-2阶有dp[i-2]种方法,然后再爬两阶就是dp[i]。所以dp[i] = dp[i - 1] + dp[i - 2];

③初始化:可以推测出,dp[0] = 0(不重要),dp[1] = 1,dp[2] = 2;

④遍历顺序:从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出遍历的顺序一定是从前到后遍历

⑤举例推导:按照递推公式,n为5时,dp数组应该是如下的:

0 1 2 3 5 8

发现不就是斐波那契数吗?!同样的代码一点毛病没有

public int climbStairs(int n) {
    int[] dp = new int[n + 1];
    dp[0] = 1;
    dp[1] = 1;
    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

也可以使用变量优化一下空间复杂度

class Solution {
    public int climbStairs(int n) {
        if(n <= 2) return n;
        int a = 1, b = 2, sum = 0;

        for(int i = 3; i <= n; i++){
            sum = a + b;  // f(i - 1) + f(i - 2)
            a = b;        // 记录f(i - 1),即下一轮的f(i - 2)
            b = sum;      // 记录f(i),即下一轮的f(i - 1)
        }
        return b;
    }
}

LeetCode746:使用最小花费爬楼梯

思路:可以选择从下标0或者1开始爬,爬1-2阶

①dp数组和下标的含义:爬到第i阶,所花费的最少体力是dp[i]

②递归公式:要算得dp[i]有两种方法,一个是从dp[i-1]花cost[i-1]向上爬一阶,一个是行dp[i-2]花cost[i-2]向上爬两阶,最终使用哪种是最少话费呢?

那就是dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);

③初始化:可以推测出,dp[0] = 0,dp[1] = 0;

④遍历顺序:从递归公式中可以看出遍历的顺序一定是从前到后遍历

⑤举例推导

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int len = cost.length;
        int[] dp = new int[len + 1];

        // 从下标为 0 或下标为 1 的台阶开始,因此支付费用为0
        dp[0] = 0;
        dp[1] = 0;

        // 计算到达每一层台阶的最小费用
        for (int i = 2; i <= len; i++) {
            dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }

        return dp[len];
    }
}

LeetCode62:不同路径

思路:如果采用深度搜索,则这棵树的深度为m+n-1,而深搜代码的时间复杂度为O(2^(m + n - 1) - 1),这是非常大的。那么采用动态规划,从(0,0)出发,到(m,n):

①dp数组和下标的含义:dp[i][j]表示从(0,0)出发,到达(i,j)有多少种路径;

②递归公式:要算得dp[i][j]有两种方法,一个是从dp[i-1][j] 向下移动一格,一个是从dp[i][j-1]向右移动一格。dp[i][j] = dp[i - 1][j] + dp[i][j - 1];

③初始化:首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理

for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;

④遍历顺序:从递归公式中可以看出遍历的顺序是从左到右一层一层遍历的

⑤举例推导

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        for(int i=0;i<m;i++){
            dp[i][0] = 1;
        }
        for(int i=0;i<n;i++){
            dp[0][i] = 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];
    }
}

 LeetCode63:不同路径ii

思路:相比1,给出了障碍物,路径需要绕开障碍物。与1的区别仅仅在于:

1. 初始化时如果障碍在边上,则仅初始化障碍前的dp值为1,障碍后仍为0(无法到达)

2. 在遍历时跳过给有障碍的dp赋值

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];
        for(int i=0;i<m && obstacleGrid[i][0] == 0;i++){
            dp[i][0] = 1;
        }
        for(int i=0;i<n && obstacleGrid[0][i] == 0;i++){
            dp[0][i] = 1;
        }
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                if(obstacleGrid[i][j] == 1)   continue;
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}

 LeetCode343:整数拆分

思路:给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 

①dp数组和下标的含义:分拆数字i所得到的最大乘积是dp[i]

②递归公式:要算得dp[i]有两种方法,从1遍历j,一个是j*(i-j),一个是j*dp[i-j]。可以这么理解,j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘。

那就是dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});

在取最大值的时候,为什么还要比较dp[i]呢?因为在递推公式推导的过程中,每次计算dp[i],取最大的而已。

③初始化:可以推测出,dp[2] = 1,dp[3] = 2;(n从3开始拆)

④遍历顺序:从递归公式中可以看出遍历的顺序一定是从前到后遍历的,且先有dp[i - j]再有dp[i]。i从3-n,j从1到i-1(不包括i-1),更优化的遍历方法是:j直接从1到i/2,这是因为一定是拆分成m个近似相同的子数相乘才是最大的

⑤举例推导

class Solution {
    public int integerBreak(int n) {
        int[] dp = new int[n+1];
        dp[2] = 1;
        for(int i = 3;i<=n;i++){
            for(int j = 1;j<=i/2;j++){
                dp[i] = Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));
            }
        }
        return dp[n];
    }
}

 LeetCode96:不同的二叉搜索树

思路:重点是推导公式,容易看出

dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量,

元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量

元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量

元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量

dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]

① dp的概念:dp[i]就是以1到i为节点构成的二叉搜索树的个数

②递归公式:i从2开始遍历到n,j从1开始遍历到i,dp[i] += dp[j-1]*dp[i-j];

③初始化:dp[0]=1; dp[1] = 1;

④遍历顺序和举例推导

class Solution {
    public int numTrees(int n) {
        //初始化 dp 数组
        int[] dp = new int[n + 1];
        //初始化0个节点和1个节点的情况
        dp[0] = 1;
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                //对于第i个节点,需要考虑1作为根节点直到i作为根节点的情况,所以需要累加
                //一共i个节点,对于根节点j时,左子树的节点个数为j-1,右子树的节点个数为i-j
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
}

二、背包问题

2.1 01背包

01背包理论基础1:二维dp数组

(物品下标-背包容量)

背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。

一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

二维数组 01背包:

  1. dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少
  2. 递归公式:不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同)。②放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值。
  3.  dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 
  4. 初始化:关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。根据递推公式可以看出需要得知dp[i][0]和dp[0][j]的初始值,才能开始递推。可以一开始就统一把dp数组统一初始为0。当背包容量为0时,无论怎么选取物品,都无法放入背包中,即价值为0,dp[i][0] = 0;当放入序号为0的物品时且背包容量为0-j时,所能放下物品的最大价值:当j<weight[0]时,背包里放不下物品0,dp[0][j]应该是0;当j>=weight[0]时,背包里可以放下编号0物品,dp[0][j]应该是value[0]。
  5. 遍历顺序:问题来了,先遍历物品还是先遍历背包重量呢?其实都可以!! 但是先遍历物品更好理解
  6. 举例推导:自己推导一下

例题:卡码网46 携带研究材料(给定背包容量,求装满的最大价值)

public class Main {
    public static void main(String[] args) {
        int[] weight = {2,2,3,1,5,2};
        int[] value = {2,3,1,5,4,3};
        int bagSize = 1;
        testWeightBagProblem(weight,value,bagSize);
    }
    
    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){

        // 创建dp数组
        int goods = weight.length;  // 获取物品的数量
        int[][] dp = new int[goods][bagSize + 1];

        // 初始化dp数组
        // 创建数组后,其中默认的值就是0
        for (int j = weight[0]; j <= bagSize; j++) {
            dp[0][j] = value[0];
        }

        // 填充dp数组
        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j <= bagSize; j++) {
                if (j < weight[i]) {
                    /**
                     * 当前背包的容量都没有当前物品i大的时候,是不放物品i的
                     * 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
                     */
                    dp[i][j] = dp[i-1][j];
                } else {
                    /**
                     * 当前背包的容量可以放下物品i
                     * 那么此时分两种情况:
                     *    1、不放物品i
                     *    2、放物品i
                     * 比较这两种情况下,哪种背包中物品的最大价值最大
                     */
                    dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
                }
            }
        }
        System.out.println(dp[goods-1][bagSize]);
    }
}

01背包理论基础2:滚动/一维dp数组

对于背包问题其实状态都是可以压缩的。

在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:

dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

即重复利用上一层,与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。

  1. 在一维数组中,dp[j]代表:容量为j的背包,所背的物品价值可以最大为dp[j];
  2. 递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);仍然相当于其中一层的不放物品i和放物品i
  3. 初始化:看一下递推公式,可知只需初始化dp[0]]=0即可;
  4. 遍历顺序:重点是倒序遍历,是为了确保物品i只被放入一次。二维的就不用倒序,因为二维dp[i][j]计算是通过上一层dp[i - 1][j]得来的,不会覆盖,而由于一维数组用的是同一个dp[j],所以会从前向后覆盖,影响dp值的计算,所以为了避免重复需要倒序。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

public class Main {
    public static void main(String[] args) {
        int[] weight = {2,2,3,1,5,2};
        int[] value = {2,3,1,5,4,3};
        int bagSize = 1;
        testWeightBagProblem(weight,value,bagSize);
    }
    
    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){

        // 创建dp数组
        int goods = weight.length;  // 获取物品的数量
        int[] dp = new int[bagSize + 1];

        // 初始化dp数组,直接默认全为0
        // 填充dp数组,放不下的即j < weight[i]就默认不改变,一维数组直接继承上一层无需赋值,简化了代码
        for (int i = 0; i < goods; i++) {
            for (int j = bagSize; j >= weight[i]; j--) {
                    dp[j] = Math.max(dp[j] , dp[j-weight[i]] + value[i]);
            }
        }
        System.out.println(dp[bagSize]);
    }
}

总结: 

1. 为什么二维背包两个for循环的嵌套顺序这么写?反过来写行不行?再讲一讲初始化的逻辑。

答:两个for循环的顺序无非是先遍历物品还是先遍历背包容量,在二维数组中其实都行得通,但是遍历物品更好理解。根据递推公式,每一个dp[i][j]其实都是由上一个dp[i-1][j]或者上一个dp[i - 1][j - weight[i]] 推导出来的,如果以表格的形式来看,位于dp[i][j]的左上角,如果先遍历物品,就是在每一个物品都遍历一遍不同的背包重量,从左到右,从上到下;而如果先遍历背包容量就是先从上到下,在从左到右,由于数据位于左上角,所以两个for的顺序都不影响dp[i][j]的推导。而这种推导方式显然要得到所有的dp[0][j]和dp[i][0]公式才能接着往下,往右推导。

2. 改成一维以后,两个for循环的顺序反过来写行不行?为什么?

答:不可以,改成一维之后只能先遍历物品再遍历背包容量,这是由于一维dp的写法中背包容量一定是要倒序遍历,其本质上还是对二维数组的遍历,右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖,保证每个物品只添加一次。如果非要换顺序,倒序遍历的作用便失去了。


LeetCode416:分割等和子集(求是否能正好装满)

思路:是否可以将数组分割成两个子集,使得子集和相等。

怎么转换成背包问题呢?

首先,每个元素我们只用一次,所以是01背包问题。背包的最大容量是target = sum/2,放入n个元素,每个元素的值既是他的value,也是他的weight;dp[j]的定义是容量为j的背包里能装的最大重量,如果正好装满,即dp[j] = j

其次,01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);对于本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);01背包的初始化:dp[0] = 0即可。

首先得到背包,最终做判断如果dp[target] = target,就说明可以分割等和子集

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int i= 0;i<nums.length;i++){
            sum += nums[i];
        }
        if(sum % 2 == 1)    return false;
        int target = sum/2;
        int[] dp = new int[target+1];
        for(int i = 0;i<nums.length;i++){
            for(int j = target;j>=nums[i];j--){
                dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
            }
        }
        if(dp[target] == target){
            return true;
        }
        return false;
    }
}

LeetCode1049:最后一块石头的重量ii(最多装多少)

思路:其实本质上就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小这样就化解成01背包问题了,同样是每个物品重量和价值都为stones[i]。target = sum/2。

得到背包后,不用完全相等,一堆石头是dp[target],另一堆是sum - dp[target]。由于target = sum/2是向下取整,所以sum - dp[target] > dp[target],

所以最终的最小石头重量为 (sum - dp[target]) - dp[target]。

class Solution {
    public int lastStoneWeightII(int[] nums) {
        int sum = 0;
        for(int i= 0;i<nums.length;i++){
            sum += nums[i];
        }
        int target = sum/2;
        int[] dp = new int[target+1];
        for(int i = 0;i<nums.length;i++){
            for(int j = target;j>=nums[i];j--){
                dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
            }
        }
        return sum - 2*dp[target];
    }
}

 LeetCode493:目标和(求装满背包有多少种方法)

思路:如何转化成01背包问题——

一部分left为+,一部分right为-,left-right = target,left+rght = sum,已知sum和target,就可以求出 left = (target + sum)/2 ,此时问题就变成了在集合中找bagSize = left的组合。

之前的几道题都是求容量为j的背包,最多能装多少,本题则是说装满有几种方法,是组合问题

* 首先排除两种情况,即①target的绝对值>sum,以及②(target+sum)除以2的余数不为0

1. dp[j]:容量为j的背包能有dp[j]种方法,

2. 递推公式:求方法的总和——dp[j] += dp[j - nums[i]]

3. 初始化:dp[0] = 1,别的初始化为0;背包容量为0的话,[0]可以有一种取法,[0,0,0,0,0]有32种取法,也是dp[0] = 1叠加起来的

4. 遍历顺序就是01背包的先遍历物品,再倒序遍历背包容量

5. 举例推导:

这道题也可以用回溯来做,甚至可以列出所有组合,dp仅仅可以求个数

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum =0;
        for(int i=0;i<nums.length;i++){
            sum += nums[i];
        }
        if(Math.abs(target) > sum)  return 0;
        if((target+sum)%2 == 1) return 0;
        int bagSize = (target+sum)/2;
        int[] dp = new int[bagSize+1];
        dp[0] = 1;
        for(int i=0;i<nums.length;i++){
            for(int j=bagSize;j>=nums[i];j--){
                dp[j] += dp[j-nums[i]];
            }
        }
        return dp[bagSize];
    }
}

LeetCode474:一和零(求装满背包最多有多少个物品)

思路:strs数组里的元素就相当于是物品,包含zeroNum个0,oneNum个1,这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。

dp[i][j]就相当于最多有i个0和j个1的最大子集大小,递推通过加一来实现即可。字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。

相当于还是先遍历物品,在内循环里通过构建zeroNum和oneNum两个双循环来遍历背包容量。

01背包递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

该二维weight递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

其实就是把dp变成二维的,本质还是一样的

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp = new int[m+1][n+1];
        for(String str: strs){    //遍历物品
            int zeroNum = 0;
            int oneNum = 0;
            //得到每个物品的weight,分为0和1两个维度
            for(char ch: str.toCharArray()){
                if(ch == '0'){
                    zeroNum++;
                }else{
                    oneNum++;
                }
            }
            for(int i = m; i>=zeroNum; i--){
                for(int j = n; j>=oneNum; j--){
                    dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
}

2.2 完全背包

理论基础

区别于01背包:每件物品都可以放入背包无限次,可以重复放入

首先再回顾一下01背包的核心代码

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。

而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:

// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}

 且在纯完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!

例题:携带研究材料(完全背包)

public class Main {
    public static void main(String[] args) {
        int[] weight = {1,2,3,4};
        int[] value = {2,4,4,5};
        int bagSize = 5;
        // 创建dp数组
        int goods = weight.length;  // 获取物品的数量
        int[] dp = new int[bagSize + 1];

        // 初始化dp数组,直接默认全为0
        // 填充dp数组,放不下的即j < weight[i]就默认不改变,一维数组直接继承上一层无需赋值,简化了代码
        for (int i = 0; i < goods; i++) {
            for (int j = weight[i]; j <= bagSize; j++) {
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
        System.out.println(dp[bagSize]);
    }
}

LeetCode518:零钱兑换ii(求组合数)

不同面额的硬币(可以重复使用)和一个总金额,求组合数

一个关键的知识点:

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

class Solution {
    public int change(int amount, int[] coins) {
        int[] dp = new int[amount+1];
        dp[0] = 1;
        for(int i=0;i<coins.length;i++){
            for(int j=coins[i];j<=amount;j++){
                dp[j] += dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
}

 LeetCode377:组合总和iv(求排列)

思路:同上,只是调一个for的顺序,顺序不同的序列被视作不同的组合所以是求排列

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target+1];
        dp[0] = 1;
        for(int i=0;i<= target;i++){
            for(int j=0;j<nums.length;j++){
                if(i >= nums[j])    dp[i] += dp[i-nums[j]];
            }
        }
        return dp[target];
    }
}

 LeetCode70:爬楼梯(求排列数)

思路:这其实就是完全背包的求排列数,秒啦

import java.util.Scanner;
public class Main{
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        while(sc.hasNextInt()){
            int n = sc.nextInt();
            int m = sc.nextInt();
            int[] dp = new int[n+1];
            dp[0] = 1;
            for(int i = 1;i<=n;i++){
                for(int j = 1;j<=m;j++){
                    if(i>=j)    dp[i] += dp[i-j];
                }
            }
            System.out.println(dp[n]);
        }
    }
}

LeetCode322:零钱兑换(求最小数)

凑成目标的最小硬币数量

思路:相比于LeetCode474:一和零是01背包求最大个数(二维weight)

这道题只不过改成了是完全背包求最小个数(weight就是数值)

这个初始化dp[0]=0,且由于求的是最小值,所以需要把所有dp一开始初始化为max才能进行比较

不多说了上代码!

class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = Integer.MAX_VALUE;
        int[] dp = new int[amount+1];
        //初始化dp数组为最大值
        for (int j = 0; j < dp.length; j++) {
            dp[j] = max;
        }
        dp[0] = 0;
        for(int i=0;i<coins.length;i++){
            for(int j=coins[i];j<=amount;j++){
                //只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
                if(dp[j-coins[i]]!= max) dp[j] = Math.min(dp[j],dp[j-coins[i]]+1);
            }
        }
        return dp[amount]==max?-1:dp[amount];
    }
}

LeetCode279:完全平方数(求最小数)

思路:其实就是元素1-10的平方,给定target,上道题稍微改动一下就能过

class Solution {
    public int numSquares(int n) {
        int max = Integer.MAX_VALUE;
        int[] dp = new int[n+1];
        //初始化dp数组为最大值
        for (int j = 0; j < dp.length; j++) {
            dp[j] = max;
        }
        dp[0] = 0;
        for(int i=1;i<=10;i++){
            for(int j=i*i;j<=n;j++){
                //这个if是为了防止凑不成,而因为有1所以一定可以凑成,所以if可以略去
                if(dp[j-i*i]!= max) dp[j] = Math.min(dp[j],dp[j-i*i]+1);
            }
        }
        return dp[n]==max?-1:dp[n];
    }
}

LeetCode139:单词拆分

判断单词是否可以被空格拆分为字典里的词

思路:关于单词拆分,回溯法也能做(这个再说);如何转换成背包呢?单词即物品,字符串即背包,这道题就转换成了物品是否能正好把背包装满。

1. dp[i]代表i前面的子串是否能由字典构成,true/false

2. 递推:如果dp[j]==true && [j,i]子串也属于字典则dp[i]==true(i<j)

3. 初始化:dp[0] = true

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        boolean[] dp = new boolean[s.length()+1];
        HashSet<String> set = new HashSet<>(wordDict);
        dp[0] = true;
        for(int i=1;i<=s.length();i++){
            for(int j=0;j<i;j++){
                if(dp[j] == true && set.contains(s.substring(j,i))){
                    //本身子字符串就不包含i,所以i就是下一个单词开始的第一个字符
                    dp[i] = true;
                }
            }
        }
        return dp[s.length()];
    }
}

2.3 多重背包

理论基础

相当于n种物品和一个容量为v的背包,每种物品只有mi件,耗费空间ci,价值是wi

多重背包摊开可以变成01背包

例题:卡码网56 携带矿石资源

import java.util.Scanner;
class multi_pack{
    public static void main(String [] args) {
        Scanner sc = new Scanner(System.in);

        /**
         * bagWeight:背包容量
         * n:物品种类
         */
        int bagWeight, n;
        
        //获取用户输入数据,中间用空格隔开,回车键换行
        bagWeight = sc.nextInt();
        n = sc.nextInt();
        int[] weight = new int[n];
        int[] value = new int[n];
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) weight[i] = sc.nextInt();
        for (int i = 0; i < n; i++) value[i] = sc.nextInt();
        for (int i = 0; i < n; i++) nums[i] = sc.nextInt();

        int[] dp = new int[bagWeight + 1];

        //先遍历物品再遍历背包,作为01背包处理
        for (int i = 0; i < n; i++) {
            for (int j = bagWeight; j >= weight[i]; j--) {
                //遍历每种物品的个数
                for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) {
                    dp[j] = Math.max(dp[j], dp[j - k * weight[i]] + k * value[i]);
                }
            }
        }
        System.out.println(dp[bagWeight]);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/622425.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

海外动态IP:揭秘其背后的技术与应用

在数字化时代&#xff0c;网络技术的发展日新月异&#xff0c;其中海外动态IP作为网络通信技术的重要一环&#xff0c;逐渐走进公众视野。海外动态IP不仅为跨国企业提供了灵活的网络接入方案&#xff0c;还为个人用户带来了更多样化的网络体验。本文将深入探讨海外动态IP的技术…

【Docker学习】重启容器的docker restart

命令&#xff1a; docker container restart 描述&#xff1a; 重启一个或多个容器 用法&#xff1a; docker container restart [OPTIONS] CONTAINER [CONTAINER...] 别名&#xff1a; docker restart(docker的一些命令可以简写&#xff0c;docker restart就等同于docker cont…

树莓派|连接CSI接口摄像头+opencv

CSI&#xff08;Camera Serial Interface&#xff09;接口摄像头是一种常见的嵌入式系统或移动设备中使用的摄像头接口。它通常用于与处理器或图像传感器进行直接连接&#xff0c;实现高速的图像数据传输。 CSI接口摄像头具有以下特点&#xff1a; 高速传输&#xff1a;CSI接口…

免翻,剪映出品的AI作图和AI视频官网免费体验!

哈喽&#xff0c;各位小伙伴们好&#xff0c;我是给大家带来各类黑科技与前沿资讯的小武。 近日&#xff0c;据剪映 Dreamina 官方消息&#xff0c;Deramina正式更名为即梦&#xff0c;同时宣布其AI作图和AI视频生成功能已全量上线。 ▲ 官网主页面 AI作图 1、通过文字描述或…

华中科大:感谢大家,我的春招之旅结束了

今天在论坛上看到一个帖子&#xff0c;一位华中科大的同学&#xff0c;因为家中父亲突然病倒&#xff0c;发求助帖&#xff1a; 请问大家&#xff0c;春招走哪个方向能最快找到工作&#xff1f;还是说继续读研呢&#xff0c;但是家里急需钱…… 当时这个帖子直接热榜第一&…

Python练习04

目录 制作一个简易的注册登陆系统 实现过程 声明需要用到的库 构造一个判断用户文件是否存在的函数 构造一个存储用户文件的函数 制作UI 制作系统主体 运行效果 制作一个简易的注册登陆系统 通过所学知识制作一个简易的注册登陆系统&#xff0c;要求可以存储账户及密码&#…

省级生活垃圾无害化处理率面板数据(2004-2022年)

01、数据简介 生活垃圾无害化处理率是指经过处理的生活垃圾中&#xff0c;达到无害化标准的垃圾所占的比例。这一指标是衡量城市垃圾处理水平的重要标准&#xff0c;反映了城市对垃圾进行有效管理和处理的能力。 生活垃圾无害化处理的主要方式包括生活垃圾焚烧、生活垃圾卫生…

2024生日快乐祝福HTNL源码修复版

源码介绍 2024生日快乐祝福HTNL源码&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面&#xff0c; 源码截图 源码下载 2024生日快乐祝福HTNL源码

数据库编程

PL/SQL程序 1.PL/SOL程序块 整个PL/SQL块分三部分&#xff1a;声明部分、执行部分、异常处理部分&#xff1b; 示例&#xff1a; declare --变量声明 v_sno varchar2(10) : ‘04001’; v_cno varchar2(10) :‘001’; v_grade number : 90; begin --程序入口 insert…

PyQt程序的打包

Qt hello - 专注于Qt的技术分享平台 记录下PyQt程序的打包。 一&#xff0c;安装 pip3 install PyInstaller 二&#xff0c;打包 pyinstaller -w -n app app.py 根据需要选择打包参数&#xff0c;例如&#xff1a;-F表示生成单文件模式&#xff0c;即只有一个可执行文件…

使用Eigen将经纬度、高程、偏北角转成变换矩阵

目录 1、前言 2、示例 3、代码解析 4、垂直于给定点的切平面变换 5、代码解析 1、前言 在地球表面进行刚体变换时候&#xff0c;要将具有经纬度、高程和偏北角的坐标信息转换为变换矩阵表达&#xff0c;首先需要了解坐标系之间的转换关系。 通常&#xff0c;我们会将经纬…

攻防演练-防守单位常见防守策略

为方便您的阅读&#xff0c;可点击下方蓝色字体&#xff0c;进行跳转↓↓↓ 01 防守单位常见防守策略 01 防守单位常见防守策略 为普及网络安全知识&#xff0c;提高网络安全防范意识&#xff0c;和网络安全工作技能。我们将向大家介绍网络安全攻防演练中防守单位的一些关键策…

自回归模型的优缺点及改进方向

在学术界和人工智能产业中&#xff0c;关于自回归模型的演进与应用一直是一个引发深入讨论和多方观点交锋的热门议题。尤其是Yann LeCun&#xff0c;这位享誉全球的AI领域学者、图灵奖的获得者&#xff0c;以及被誉为人工智能领域的三大巨擘之一&#xff0c;他对于自回归模型持…

2.三极管

2.习题 3.知识补充

ssm125四六级报名与成绩查询系统+jsp

四六级报名与成绩查询系统的设计与实现 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对四六级报名信息管理混乱&am…

最短路(图论学习总结部分内容)

文章目录 前言二、最短路多源最短路 F l o y d Floyd Floyd​ 算法例题及变形 e g 1 &#xff1a; S o r t i n g I t A l l O u t eg1&#xff1a;Sorting\ It\ All\ Out eg1&#xff1a;Sorting It All Out ( 蓝书例题&#xff0c;传递闭包 ) (蓝书例题&#xff0c;传递闭包…

pytest教程-44-钩子函数-pytest_report_collectionfinish

领取资料&#xff0c;咨询答疑&#xff0c;请➕wei: June__Go 上一小节我们学习了pytest_report_header钩子函数的使用方法&#xff0c;本小节我们讲解一下pytest_report_collectionfinish钩子函数的使用方法。 pytest_report_collectionfinish 钩子函数在 pytest 完成所有测…

python智能电力监控与资费电费缴纳管理系统vue+django

本系统的设计与实现共包含6个表:分别是配置文件信息表&#xff0c;电力记录信息表&#xff0c;故障报修信息表&#xff0c;缴费订单信息表&#xff0c;用户表信息表&#xff0c;用户信息表&#xff0c; 本文所设计的电费缴纳系统的设计与实现拥有前端和后端&#xff0c;前端使…

C++入门必读-Qt的菜单控件

菜单控件 QT提供的菜单控件&#xff0c;可以帮助我们完成如下菜单的制作。这在项目开发中非常有用。 移除默认的菜单 从头开发菜单控件&#xff0c;我们先移除默认的菜单栏。 创建菜单栏 在窗体空白处&#xff0c;鼠标右键点击选择《创建菜单栏》 添加菜单内容 继续输入子菜单,…

有哪些可以用电脑做的挣钱副业,有电脑就行

以下是一些可以用电脑做的挣钱副业 1. 写作和翻译 可以在各大网络平台上接单进行写作或者翻译。 2. 做任务 还在做致米宝库这个软件&#xff0c;软件每天会发布一些项目任务&#xff0c;也能学到一些网上赚钱的知识技术&#xff0c;我平时就做些简单任务和一个虚拟项目。 任…