完全背包
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
同样leetcode上没有纯完全背包问题,都是需要完全背包的各种应用,需要转化成完全背包问题,所以我这里还是以纯完全背包问题进行讲解理论和原理。
在下面的讲解中,我拿下面数据举例子:
背包最大重量为4,物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
每件商品都有无限个!
问背包能背的物品最大价值是多少?
1. 确定dp数组以及下标的含义
dp[i] [j] 表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少。
2. 确定递推公式
这里在把基本信息给出来:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
对于递推公式,首先我们要明确有哪些方向可以推导出 dp[i][j]。
这里依然拿dp[1] [4]的状态来举例:
求取 dp[1] [4] 有两种情况:
- 放物品1
- 还是不放物品1
如果不放物品1, 那么背包的价值应该是 dp[0] [4] 即 容量为4的背包,只放物品0的情况。
推导方向如图:
如果放物品1, 那么背包要先留出物品1的容量,目前容量是4,物品1 的容量(就是物品1的重量)为3,此时背包剩下容量为1。
容量为1,只考虑放物品0 和物品1 的最大价值是 dp[1][1], 注意 这里和 01背包理论基础(二维数组) (opens new window)有所不同了!
在 01背包理论基础(二维数组) (opens new window)中,背包先空留出物品1的容量,此时容量为1,只考虑放物品0的最大价值是 dp[0] [1],因为01背包每个物品只有一个,既然空出物品1,那背包中也不会再有物品1!
而在完全背包中,物品是可以放无限个,所以 即使空出物品1空间重量,那背包中也可能还有物品1,所以此时我们依然考虑放 物品0 和 物品1 的最大价值即: dp[1] [1], 而不是 dp[0] [1]
所以 放物品1 的情况 = dp[1] [1] + 物品1 的价值,推导方向如图:
两种情况,分别是放物品1 和 不放物品1,我们要取最大值(毕竟求的是最大价值)
dp[1][4] = max(dp[0][4], dp[1][1] + 物品1 的价值)
以上过程,抽象化如下:
- 不放物品i:背包容量为j,里面不放物品i的最大价值是dp[i - 1] [j]。
- 放物品i:背包空出物品i的容量后,背包容量为j - weight[i],dp[i] [j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
递推公式: dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
(注意,完全背包二维dp数组 和 01背包二维dp数组 递推公式的区别,01背包中是 dp[i - 1][j - weight[i]] + value[i])
)
3. dp数组如何初始化
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
首先从dp[i] [j]的定义出发,如果背包容量j为0的话,即dp[i] [0],无论是选取哪些物品,背包价值总和一定为0。如图:
在看其他情况。
状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
可以看出有一个方向 i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
dp[0] [j],即:存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
那么很明显当 j < weight[0]
的时候,dp[0] [j] 应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]
时,dp[0] [j] 如果能放下weight[0]的话,就一直装,每一种物品有无限个。
代码初始化如下:
for (int i = 1; i < weight.size(); i++) { // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。
dp[i][0] = 0;
}
// 正序遍历,如果能放下就一直装物品0
for (int j = weight[0]; j <= bagWeight; j++)
dp[0][j] = dp[0][j - weight[0]] + value[0];
(物品有无限个)
此时dp数组初始化情况如图所示:
dp[0] [j] 和 dp[i] [0] 都已经初始化了,那么其他下标应该初始化多少呢?
其实从递归公式: dp[i][j] = max(dp[i - 1] [j], dp[i] [j - weight[i]] + value[i]); 可以看出dp[i] [j] 是由上方和左方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。
但只不过一开始就统一把dp数组统一初始为0,更方便一些。
最后初始化代码如下:
// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagWeight; j++) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
4. 确定遍历顺序
01背包二维DP数组,先遍历物品还是先遍历背包都是可以的。
因为两种遍历顺序,对于二维dp数组来说,递推公式所需要的值,二维dp数组里对应的位置都有。
所以既可以 先遍历物品再遍历背包:
for (int i = 1; i < n; i++) { // 遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
}
}
也可以 先遍历背包再遍历物品:
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
for (int i = 1; i < n; i++) { // 遍历物品
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
}
}
5. 举例推导dp数组
以本篇举例数据为例,填满了dp二维数组如图:
因为 物品0 的性价比是最高的,而且 在完全背包中,每一类物品都有无限个,所以有无限个物品0,既然物品0 性价比最高,当然是优先放物品0。
52.携带研究材料
题目描述
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的重量,并且具有不同的价值。
小明的行李箱所能承担的总重量是有限的,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。
输入描述
第一行包含两个整数,n,v,分别表示研究材料的种类和行李所能承担的总重量
接下来包含 n 行,每行两个整数 wi 和 vi,代表第 i 种研究材料的重量和价值
输出描述
输出一个整数,表示最大价值。
输入示例
4 5
1 2
2 4
3 4
4 5
输出示例
10
提示信息
第一种材料选择五次,可以达到最大值。
数据范围:
1 <= n <= 10000; 1 <= v <= 10000; 1 <= wi, vi <= 10^9.
def totalBag(n,bag_weight,bag,value):
dp=[[0]*(1+bag_weight) for _ in range(n)]
for i in range(bag[0],bag_weight+1):
dp[0][i]=dp[0][i-bag[0]]+value[0]
for i in range(1,n):
for j in range(bag_weight+1):
if j<bag[i]:
dp[i][j]=dp[i-1][j]
else:
dp[i][j]=max(dp[i-1][j],dp[i][j-bag[i]]+value[i])
return dp[n-1][bag_weight]
n,bag_weight=map(int,input().split())
bag=[]
value=[]
for i in range(n):
a,b=map(int,input().split())
bag.append(a)
value.append(b)
print(totalBag(n,bag_weight,bag,value))
518.零钱兑换II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
- 输入: amount = 5, coins = [1, 2, 5]
- 输出: 4
解释: 有四种方式可以凑成总金额:
- 5=5
- 5=2+2+1
- 5=2+1+1+1
- 5=1+1+1+1+1
示例 2:
- 输入: amount = 3, coins = [2]
- 输出: 0
- 解释: 只用面额2的硬币不能凑成总金额3。
示例 3:
- 输入: amount = 10, coins = [10]
- 输出: 1
注意,你可以假设:
-
0 <= amount (总金额) <= 5000
-
1 <= coin (硬币面额) <= 5000
-
硬币种类不超过 500 种
-
结果符合 32 位符号整数
纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!
注意题目描述中是凑成总金额的硬币组合数,为什么强调是组合数呢?
例如示例一:
5 = 2 + 2 + 1
5 = 2 + 1 + 2
这是一种组合,都是 2 2 1。
如果问的是排列数,那么上面就是两种排列了。
组合不强调元素之间的顺序,排列强调元素之间的顺序。
本题的一维dp中,难点在于理解便利顺序。
在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
#二维
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
dp=[[0]*(amount+1) for _ in range(len(coins))]
#dp[i][j]代表从0-i中选取物品,能凑成j的方式
for i in range(len(coins)):
dp[i][0]=1
for j in range(1,amount+1):
dp[0][j]=dp[0][j-coins[0]]
for i in range(1,len(coins)):
for j in range(1,amount+1):
if j<coins[i]:
dp[i][j]=dp[i-1][j]
else:
dp[i][j]=dp[i-1][j]+dp[i][j-coins[i]]
return dp[len(coins)-1][amount]
#一维
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
dp=[0]*(amount+1)
#dp[j] 凑成j的方式
dp[0]=1
for i in range(len(coins)):
for j in range(coins[i],amount+1):
dp[j]+=dp[j-coins[i]]
return dp[amount]
377. 组合总和 Ⅳ
难度:中等
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
- nums = [1, 2, 3]
- target = 4
所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)
请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。
本题题目描述说是求组合,但又说是可以元素相同顺序不同的组合算两个组合,其实就是求排列!
弄清什么是组合,什么是排列很重要。
组合不强调顺序,(1,5)和(5,1)是同一个组合。
排列强调顺序,(1,5)和(5,1)是两个不同的排列。
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
dp=[0]*(target+1)
dp[0]=1
for j in range(1,target+1):
for i in range(len(nums)):
if j>=nums[i]:
dp[j]+=dp[j-nums[i]]
return dp[target]
'''
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。'''
70. 爬楼梯(进阶版)
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
输入描述:输入共一行,包含两个正整数,分别表示n, m
输出描述:输出一个整数,表示爬到楼顶的方法数。
输入示例:3 2
输出示例:3
提示:
当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。
此时你有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶段
- 1 阶 + 2 阶
- 2 阶 + 1 阶
我们之前做的 爬楼梯 是只能至多爬两个台阶。
这次改为:一步一个台阶,两个台阶,三个台阶,.......,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?
这又有难度了,这其实是一个完全背包问题。
1阶,2阶,.... m阶就是物品,楼顶就是背包。
每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。
问跳到楼顶有几种方法其实就是问装满背包有几种方法。
此时大家应该发现这就是一个完全背包问题了!
n, m = map(int, input().split())
dp=[0]*(n+1)
dp[0]=1
for i in range(1,n+1):
for j in range(1,m+1):
if i>=j:
dp[i]+=dp[i-j]
print(dp[n])
322. 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
- 输入:coins = [1, 2, 5], amount = 11
- 输出:3
- 解释:11 = 5 + 5 + 1
示例 2:
- 输入:coins = [2], amount = 3
- 输出:-1
示例 3:
- 输入:coins = [1], amount = 0
- 输出:0
示例 4:
- 输入:coins = [1], amount = 1
- 输出:1
示例 5:
- 输入:coins = [1], amount = 2
- 输出:2
提示:
- 1 <= coins.length <= 12
- 1 <= coins[i] <= 2^31 - 1
- 0 <= amount <= 10^4
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
#dp[j] 凑成金额j用的最少的硬币数
coins_max=float('inf')
dp=[coins_max]*(amount+1)
dp[0]=0
for i in range(len(coins)):
for j in range(coins[i],amount+1):
dp[j]=min(dp[j],dp[j-coins[i]]+1)
if dp[amount]==coins_max:
return -1
return dp[amount]
279.完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
- 输入:n = 12
- 输出:3
- 解释:12 = 4 + 4 + 4
示例 2:
- 输入:n = 13
- 输出:2
- 解释:13 = 4 + 9
提示:
- 1 <= n <= 10^4
class Solution:
def numSquares(self, n: int) -> int:
#dp[j] 从0-j中的完全平方数,凑成j的完全平方数的最少数量
dp=[float('inf')]*(n+1)
dp[0]=0
for i in range(1,int(n**0.5)+1):#物品
for j in range(i**2,n+1):#背包
dp[j]=min(dp[j-i**2]+1,dp[j])
return dp[n]
139.单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
- 输入: s = "leetcode", wordDict = ["leet", "code"]
- 输出: true
- 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
示例 2:
- 输入: s = "applepenapple", wordDict = ["apple", "pen"]
- 输出: true
- 解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
- 注意你可以重复使用字典中的单词。
示例 3:
- 输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
- 输出: false
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
n=len(s)
dp=[False]*(n+1)
dp[0]=True
for i in range(1,n+1):#背包
for j in range(i):#物品
if dp[j] and s[j:i] in wordDict:
dp[i]=True
break
return dp[n]
#dp[i] 表示字符串 s 的前 i 个字符是否可以被拆分成字典中的单词。
'''
n = 8,dp 数组的长度为 9。
dp[0] = True。
对于 i = 4:
j 从 0 遍历到 3。
当 j = 0,子字符串 s[0:4] = "leet" 存在于字典中,所以 dp[4] = True。
对于 i = 8:
j 从 0 遍历到 7。
当 j = 4,dp[4] 已为 True,子字符串 s[4:8] = "code" 存在于字典中,所以 dp[8] = True。
最终返回 dp[8] = True。
'''