Leetcode 刷题笔记(二十五) ——动态规划篇之背包问题:完全背包

系列文章目录

一、 数组类型解题方法一:二分法
二、数组类型解题方法二:双指针法
三、数组类型解题方法三:滑动窗口
四、数组类型解题方法四:模拟
五、链表篇之链表的基础操作和经典题目
六、哈希表篇之经典题目
七、字符串篇之经典题目
八、字符串篇之 KMP
九、解题方法:双指针
十、栈与队列篇之经典题目
十 一、栈与队列篇之 top-K 问题
十 二、二叉树篇之二叉树的前中后序遍历
十 三、二叉树篇之二叉树的层序遍历及相关题目
十 四、二叉树篇之二叉树的属性相关题目
十 五、 二叉树篇之二叉树的修改与构造
十 六、 二叉树篇之二叉搜索树的属性
十 七、二叉树篇之公共祖先问题
十 八、二叉树篇之二叉搜索树的修改与构造
十 九、回溯算法篇之组合问题
二 十、回溯算法篇之分割、子集、全排列问题
二十一、贪心算法篇之入门题目
二十二、贪心算法篇之进阶题目
二十三、动态规划篇之基础题目
二十四、动态规划篇之背包问题:01背包
更新中 …


前言

刷题路线来自 :代码随想录
一个商品只能放入一次是01背包,而完全背包每个商品的数量没有数量限制,也就是可以放入背包多次
如:
在这里插入图片描述
dp 数组:
在这里插入图片描述

题录

440 · 背包问题 III

Lintcode 链接(Leetcode 上没有原题)
给定 n 种物品, 每种物品都有无限个. 第 i 个物品的体积为 A[i], 价值为 V[i].再给定一个容量为 m 的背包. 问可以装入背包的最大价值是多少?
在这里插入图片描述
题解:
因为每个物品可以多次放入,所以在放的下的情况下,若要放下新物品的最大价值为 dp[i][j - A[i - 1]] + V[i - 1]),不同与 01 背包,这里是 dp[i][] 而不是 dp[i - 1][],表示在放下新物品的本层dp 数组中寻找放下新物品后剩下的空间的最大价值。

