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

news2024/10/6 8:40:08

目录

理论基础

 一、基础题目

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

P8802 [蓝桥杯 2022 国 B] 出差

P8802 [蓝桥杯 2022 国 B] 出差 分析 很明显&#xff1a;单源最短路径 没有负权边 dijkstra 1.存图 2.准备两个数组 dis[]&#xff1a;更新源点到各个点的距离 vis[]&#xff1a;标记是否访问 3.从源点开始&#xff0c;更新源点到与其邻接的点的距离&#xff0c;每次选…

探索 Joomla! CMS:打造个性化网站的利器

上周我们的Hostease客户咨询建站服务。他想要用Joomla建站。Hostease提供免费安装Joomla CMS服务。这可以让客户搭建网站变得更加简单和高效。下面是针对Joomla建站的一些使用心得。 Joomla CMS是一款开放自由的软件&#xff0c;为用户提供了创建和维护网站的自由度。它经过全…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-15.4讲--ARM异常中断返回

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

【已解决】QT C++中QLineEdit不可粘贴输入

本博文源于生产实际&#xff0c;主要解决LineEdit不可粘贴输入的情况。下面将进行具体分析 问题来源 输入框只能一个个输入&#xff0c;不可复制粘贴。 分析 给QLineEdit装一个监听事件&#xff0c;监听它的事件即可。 问题解决步骤 问题一共分为三步&#xff1a; 书写监…

认养小游戏功能介绍

认养小游戏通常模拟了真实的农业生产过程&#xff0c;让玩家能够在线上体验种植、养殖的乐趣。以下是一些常见的认养小游戏功能介绍&#xff1a; 选择认养的农产品&#xff1a;首先&#xff0c;玩家可以从游戏中提供的多种农产品中选择自己想要认养的种类&#xff0c;如蔬菜、…

深入了解模拟和存根:提高单元测试质量的关键技术

一、引言 在进行单元测试时&#xff0c;我们经常会遇到对外部资源的依赖&#xff0c;如数据库、网络接口等。模拟&#xff08;Mocking&#xff09;和存根&#xff08;Stubbing&#xff09;是两种帮助我们模拟这些外部资源&#xff0c;使我们能够在隔离环境中测试单元的方法。在…

PyQt6--Python桌面开发(6.QLineEdit单行文本框)

