背包基础篇(01、完全、分组、多重、混合)
问题链接:
2. 01背包问题 - AcWing题库8. 二维费用的背包问题 - AcWing题库
3. 完全背包问题 - AcWing题库
9. 分组背包问题 - AcWing题库
4. 多重背包问题 I - AcWing题库
一、01 背包
01 背包:每件物品只能选一次,物品有重量
v[i],价值w[i],背包总容量V,求最大总价值。
1、二维 DP
状态定义:dp[i][j]:前i件物品,背包容量为j时的最大价值。
转移方程:
不选第 i件: d p i j = d p i − 1 j dpij=dpi-1j dpij=dpi−1j
不选第 i件: d p i j = d p i − 1 j − v \[ i ] + w i dpij=dpi-1j-v\[i]+wi dpij=dpi−1j−v\[i]+wi (需要背包容量j 大于等于 v[i],表示可以装下这个物品 )
代码模板
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int v1[N],w1[N]; // v1[]重量,w1[]价值
int dp[N][N]; // dp[i][j]前i个物品,背包容量j的最大价值
int main(){
int n,v;
cin>>n>>v;
// 读入n件物品:重量、价值
for(int i=1;i<=n;i++){
cin>>v1[i]>>w1[i];
}
// 遍历每件物品
for(int i=1;i<=n;i++){
// j倒序也能跑,但二维推荐正序
for(int j=v;j>=0;j--){
dp[i][j]=dp[i-1][j]; // 情况1:不选第i件物品
if(v1[i]<=j) // 背包装得下才可以选
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v1[i]]+w1[i]); // 取选/不选的最大值
}
}
cout<<dp[n][v]; // n件物品,容量v的最大答案
return 0;
}
2、一维 DP
二维压缩成一维,j 必须倒序遍历,防止物品被多次选取,如果是正序遍历的话,容量是从小的开始的,会导致一个物品在放入之后,随着容量的增大,会被再次放入,会变成完全背包问题。
状态定义:
dp[j]:容量j背包的最大价值。
转移方程: d p j = m a x ( d p j , d p j − v \[ i ] + w i ) dpj=max(dpj,dpj-v\[i]+wi) dpj=max(dpj,dpj−v\[i]+wi)
关键点:
j从总容量V到v[i],倒序!
代码模板
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,v1;
cin>>n>>v1;
vector<int>v(n+1),w(n+1);
for(int i=0;i<n;i++){
cin>>v[i]>>w[i];
}
vector<int>dp(v1+1);
for(int i=0;i<n;i++){
for(int j=v1;j>=v[i];j--){
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout<<dp[v1];
return 0;
}
01 背包一维必须倒序 j;完全背包是正序 j。
3、不考虑价值,恰好装满的方案数
- 内层循环:
j 从 V 倒序到 v[i] - 转移:
dp[j] = (dp[j] + dp[j‑v[i]]) % MOD - ①恰好装满 V:答案
dp[V] - ②体积不超过 V:答案
sum(dp[0]~dp[V]) % MOD
代码模板
#include<bits/stdc++.h>
using namespace std;
int dp[1003];
const int N=1e9+7;
int v1[1003],w1[1003];
int main(){
int n,v;
cin>>n>>v;
dp[0]=1;
for(int i=0;i<n;i++){
cin>>v1[i]>>w1[i];
}
for(int i=0;i<n;i++){
for(int j=v;j>=v1[i];j--){
dp[j]=(dp[j]+dp[j-v1[i]])%N;
}
}
cout<<dp[v];
return 0;
}
4、体积不超过 V,求能拿到最大价值的方案数
题意:先选出总价值最大的选法,统计一共有多少种这样的选法。 背包容量 V,物品体积不超过 V,不一定非要把背包装满。 需要两个数组:
dp[j]:容量 j 的背包可以获得的最大价值cnt[j]:容量 j,达到最大价值f[j]的方案数量
初始化:cnt其他都是0,只有 cnt[0]=1,因为背包容量为0,是只有一种方案数,就是什么都不装。
代码模板
#include<bits/stdc++.h>
using namespace std;
int const N=1e9+7;
int dp[1003];
int cnt[1003];
int v1[1003],w1[1003];
int main(){
int n,v;
cin>>n>>v;
for(int i=0;i<n;i++){
cin>>v1[i]>>w1[i];
}
for(int i=0;i<=v;i++){
cnt[i]=1;
}
for(int i=0;i<n;i++){
for(int j=v;j>=v1[i];j--){
if(dp[j]<dp[j-v1[i]]+w1[i]){
dp[j]=dp[j-v1[i]]+w1[i];
cnt[j]=cnt[j-v1[i]]%N;
}
else if(dp[j]==dp[j-v1[i]]+w1[i]){
cnt[j]=(cnt[j]+cnt[j-v1[i]])%N;
}
}
}
cout<<cnt[v];
return 0;
}
5、背包问题求具体方案
二维 dp 从后往前递推,然后正向循环回溯输出选择物品编号,用来输出 01 背包选的物品。
数组含义
dp[i][j]:只考虑第 i ~ n 号物品 ,背包容量为j时,可以得到的最大价值。
注意:
i从n往下算,代表:从第i件到最后一件物品。
v1[i],w1[i]:第i件物品体积、价值
📝DP 递推部分(i 从 n 到 1)
for(int i=n;i>=1;i--){
for(int j=v;j>=0;j--){
dp[i][j]=dp[i+1][j]; // 情况1:不选第i件物品
if(j>=v1[i])
dp[i][j]=max(dp[i+1][j], dp[i+1][j-v1[i]]+w1[i]);
}
}
dp[i][j] = dp[i+1][j]:不选i号物品 ,那么就依赖后面i+1~n物品的结果。- 如果背包装得下
i物品(j>=v1[i]): 对比两种选择:- 不选 i:
dp[i+1][j] - 选 i:
dp[i+1][j‑v1[i]] + w1[i],选完i之后剩下容量j‑v1[i],再看后面i+1~n物品
- 不选 i:
- 取两者 max 赋值给
dp[i][j]
边界:
dp[n+1][j],没有物品可以选,全部是 0,全局数组自动初始 0。
和普通二维背包对比: 普通二维:dp[i][j]代表前 i 件物品(1~i) ,i 从 1 到 n。 这份代码:dp[i][j]代表i~n 后缀物品,i 从 n 倒推到 1。
✂回溯部分:找出到底选了哪些物品
int vv=v;
for(int i=1;i<=n;i++){
if(vv<=0) break;
if(vv>=v1[i] && dp[i][vv]==dp[i+1][vv-v1[i]]+w1[i]){
cout<<i<<" ";
vv-=v1[i];
}
}
逻辑 :从第 1 件物品开始依次判断,这件物品有没有被选上。 条件:
dp[i][vv] == dp[i+1][vv‑v1[i]] + w1[i]
含义 :当前最优价值 =【选 i 物品后的价值】,说明最优解里面选了 i 号物品。
- 如果成立:输出物品编号
i,背包剩余容量减去该物品体积vv -= v1[i] - 如果不成立:说明最优解没有选
i,直接看下一个i+1。
代码模板
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int v1[N],w1[N];
int dp[N][N];
int main(){
int n,v;
cin>>n>>v;
for(int i=1;i<=n;i++){
cin>>v1[i]>>w1[i];
}
for(int i=n;i>=1;i--){
for(int j=v;j>=0;j--){
dp[i][j]=dp[i+1][j];
if(j>=v1[i])
dp[i][j]=max(dp[i+1][j],dp[i+1][j-v1[i]]+w1[i]);
}
}
int vv=v;
for(int i=1;i<=n;i++){
if(vv<=0)
break;
if(vv>=v1[i]&&dp[i][vv]==dp[i+1][vv-v1[i]]+w1[i]){
cout<<i<<" ";
vv-=v1[i];
}
}
return 0;
}
6、二维费用的背包问题
每件物品有两种消耗(双重约束),本题:
- 消耗 1:体积 (v1i),背包体积上限 v
- 消耗 2:重量 (m1i),背包重量上限 m
- 物品价值 (w1i),每件物品最多选 1 次 求在体积≤v 并且 重量≤m条件下的最大价值。
普通 01 背包只有 1 个约束(体积);二维费用背包增加第二个约束条件,dp 数组多一维。
dp 状态定义
dp[j][k]:总体积用了j,总重量用了k,可以获得的最大价值。
状态转移
dp[j][k]=max(dp[j][k],dp[j‑v1[i]][k‑m1[i]]+w1[i]);
- 不选该物品:保留原来
dp[j][k] - 选该物品:体积减去(v1i),重量减去(m1i),再加上当前物品价值。
循环关键点
for(int i=0;i<n;i++)
for(int j=v;j>=v1[i];j--) //体积维度:倒序
for(int k=m;k>=m1[i];k--) //重量维度:也要倒序
✅两个费用维度都必须倒序遍历
- 一维 01 背包:只需要容量倒序,避免重复选同一个物品。
- 二维费用 01 背包:两维全部倒序。 只要有一维写成正序,就会变成完全背包逻辑,同一个物品可以多次选取,直接 WA。
j 循环和 k 循环可以互换先后顺序,结果不变。
初始化两种模式
- 不要求装满(本题代码) 全局 dp 数组默认 0;允许体积、重量不用耗尽,求最大价值。输出
dp[v][m]。 - 要求恰好装满 dp 全部初始为负无穷,
dp[0][0]=0。代表体积 0 重量 0 时价值为 0,其余状态不可达。
代码模板
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int dp[N][N];
int v1[N],m1[N],w1[N];
int main(){
int n,v,m;
cin>>n>>v>>m;
for(int i=0;i<n;i++){
cin>>v1[i]>>m1[i]>>w1[i];
}
for(int i=0;i<n;i++){
for(int j=v;j>=v1[i];j--){
for(int k=m;k>=m1[i];k--){
dp[j][k]=max(dp[j][k],dp[j-v1[i]][k-m1[i]]+w1[i]);
}
}
}
cout<<dp[v][m];
return 0;
}
二、完全背包
特点:物品可以无限选取,每件物品能选多次
状态定义:
dpj 表示背包容量为 j 时能获得的最大价值
1、一维DP
转移方程: d p j = m a x ( d p j , d p j − v \[ i ] + w i ) dpj=max(dpj,dpj-v\[i]+wi) dpj=max(dpj,dpj−v\[i]+wi)
代码模板
#include<bits/stdc++.h>
using namespace std;
int dp[1009];
int v1[1009],w1[1009];
int main(){
int n,v;
cin>>n>>v;
for(int i=0;i<n;i++){
cin>>v1[i]>>w1[i];
}
for(int i=0;i<n;i++){
for(int j=v1[i];j<=v;j++){
dp[j]=max(dp[j],dp[j-v1[i]]+w1[i]);
}
}
cout<<dp[v];
return 0;
}
- 01 背包:j 逆序
j=m; j>=v[i](防止重复选)- 完全背包:j 正序
j=v[i]; j<=m(允许重复选取)
2、不考虑价值,恰好装满的方案数
- 内层循环:
j 从 v[i]正序到V - 转移:
dp[j] = (dp[j] + dp[j‑v[i]]) % MOD - ①恰好装满 V:答案
dp[V] - ②体积不超过 V:答案
sum(dp[0]~dp[V])%MOD
代码模板
#include<bits/stdc++.h>
using namespace std;
int dp[1003];
int v1[1003],w1[1003];
int main(){
int n,v;
cin>>n>>v;
dp[0]=1;
for(int i=0;i<n;i++){
cin>>v1[i]>>w1[i];
}
for(int i=0;i<n;i++){
for(int j=v1[i];j<=v;j++){
dp[j]+=dp[j-v1[i]];
}
}
cout<<dp[v];
return 0;
}
3、体积不超过 V,求能拿到最大价值的方案数
dp[j]:背包容量j,可以得到的最大价值,全局数组默认初始为 0cnt[j]:背包容量j,取得dp[j]最大价值的方案数量
初始化 :cnt[0]=1;
cnt[0]=1:背包体积为 0,什么物品都不选,价值 0,算作 1 种合法方案。cnt[1...v]全局默认 0,不需要赋值。
循环
for(int i=0;i<n;i++)
for(int j=v1[i];j<=v;j++)
- j 从小到大正序:完全背包,允许同一物品重复选取。
- 如果改成 01 背包,内层改为
for(int j=v;j>=v1[i];j--)倒序,转移代码完全不变。
状态转移
int choose = dp[j-v1[i]] + w1[i]; //选当前物品的总价值
if(dp[j] < choose){
//选物品价值更大,更新价值;方案继承j‑v1[i]的方案
dp[j]=choose;
cnt[j]=cnt[j-v1[i]]%N;
}else if(dp[j]==choose){
//选、不选价值相等,两类方案相加
cnt[j]=(cnt[j]+cnt[j-v1[i]])%N;
}
//choose < dp[j]:不选该物品,dp、cnt保持原样
代码模板
#include<bits/stdc++.h>
using namespace std;
int const N=1e9+7;
int dp[1003];
int cnt[1003];
int v1[1003],w1[1003];
int main(){
int n,v;
cin>>n>>v;
for(int i=0;i<n;i++){
cin>>v1[i]>>w1[i];
}
cnt[0]=1;
for(int i=0;i<n;i++){
for(int j=v1[i];j<=v;j++){
if(dp[j]<dp[j-v1[i]]+w1[i]){
dp[j]=dp[j-v1[i]]+w1[i];
cnt[j]=cnt[j-v1[i]]%N;
}
else if(dp[j]==dp[j-v1[i]]+w1[i]){
cnt[j]=(cnt[j]+cnt[j-v1[i]])%N;
}
}
}
cout<<cnt[v];
return 0;
}
三、分组背包
一维DP
物品分成 n 组,每组最多只能选 1 件物品,组内物品互斥;求背包容量 v 下最大总价值。
数组含义
s[i]:第i组有多少件物品v1[i][k],w1[i][k]:第i组,组内第k件物品的体积、价值dp[j]:背包容量j,可以获得的最大价值
三层循环顺序(分组背包固定顺序)
for(组 i : 0~n‑1) //第一层:遍历组别
for(j = v downto 0) //第二层:容量倒序!不能正序
for(组内物品k) //第三层:遍历本组所有物品
循环顺序绝对不能乱。
每一层作用:
第一层 i:枚举每一组 一组一组处理,每组最多挑一件。
第二层 j:j 从 v 倒序到 0 * 和 01 背包一样倒序,防止同一组选多个物品。
如果写成正序,就会变成完全背包效果,同一组可以重复拿多件,直接 WA。
第三层 k:遍历本组里面每一个物品 尝试:本组选第 k 个物品,更新 dp j。 转移式子:
dp[j]=max(dp[j], dp[j‑v1[i][k]] + w1[i][k]);
dp[j‑v1[i][k]]是处理前面 i‑1 组得到的旧状态,保证本组最多只选一件。
代码模板
#include<bits/stdc++.h>
using namespace std;
int s[108];
int v1[108][108];
int w1[108][108];
int dp[108];
int main(){
int n,v;
cin>>n>>v;
for(int i=0;i<n;i++){
cin>>s[i];
for(int j=0;j<s[i];j++){
cin>>v1[i][j]>>w1[i][j];
}
}
for(int i=0;i<n;i++){
for(int j=v;j>=0;j--){
for(int k=0;k<s[i];k++){
if(j>=v1[i][k]){
dp[j]=max(dp[j],dp[j-v1[i][k]]+w1[i][k]);
}
}
}
}
cout<<dp[v];
return 0;
}
四、多重背包I、II
朴素暴力版
dp[j]:背包容量为j时,能拿到的最大价值。全局数组默认初始为 0。
三层循环拆解
for(int i=0;i<n;i++) //第一层:枚举每一类物品
{
int s,v1,w1;
cin>>v1>>w1>>s;
for(int j=v;j>=v1;j--) //第二层:容量j倒序(模仿01背包)
{
//k:枚举该物品选k件,k∈[1,s]
for(int k=1;k<=s&&k*v1<=j;k++)
{
dp[j]=max(dp[j], dp[j‑k*v1] + k*w1);
}
}
}
每一层作用
- 第一层 i:遍历每一类物品 每一类最多拿
s个。 - 第二层 j:j 从 v 往下倒序 和 01 背包一样倒序,保证使用上一轮 i‑1 的旧 dp 状态,不会出现物品无限选。
- 第三层 k:暴力枚举选多少件
k代表当前物品选k件。 条件:
k<=s:不能超过该物品最多数量k*v1 <= j:选 k 件的总体积不能超过当前背包容量 j
状态转移方程
dp[j] = max(dp[j], dp[j - k*v1] + k*w1);
含义:选
k件该物品,占用体积k*v1,获得价值k*w1;剩下容量j‑k*v1用前面物品的最优解。
#include<bits/stdc++.h>
using namespace std;
int dp[109];
int main(){
int n,v;
cin>>n>>v;
for(int i=0;i<n;i++){
int s,v1,w1;
cin>>v1>>w1>>s;
for(int j=v;j>=v1;j--){
for(int k=1;k<=s&&k*v1<=j;k++){
dp[j]=max(dp[j],dp[j-k*v1]+k*w1);
}
}
}
cout<<dp[v];
return 0;
}
二进制优化版
dp[j]:容量j背包的最大价值,全局数组默认初始 0。vv、vw:存放二进制拆分之后生成的虚拟物品的体积、价值。
把原来一类最多
s件的物品,拆成若干个新物品,每个新物品代表一次性拿k件原物品。
二进制拆分核心原理
把数量s拆成若干个 2 的幂:(1,2,4,8...),再加上剩下余数。 0~s 之间任意数目,都可以用若干个拆分出的数字相加凑出来。
举例:
s=13
- k=1:取出 1,s=12,k=2
- k=2:取出 2,s=10,k=4
- k=4:取出 4,s=6,k=8
- k=8 > 6,退出循环,把剩余 s=6 加入 拆分结果:
1,2,4,6。1‑13任意数字都可以组合得到。
每一组k件,合并成一件虚拟物品:
- 虚拟体积 = k ∗ v 1 k*v1 k∗v1
- 虚拟价值 = k ∗ w 1 k*w1 k∗w1
第一段:读入物品 + 二进制拆分
for(int i=0;i<n;i++){
int v1,w1,s;
cin>>v1>>w1>>s;
int k=1;
while(k<=s){
vv.push_back(v1*k);
vw.push_back(k*w1);
s=s-k;
k=k*2;
}
if(s>0){
vv.push_back(v1*s);
vw.push_back(s*w1);
}
}
注意:这里直接修改局部变量
s,不影响下一轮读入。
第二段:对所有虚拟物品跑 01 背包
for(int i=0;i<vv.size();i++){
for(int j=v;j>=vv[i];j--){
dp[j]=max(dp[j],dp[j-vv[i]]+vw[i]);
}
}
拆分完成后,每一个虚拟物品只能选一次 ,所以内层循环j倒序,标准 01 背包写法。
状态转移:
d p j = m a x ( d p j , d p j − v v \[ i ] + v w i ) dpj=max(dpj,dpj-vv\[i]+vwi) dpj=max(dpj,dpj−vv\[i]+vwi)
#include<bits/stdc++.h>
using namespace std;
int dp[2003];
vector<int>vv; //拆分后虚拟物品的体积
vector<int>vw; //拆分后虚拟物品的价值
int main(){
int n,v;
cin>>n>>v;
for(int i=0;i<n;i++){
int v1,w1,s;
cin>>v1>>w1>>s;
int k=1;
//二进制拆分:1,2,4,8......
while(k<=s){
vv.push_back(v1*k);
vw.push_back(k*w1);
s=s-k;
k=k*2;
}
//剩余不足2的幂次,单独作为一组
if(s>0){
vv.push_back(v1*s);
vw.push_back(s*w1);
}
}
//全部虚拟物品当作01背包处理,j倒序
for(int i=0;i<vv.size();i++){
for(int j=v;j>=vv[i];j--){
dp[j]=max(dp[j],dp[j-vv[i]]+vw[i]);
}
}
cout<<dp[v];
return 0;
}
五、混合背包
一维DP
混合背包问题:同一个题目里面同时存在三种物品
s[i] = -1:01 背包物品,最多选 1 次s[i] = 0:完全背包物品,可以无限选s[i] > 0:多重背包物品 ,最多选s[i]件,使用二进制拆分优化转成 01 背包
vv,ww:vector,存储二进制拆分之后生成的 "新虚拟物品" 的体积和价值
分模块知识点
-
s [i]==-101 背包for(int j=v;j>=v1[i];j--){
dp[j]=max(dp[j],dp[j-v1[i]]+w1[i]);
}
内层循环容量倒序,保证每件物品只能选一次。
-
s [i]==0完全背包for(int j=v1[i];j<=v;j++){
dp[j]=max(dp[j],dp[j-v1[i]]+w1[i]);
}
内层循环容量正序,允许物品多次选取。
s [i]>0多重背包(二进制拆分优化)
核心思想:把
s件物品,拆成若干组(1,2,4,8...剩余数),每组当成一件新的 01 物品。 任意0~s之间的数量,都可以用这些组组合凑出来。
vv.clear();
ww.clear();
int k=1;
while(k<=s[i]){
vv.push_back(k*v1[i]); //k件合并为1个虚拟物品
ww.push_back(k*w1[i]);
s[i]=s[i]-k;
k=2*k;
}
if(s[i]>0){ //剩下不足2的幂次的部分单独作为一组
vv.push_back(s[i]*v1[i]);
ww.push_back(s[i]*w1[i]);
}
//拆分完成,全部当做01背包,倒序遍历
for(int jj=0;jj<vv.size();jj++){
for(int kk=v;kk>=vv[jj];kk--){
dp[kk]=max(dp[kk],dp[kk-vv[jj]]+ww[jj]);
}
}
举例:s=13 拆分:1,2,4,6;1+2+4+6=13。 1‑13 任意数字都可以由其中若干项相加得到。
整体逻辑(混合背包)
遍历每一件物品,根据s[i]的标记,走不同背包逻辑:
- 01 → j 倒序
- 完全 → j 正序
- 多重 → 二进制拆分,转为多件 01 物品,j 倒序
关键点:dp 数组是共用的,三种物品依次处理,状态不断滚动更新。