public class Solution {
    public int backPackIII(int[] A, int[] V, int m) {
        // write your code here
        int row = V.length;
        int[][] dp = new int[row + 1][m + 1];
        for (int i = 1; i <= row; i++) {
            for (int j = 1; j <= m; j++) {
                if (j >= A[i - 1]) {   
                	// 放得下,注意这里为 ≥
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - A[i - 1]] + V[i - 1]);
                } else {
                // 放不下
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[row][m];
    }
}

优化(一维滚动 dp 数组):
不同于 01 背包,这里遍历背包时是从前向后遍历,因为以一个物品可以放入多次

public class Solution {
    public int backPackIII(int[] A, int[] V, int m) {
        // write your code here
        int row = V.length;
        int[] dp = new int[m + 1];
        for (int i = 0; i < row; i++) {  // 遍历物品
            for (int j = A[i]; j <= m; j++) {   // 遍历背包,从A[i],开始因为前边放不下,数组中的最大价值为不放入的值,不用改变
                dp[j] = Math.max(dp[j], dp[j - A[i]] + V[i]);
            }
        }
        return dp[m];
    }
}

518. 零钱兑换 II

Leetcode 链接
在这里插入图片描述
题解:
组合问题
第二层的循环遍历背包为从前向后遍历,下标从 coins[i] 开始,因为 coins[i] 之前放不下新物品,数组值不改变。

class Solution {
    public int change(int amount, int[] coins) {
        // 最大组合
        int row = coins.length;
        int[] dp = new int[amount + 1];
        dp[0] = 1;
        for (int i = 0; i < row; i++) {
        	// 从前向后遍历
            for (int j = coins[i]; j <= amount; j++) {
                dp[j] = dp[j] + dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
}

377. 组合总和 Ⅳ

Leetcode 链接
在这里插入图片描述
题解:
排列问题,不同于前边的组合问题不强调顺序。(1,5)和(5,1)是同一个组合,但是是两个不同的排列。这里需要满足以下两个条件

  1. 先遍历背包,后遍历物品
  2. 不能使用二维 dp 数组
class Solution {
    public int combinationSum4(int[] nums, int target) {
        int row = nums.length;
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for (int j = 1; j <= target; j++) {
            for (int i = 0; i < row; i++) {
                if (j >= nums[i]) {
                    dp[j] = dp[j] + dp[j - nums[i]];
                }
            }
        }
        return dp[target];
    }
}

70. 爬楼梯(进阶)

Leetcode 链接
将题中的可以爬 1 或 2 个台阶改为可以爬 1 或 2 或 … 或 m 个台阶。(力扣无原题)
在这里插入图片描述
题解:
排列问题

class Solution {
    public int climbStairs(int n) {
        int[] dp = new int[n + 1];
        dp[0] = 1;
        int m = 2; // m 表示每次可趴 1 - m 阶,这里为 1、2
        for (int j = 1; j <= n; j++) {   // 先遍历背包
            for (int i = 1; i <= m; i++) {   // 再遍历物品
                if (j >= i) {
                    dp[j] = dp[j] + dp[j - i];
                }
            }
        }
        return dp[n];
    }
}

322. 零钱兑换

Leetcode 链接
在这里插入图片描述
题解:
背包大小为 amount,求最放满背包物品的最少数量

递推公式:Math.min(放入前最少硬币数,放入后最少硬币数)
放入后最少硬币数 = 1 + 放入后背包剩余空间能放入的最少硬币数
初始化: 除了0下标位置外,全部初始化为Integer的最大值max,保证第一个硬币的遍历正常
难点: 怎么判断有满足背包大小的硬币组合吗? 如:[2, 5] amount = 3,返回 -1

  1. 如果背包大小不够放下第一个物品,dp数组当前位置值不改变,就为max。
  2. 如果背包能装下新物品,但是剩下的空间没有组合满足,让当前位置值不变,原来为如果max现在就还是max
  3. 所以max就表示没有满足背包大小的组合
class Solution {
    public int coinChange(int[] coins, int amount) {
        int row = coins.length;
        int max = Integer.MAX_VALUE;
        
        int[][] dp = new int[row + 1][amount + 1];
        // dp[0][i] 中除dp[0][0] 为 0外,全初始化为 max 
        // 在背包刚好装下新物品时,1 + dp[i][0] = 1
        for (int i = 1; i <= amount; i++) {
            dp[0][i] = max;
        }
        
        for (int i = 1; i <= row; i++) {
            for (int j = 1; j <= amount; j++) {
                if (j >= coins[i - 1] && (dp[i][j - coins[i - 1]] != max)) {
                	// 如果背包能放下新物品,并且背包剩余大小有满足的组合
                    dp[i][j] = Math.min(dp[i - 1][j], 1 + dp[i][j - coins[i - 1]]);
                } else {
                	// 放不下或者 dp[i][j - coins[i - 1]] = max,表示无满足剩余背包大小的组合
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[row][amount] == max ? -1 : dp[row][amount];
    }
}

空间优化:

class Solution {
    public int coinChange(int[] coins, int amount) {
        int row = coins.length;
        int max = Integer.MAX_VALUE;
        
        int[] dp = new int[amount + 1];
        // 除dp[0] 外 都为max,dp[0] = 0
        for (int i = 1; i <= amount; i++) {
            dp[i] = max;
        }
        
        for (int i = 0; i < row; i++) {
            for (int j = coins[i]; j <= amount; j++) {
                if ((dp[j - coins[i]] != max)) {
                	// 放下后,有满足背包剩余大小的组合
                    dp[j] = Math.min(dp[j], 1 + dp[j - coins[i]]);
                } 
            }
        }
        return dp[amount] == max ? -1 : dp[amount];
    }
}

279. 完全平方数

Leetcode 链接
在这里插入图片描述
题解:
同上题,这里的物品不同。背包大小为 n, 用数量不限的完全平方数大小的物品装满

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        int max = Integer.MAX_VALUE;
        for (int i = 1; i <= n; i++) {
            dp[i] = max;
        }
        for (int i = 1; i * i <= n; i ++) {  
            for (int j = i * i; j <= n; j++) {
            	// 因为物品大小是从最小的 1 开始,所以不存在没有满足背包大小的组合情况, N 个1可以放满任何大小的背包,这里的if 语句可以省略
                //if (dp[j - i * i] != max) 
                    dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
}

139. 单词拆分

Leetcode 链接
在这里插入图片描述
题解:
dp[i]:[0, i] 的子串被分割后能在字典中找到
递推公式:dp[j] = dp[i] && 子串 [i , j] 能在字典中找到

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

总结

  1. 统一 i 为物品下标,j 为背包下标。
  2. 当新增物品刚好放入背包时怎么做,决定怎么初始化 dp 数组。

01 背包与完全背包区别

  1. 01 背包中物品只能使用一次,完全背包物品可以使用无数次。
  2. 递推公式区别: 01背包在新物品放入有剩余的时候,剩余空间怎么放物品是不考虑放入过的新物品的,而完全背包可以再次放入新物品。所以二维递推公式前者背包剩余空间怎么放物品需要在没人放入前dp[i - 1][剩余V] 找,而完全背包就可以在 放入后的dp[i][剩余V] 找。而一维滚动数组, 01 背包是从后往前遍历,这样原来的状态得到最大程度的保留,而完全背包为从前往后遍历。
  3. 注意使用二维dp时,i = 0 留给初始化,保证 dp[i - 1][] 不会越界,所以物品数组 nums[i - 1] 为新添加的物品,而一维滚动dp数组时,物品从 0 下标开始,两者遍历的初始下标不同
  4. 使用一维dp数组时可以修改遍历的起始下标,代替二维 dp 数组中的内否放入背包的判断。

背包问题的变形

  1. 物品的最大价值(和)
    能放入时最大价值在放入新物品和 放入新物品产生的两种结果中取最大值
    01 背包:
    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - A[i - 1]] + V[i - 1])
    dp[j] = Math.max(dp[j], dp[j - A[i]] + V[i]) (遍历背包时从后向前遍历)
    完全背包:
    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - A[i - 1]] + V[i - 1])
    dp[j] = Math.max(dp[j], dp[j - A[i]] + V[i]) (遍历背包时从前向后遍历)

  2. 背包中物品的组合
    能放入时组合数等于放入新物品组合数 放入新物品组合数
    01 背包:
    dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]];
    dp[j] = dp[j] + dp[j - nums[i]]; (遍历背包时从后向前遍历)
    完全背包:
    dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i - 1]];
    dp[j] = dp[j] + dp[j - nums[i]]; (遍历背包时从前向后遍历)

  3. 背包中物品的排列
    能放入时组合数等于放入新物品排列数 放入新物品排列数
    完全背包:
    dp[j] = dp[j] + dp[j - nums[i]](不可使用二维 dp 数组,必须先遍历背包,再遍历物品)

  4. 放满背包的最少物品
    初始化时 dp[0][0] = dp[0] = 1,dp[0][1~i] = dp[1 ~i] = max
    完全背包:
    if (dp[j - i * i] != max),有能满足的组合
    dp[i][j] = Math.min(dp[i - 1][j], 1 + dp[i][j - coins[i - 1]]);
    dp[j] = Math.min(dp[j], 1 + dp[j - coins[i]]);

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值