【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];
        //初始化
        for (int j = 0; j <= n; j++) {
            dp[j] = max;
        }
	//如果不想要寫for-loop填充數組的話,也可以用JAVA內建的Arrays.fill()函數。
	//Arrays.fill(dp, Integer.MAX_VALUE);
	
        //当和为0时,组合的个数为0
        dp[0] = 0;
        // 遍历物品
        for (int i = 1; i * i <= n; i++) {
            // 遍历背包
            for (int j = i * i; j <= n; j++) {
                //if (dp[j - i * i] != max) {    这是为了防止凑不成
                    dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
                //}
            }
        }
        return 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]);
    }
}

  • 13
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值