摘要
本文主要介绍了完全背包理论基础、以及LeetCode动态规划的两个题目,包括518. 零钱兑换 II和377. 组合总和 Ⅳ。
1、完全背包
1.1 题目描述
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次) ,求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
1.2 思路
-
dp
数组以及下标的含义: 在一维dp
数组中,dp[j]
表示:容量为j的背包,所背的物品价值可以最大为dp[j]
。 -
递推公式:
-
第一种情况:放不下物品,0-i的物品, j容量可以放下的最大价值等于0-(i-1)的物品,j-1容量的价值
dp[j] = dp[j]
-
第二种情况:可以放下物品,由
dp[j - weight[i]]
推出,dp[j - weight[i]]
为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[j - weight[i]] + value[i]
(物品i的价值),就是背包放物品i得到的最大价值dp[j] = Math.max(dp[j], value[i] + dp[j-weight[i]])
-
如果
j < items[i].weight
则放不下物品,反之可以放下物品
-
-
dp
数组如何初始化 -
dp
数组遍历顺序:先遍历物品,然后遍历背包,背包是正序遍历 -
打印
dp
数组
1、01背包和完全背包的区别?
01背包和完全背包唯一不同就是体现在遍历顺序上,都是先遍历物品,然后遍历背包,但是01背包遍历背包是倒序遍历,完全背包遍历背包是正序遍历
2、为什么完全背包是正序遍历?
正序遍历物品更适合完全背包问题,因为你需要考虑是否将当前物品添加到背包中,以获得最大化的总价值。正序遍历背包容量时,你可以利用之前计算得到的结果,根据状态转移方程来更新当前背包容量的值。
1.3 代码
public class Rucksack3 {
public static void main(String[] args) {
Item item1 = new Item(1, 15);
Item item2 = new Item(3, 20);
Item item3 = new Item(4, 30);
Item[] items = {item1, item2, item3};
int m = 4;
rucksack(items, m);
}
// 滚动数组就是把上一层数组拷贝下来了
// dp[j] 放入重量为 j 的背包的最大价值为 dp[j]
// 不放物品 dp[j] = dp[j]
// 放物品 dp[j] = Math.max(dp[j], items[i].value + dp[j-items[i].weight])
// 如果j < j-items[i].weight] 则不取物品,反之可以取物品
public static void rucksack(Item[] items, int m) {
int n = items.length;
int[] dp = new int[m + 1];
// 先遍历物品,然后遍历背包,背包是正序遍历
for (int i = 0; i < n; i++) {
for (int j = 1; j <= m; j++) {
if (j < items[i].weight) {
dp[j] = dp[j];
} else {
dp[j] = Math.max(dp[j], items[i].value + dp[j - items[i].weight]);
}
}
}
// 打印
print(dp, n, m);
}
public static void print(int[] dp, int n, int m) {
for (int j = 0; j <= m; j++) {
System.out.print(dp[j] + "\t");
}
}
public static class Item {
public int weight;
public int value;
public Item() {
}
public Item(int weight, int value) {
this.weight = weight;
this.value = value;
}
}
}
2、518. 零钱兑换 II
2.1 思路
动规五部曲
-
dp
数组以及下标的含义- dp[j] 表示可以凑成总金额 j 的方式为 dp[j]
-
递推公式
- dp[j] += dp[j - coins[i]]
- 这个递推公式大家应该不陌生了,我在讲解01背包题目的时候在这篇494. 目标和中就讲解了,求装满背包有几种方法,公式都是:dp[j] += dp[j - nums[i]];
-
dp
数组如何初始化- 首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。
- dp[0]=1还说明了一种情况:如果正好选了coins[i]后,也就是j-coins[i] == 0的情况表示这个硬币刚好能选,此时dp[0]为1表示只选coins[i]存在这样的一种选法。
-
dp
数组遍历顺序- 完全背包,物品和背包都是从小到大的遍历顺序
-
举例推导
dp
数组
1、如何推导出递推公式:dp[j] += dp[j - coins[i]]
?
dp[j]
表示总金额为j
时的组合方法数。dp[j - coins[i]]
表示在不考虑当前硬币coins[i]
的情况下,总金额为j - coins[i]
时的组合方法数
2.2 代码
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=1; j<=amount; j++) {
if(j >= coins[i]) {
dp[j] += dp[j-coins[i]];
}
}
}
return dp[amount];
}
3、377. 组合总和 Ⅳ
3.1 思路
本题与动态规划:518.零钱兑换II就是一个鲜明的对比,一个是求排列,一个是求组合,遍历顺序完全不同。
- 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
- 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
1、什么是组合和排列?
本题题目描述说是求组合,但又说是可以元素相同顺序不同的组合算两个组合,其实就是求排列!
组合不强调顺序,(1,5)和(5,1)是同一个组合。
排列强调顺序,(1,5)和(5,1)是两个不同的排列
2、为什么求排列数就是外层for遍历背包,内层for循环遍历物品?
在完全背包问题中,每种物品可以被选择多次,因此考虑不同顺序的选择是合理的。如果你希望计算排列数,那么你需要考虑不同物品的选择顺序,这就需要在内层循环中遍历不同的物品。
3.2 代码
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target+1];
dp[0] = 1;
for(int j=1; j<=target; j++) {
for(int i=0; i<nums.length; i++) {
if(j >= nums[i]) {
dp[j] += dp[j-nums[i]];
}
}
}
return dp[target];
}