QLineEdit单行文本框 import sys import time from PyQt6.QtGui import QValidator,QIntValidator from PyQt6.QtWidgets import QApplication,QLabel,QLineEdit from PyQt6 import uicif __name__ __main__:appQApplication(sys.argv)uiuic.loadUi("./QLine单行文本框.u…

618值得入手的平价好物清单,看完再买不吃亏!

即将到来的618年中购物狂欢节&#xff0c;无疑是一年一度的购物盛宴。为了让大家的购物体验更加愉悦和充实&#xff0c;我特地为大家精选了一系列好物。如果你也打算在618尽情购物&#xff0c;那就赶紧收藏这份清单吧&#xff01; 一、舒适佩戴不伤耳——南卡骨传导耳机Runner…

MySQL数据库实验三

本文承接前面的俩次实验基础上完成&#xff0c;不过实现的都是基础操作的练习 目录 目录 前言 实验目的 实验要求 实验内容及步骤 updata操作 delete操作 alter操作 添加列 删除列 修改列的数据类型 要求实现 实验结果 代码结果 注意事项 思考题 总结 前言 本文是MySQL数据库…

有了Supervisor,再也不用担心程序撂挑子了!

目录 1. 概述 2. 问题场景 3. Supervisor 简介 4.部署流程 4.1. 安装 Supervisor 4.2. 自定义服务配置文件 4.3. 自定义应用配置文件 4.4. 启动 supervisord 服务进程 4.5. 启动 supervisorctl 客户端进程 4.6. 验证 supervisor 的监控重启特性 5. 高级特性 5.1. 进…

大模型微调之 在亚马逊AWS上实战LlaMA案例(七)

大模型微调之 在亚马逊AWS上实战LlaMA案例&#xff08;七&#xff09; 微调SageMaker JumpStart上的LLaMA 2模型 这是在us-west-2的测试结果。 展示了如何使用SageMaker Python SDK部署预训练的Llama 2模型&#xff0c;并将其微调到你的数据集&#xff0c;用于领域适应或指令…

Python注意事项【自我维护版】

各位大佬好 &#xff0c;这里是阿川的博客 &#xff0c; 祝您变得更强 个人主页&#xff1a;在线OJ的阿川 大佬的支持和鼓励&#xff0c;将是我成长路上最大的动力 阿川水平有限&#xff0c;如有错误&#xff0c;欢迎大佬指正 本篇博客在之前的博客上进行的维护 创建Python…

Java转Kotlin调用JNI方法异常

一、背景 Java调用JNI方法时没有任何问题&#xff0c;但是使用Java转Kotlin以后出现了崩溃异常&#xff1a;A java_vm_ext.cc:597] JNI DETECTED ERROR IN APPLICATION: jclass has wrong type: 校验参数后没有任何变化&#xff0c;经过分析验证找到解决方案 二、原因…

Python中的多进程、多线程、协程

Python中的多线程、多进程、协程 一、概述 1. 多线程Thread &#xff08;threading&#xff09;&#xff1a; 优点&#xff1a;同一个进程中可以启动多个线程&#xff0c;充分利用IO时&#xff0c;cpu进行等待的时间缺点&#xff1a;相对于进程&#xff0c;多线程只能并发执…

Windows:管理用户账户,密码策略和安全配置

在Windows操作系统中&#xff0c;管理用户账户和密码策略是确保系统安全的关键步骤。本文将探讨如何通过PowerShell和其他Windows工具管理用户账户&#xff0c;包括查看和设置密码策略、检查用户状态&#xff0c;以及导出和导入安全策略。这些管理任务对于系统管理员尤其重要&a…

STM32学习和实践笔记(25):USART(通用同步、异步收发器)

一&#xff0c;STM32的USART简介 USART即通用同步、异步收发器&#xff0c;它能够灵活地与外部设备进行全双工数据交换&#xff0c;满足外部设备对工业标准 NRZ 异步串行数据格式的要求。 UART即通用异步收发器&#xff0c;它是在USART基础上裁剪掉了同步通信功能。 开发板上…

Star-CCM+分配零部件至区域2-根据零部件的特性分组分配零部件至区域

前言 前文已经讲解了将零部件分配至区域的方法。其中有一种方法是"将所有部件分配到一个区域"。在工程应用中&#xff0c;有时会把同一种类型的部件分配到一个区域&#xff0c;因此在一个项目中有可能需要多次进行"将所有部件分配到一个区域"。如在电机温…

主机通过带光发端和ops接收端控制屏串口调试记录

场景就是主机电脑使用cutecom通过光纤口再到ops接收端从而控制屏过程 光纤口有个发送端波特率&#xff0c;Ops有接收端波特率&#xff0c;屏有自己的波特率&#xff0c;主机电脑可以通过发串口指令去设置发送端波特率和ops接收端波特率。因为主机只有一个&#xff0c;屏有多种…

【机器学习300问】82、RMSprop梯度下降优化算法的原理是什么?

RMSprop&#xff0c;全称Root Mean Square Propagation&#xff0c;中文名称“均方根传播”算法。让我来举个例子给大家介绍一下它的原理&#xff01; 一、通过举例来感性认识 建议你第一次看下面的例子时忽略小括号里的内容&#xff0c;在看完本文当你对RMSprop有了一定理解时…

智能助手上线,大模型提供云服务专属顾问

业务背景 在使用云服务的时候&#xff0c;当您遇到复杂问题&#xff0c;如配置、关联或计费方式不明确时&#xff0c;可能需要向客服提交工单进行技术沟通。在漫长的工作过程中&#xff0c;耗费了宝贵的时间和精力。 2024 年 4 月&#xff0c;百度智能云正式推出了融合文心大…