先讲一下01背包问题:
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
这道题目如果使用暴力解法,即回溯法来做的话,时间复杂度很大,,n表示物品的数量。
如果使用动态规划的方法来做:
1、确定dp数组以及下标的含义
我们需要使用二维数组的两个维度分别表示,物品 和 背包容量,即dp[i][j]。
i 来表示物品、j表示背包容量
dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
2、确定递推公式
对于递推公式,首先我们要明确有哪些方向可以推导出 dp[i][j]
这里我们dp[1][4]的状态来举例:
dp[1][4] 来自于 放物品1 ,还是不放物品1。
如果不放物品1, 那么背包的价值应该是 dp[0][4] 即容量为4的背包,只放物品0的情况。
如果放物品1, 那么背包要先留出物品1的容量,目前容量是4,物品1 需要重量为3,此时背包剩下容量为1。容量为1,只考虑放物品0 的最大价值是 dp[0][1],这个值我们之前就计算过。
所以 放物品1 的情况 = dp[0][1] + 物品1 的价值
两种情况,分别是放物品1 和 不放物品1,我们要取最大值
以上过程,抽象化如下:
-
不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。
-
放物品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得到的最大价值。
递归公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
3、dp数组如何初始化
1.如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。
2.dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
那么很明显当 j < weight[0]
的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]
时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
3.其他下标应该初始化多少?dp[i][j] 是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。
4、确定遍历顺序
先遍历物品还是先遍历背包,其实都可以
5、举例推导dp数组
KamaCoder 46. 携带研究材料 (01背包问题)
题目链接:01背包问题
题目讲解:代码随想录
题目描述:小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。
小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料只能选择一次,并且只有选与不选两种选择,不能进行切割。
输入描述
第一行包含两个正整数,第一个整数 M 代表研究材料的种类,第二个正整数 N,代表小明的行李空间。
第二行包含 M 个正整数,代表每种研究材料的所占空间。
第三行包含 M 个正整数,代表每种研究材料的价值。
输出描述
输出一个整数,代表小明能够携带的研究材料的最大价值。
这道题目,M可以看成是物体中重量,N可以看成物体的价值。
package main
import(
"fmt"
)
func max(a, b int) int{
if a < b{
a = b
}
return a
}
func main(){
// 定义 物品种类,和背包大小
var itemKinds, bagWeight int
fmt.Scan(&itemKinds, &bagWeight)
// 定义 每种物品所占背包的大小
// 定义 每种物品的价值
var weights, values []int
weights = make([]int, itemKinds)
values = make([]int, itemKinds)
for i := 0; i < itemKinds; i++{
fmt.Scan(&weights[i])
}
for i := 0; i < itemKinds; i++{
fmt.Scan(&values[i])
}
// dp[i][j] -- 0--i物品任意选(选/不选),行李空间为j时 最大价值
// dp[i][j] = dp[i-1][j] (不选物品i) , 选i dp[i-1][j-weight[i]] + values[i])
dp := make([][]int, itemKinds)
for i := 0; i < itemKinds; i++{
dp[i] = make([]int, bagWeight+1)
}
// 初始化dp,当背包容量为0时,放不进任何物品,所以肯定为0
// 对第一个物品,如果背包容量小于第一个物品的weight,则
// 放不进;背包容量大于等于第一个物品的weight,则直接放第一个物品
for j := weights[0]; j <= bagWeight; j++{
dp[0][j] = values[0]
}
// 开始遍历,对每一物品,遍历背包容量
for i := 1; i < itemKinds; i++{
for j := 1; j <= bagWeight; j++{
if j < weights[i]{ // 物品i的大小比当前背包容量大,物品i不放入背包
dp[i][j] = dp[i-1][j]
}else{ // 物品i放入背包中
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i]] + values[i])
}
}
}
fmt.Println(dp[itemKinds-1][bagWeight])
}
LeetCode 416. 分割等和子集
题目链接:416. 分割等和子集
题目讲解:代码随想录
题目描述:给你一个 只包含正整数 的 非空 数组
nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
- 背包的大小为sum / 2
- 物品 相当于集合里的元素,重量为元素的数值,价值也为元素的数值
func canPartition(nums []int) bool {
sum := 0
for _, num := range nums{
sum += num
}
// 如果 nums 的总和为奇数,则不可能平分成两个子集
if sum % 2 == 1{
return false
}
target := sum / 2
dp := make([][]int, len(nums))
for i := 0; i < len(nums); i++{
dp[i] = make([]int, target+1)
}
for j := nums[0]; j <= target; j++{
dp[0][j] = nums[0]
}
for i := 1; i < len(nums); i++{
for j := 0; j <= target; j++{
if j < nums[i]{
dp[i][j] = dp[i-1][j]
}else{
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i])
}
}
}
return dp[len(nums)-1][target] == target
}
func max(a, b int)int{
if a > b{
return a
}
return b
}