1背包问题 (一维)0-1背包问题,每个物品只能被选择一次
视频讲解:
递推公式:
最终,我们希望的 dp[j]
是这两种方案的最大值:
这个公式的意思是:
- dp[j]:当前的行李箱容量是
j
时,它的最优价值是多少? - dp[j - weight[i]]:如果选了当前材料,它占用了
weight[i]
的空间,所以剩下的空间是j - weight[i]
,而这个空间的最优价值已经计算过了。 - dp[j - weight[i]] + value[i]:意味着加上当前材料的价值,看看总价值有没有提升。
- 遍历顺序为逆序遍历:
-
#include<iostream> #include<vector> using namespace std; int main() { int a,b; cin>>a>>b; vector<int>weight(a); vector<int>value(a);//首先需要定义数组 vector<int>dp(b+1); for(int i=0;i<a;i++)cin>>weight[i]; for(int j=0;j<a;j++)cin>>value[j]; for(int i=0;i<a;i++)//遍历每一种物品,每一轮判断是否装入该物品,并不断更新dp[i] { for(int j=b;j>=weight[i];j--)//逆序遍历,0-1背包问题避免一个物体在同一轮中被重复选择 { dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);//递推公式 } } cout<<dp[b]; }
举例子来验证会更清晰
-
输入数据
材料 体积(kg) 价值($) A(实验设备) 2 10 B(实验文献) 3 20 C(实验样本) 4 30 行李箱容量:
N = 5
我们使用
dp[j]
数组,dp[j]
表示容量为j
的行李箱所能装下的最大价值。初始状态:
dp[0] dp[1] dp[2] dp[3] dp[4] dp[5] [ 0 0 0 0 0 0 ]
正序遍历的错误
正序遍历的代码(有问题):
for (int i = 0; i < M; i++) { // 遍历每种材料 for (int j = weight[i]; j <= N; j++) { // ⚠️ 正序遍历 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } }
我们按照正序
j
计算,看看发生了什么问题。
Step 1:处理材料 A(体积 2,价值 10)
j = 2
,更新dp[2] = max(dp[2], dp[0] + 10) = max(0, 10) = 10
j = 3
,更新dp[3] = max(dp[3], dp[1] + 10) = max(0, 10) = 10
-
dp
数组: j = 4
,更新dp[4] = max(dp[4], dp[2] + 10) = max(0, 10 + 10) = 20
❌ 错误j = 5
,更新dp[5] = max(dp[5], dp[3] + 10) = max(0, 10 + 10) = 20
❌ 错误- 原因:在同一轮中重复使用了dp[2],dp[3],导致材料A不止被选择一次,因为第一遍计算dp[2]表示该物体已经被选择一次,第2次计算dp[4]使用已经更新过的dp[2]计算,表示又被选择了一次。max的过程实质就是一个斟酌选不选材料A的过程
416. 分割等和子集
本题是 01背包的应用类题目
视频讲解:动态规划之背包问题,这个包能装满吗?| LeetCode:416.分割等和子集_哔哩哔哩_bilibili
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
如何转化为背包问题
-
计算数组的总和:首先,计算数组的总和
sum
。如果sum
是奇数,那么无法将数组分割成两个和相等的子集,因为如果能够分割,两个子集的和必须相等,而每个子集的和必然是sum/2
。所以如果sum
为奇数,直接返回false
。 -
目标和为
sum / 2
:假设sum
为偶数,那么我们需要检查是否能够从数组中找到一个子集,其和为sum / 2
。如果存在这样的子集,那么剩余的元素和必然也为sum / 2
,从而实现分割。 -
动态规划的背包思路:我们使用动态规划来判断是否能够找到一个子集,其和为
sum / 2
。将问题转化为一个背包问题,背包的容量是sum / 2
,每个物品的重量就是数组中的一个元素。我们需要判断是否能填充背包,使得背包的总重量正好为sum / 2
。 -
动态规划解决方案
-
dp数组定义:定义一个布尔类型的数组
dp
,其中dp[i]
表示是否可以从数组中选出若干元素,得到和为i
的子集。 -
初始化:初始化
dp[0] = true
,因为和为0
总是可以的,即不选择任何元素。 -
状态转移:遍历数组中的每个元素,更新
dp
数组。对于每个元素num
,从背包容量target = sum / 2
开始,逆序遍历dp
数组,检查是否可以通过选取该元素来更新dp
数组的状态。即,如果dp[j - num]
为true
,则dp[j]
也应该为true
。 -
结果:如果
dp[target]
为true
,表示存在一个和为target
的子集,返回true
;否则,返回false
。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum=0;
for(int nums:nums)
{
sum+=nums;
}
if(sum%2)return false;
int target=sum/2;
vector<bool>dp(target+1,false);//明确dp[n]的含义,表示数组中是否能找到和为n的子集
dp[0]=true;//初始化,表示和为0可以通过不选任何元素得到,是成立的
for(int nums:nums)//表示遍历背包中的每一个物品,判断是否添加
{
for(int j=target;j>=nums;j--)//逆序遍历,不能一步到位求dp[target],需要数组每个数的积累
{
dp[j]=dp[j]||dp[j-nums];//表示两种可能性,保持原状不添加数字,或者添加该遍历到的数字
}
}
return dp[target];
}
};
思考:其实dp[n]数组的含义是非常直观的,可以直接从题目中获取,请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。这句话就表明返回类型为布尔类型,true or false