随笔 - 164  文章 - 0  评论 - 4  阅读 - 9757

动态规划-背包问题

背包问题

相关资料

整体介绍与讲解可见如下链接
https://oi-wiki.org/dp/knapsack/
背包问题不难,只要你理解了01背包的思想,掌握了动态规划的思想及用法及优化方法,合理构造状态和状态转移方程,那么问题就有解了!

洛谷背包问题集合


0/1背包

总空间为V的背包,一共有N个物品,每个物品都有自己的价值w和占用空间t,问你用这样的背包装物品所能得到的最大价值是多少?

由于每个物体只有两种可能的状态(取与不取),对应二进制中的 0 和 1,这类问题便被称为「0-1 背包问题」

解法:
定义二维DP[i][j]表示将前 i 个物品装入容量为 j 的背包中获得的最大值
那么,遍历所有物品 i ~[1,N],遍历背包空间 j ~[0,V]

  • 如果说当前物品的t[i]>j的话,当前物品无法装入,则dp[i][j]=dp[i1][j]保持不变
  • 反之,判断当前物品装入能否取到更大的价值,则dp[i][j]=max(dp[i1][j],dp[i1][jt[i]]+w[i])

那么,最大的总价值即为dp[N][V]
代码如下:

vector<vector<int>> dp(n+5,vector<int>(v+5,0));
for(int i=1;i<=n;++i){
for(int j=0;j<=v;++j){
if(j<t[i]) dp[i][j]=dp[i-1][j];
else dp[i][j]=max(dp[i-1][j],dp[i-1][j-t[i]]+w[i]);
}
}

为了防止二维空间占用过大,可以利用滚动数组来实现空间优化
在滚动数组时注意,第二维应当从后往前遍历!不然会出现同一件商品重复放入的问题
则最终的答案为dp[V]

vector<int> dp(v+5,0);
for(int i=1;i<=n;++i){
for(int j=v;j>=t[i];--j){// 从后往前遍历!!!
dp[j]=max(dp[j],dp[j-t[i]]+w[i]);
}
}
  • 背包问题存储路径

在某些情况下我们想要知道某个背包容量的最优物品选择,只需要在跑01背包的过程中维护相应的信息即可

注意: 在记录路径时就不可以使用滚动数组的方法了,这样会丢失信息
在二维数组 dp[i][j]的基础上再开一个二维数组 pre[i][j],代表前 i 个物品容量为 j 时价值最大为 dp[i][j],最后一个选择的物品是 pre[i][j]

在跑01背包的过程中维护 pre数组 即可

for(int i = 1; i <= n; ++ i){
for(int j = 0; j <= v; ++ j){
if(j >= a[i] && dp[i - 1][j - a[i]] + w[i] > dp[i - 1][j]){
dp[i][j] = dp[i - 1][j - a[i]] + w[i];
pre[i][j] = i;
}else{
dp[i][j] = dp[i - 1][j];
pre[i][j] = pre[i - 1][j];
}
}
}

找路径只需要遍历一遍 pre 即可

int l = n, r = m;
vector<int> t;
while(pre[l][r]){
int last = pre[l][r];
t.push_back(last);
l = last - 1;
r -= w[last];
}

相关资料

https://www.cnblogs.com/dx123/p/17301748.html

例题

模板题

洛谷 P1048 [NOIP2005 普及组] 采药
hdu 2602 Bone Collector
acwing 2. 01背包问题

//>>>Qiansui
#include<bits/stdc++.h>
using namespace std;
const int maxm=1e3+5;
int n,v,t[maxm],w[maxm],dp[maxm];
void solve(){
cin>>n>>v;
for(int i=1;i<=n;++i){
cin>>t[i]>>w[i];
}
for(int i=1;i<=n;++i){
for(int j=v;j>=t[i];--j){
dp[j]=max(dp[j],dp[j-t[i]]+w[i]);
}
}
cout<<dp[v]<<'\n';
return ;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _=1;
// cin>>_;
while(_--){
solve();
}
return 0;
}

其余题

  • 存在一个物品完全选择和部分选择的01背包 2022 ICPC 杭州站 C. No Bug No Game

  • 需一点思维,转成01背包问题 洛谷 P2392 kkksc03考前临时抱佛脚
    一门一门复习的总时间和是固定的,那么利用两个脑子的理论最优时间就是总时间的一半,故利用总时间的一半去跑 01 背包找可以装的最大时间,那么最大时间和剩下时间中大的那个就是实际最优时间
    Qiansui_code

  • 需一点思维,转成01背包问题 cf 894 div.3 F. Magic Will Save the World
    与上题类似,是上一题的进阶版
    对于所有怪物的能力和 sum 是不变的,而我们只需要考虑当用 水咒 尽可能消灭怪物时再用 火咒 能否成功消灭怪物,为此,因为题目的数据范围较小,我们可以对 水咒 从 0 开始枚举到能力和 sum ,利用01背包杀可能多的怪,再判断剩下的怪需要用多少倍的火咒才能消灭,两者取大的最小值即为答案
    Qiansui_code

  • 需一点思维,转成01背包问题 leetcode 6922. 将一个数字表示成幂的和的方案数

  • 多个01背包从前往后牵制 2023牛客多校第五场 H Nazrin the Greeeeeedy Mouse
    (2023.7.31 感觉有点小难,题目的关键在于考虑背包之间的牵制关系怎么利用DP解决)


完全背包问题

01背包变式
基本内容与01背包相同,完全背包问题多了一个条件,就是每件物品有无限个,依旧是V的背包,问你最大的价值?

一种方法是依然将其视为01背包问题,然后暴力枚举每个物品的数量来转移,但这样的方法显然不是我们想要的

那么整体的解法与01背包相同,就是这里的j循环需要从前往后遍历,因为每个物品可以取多次,从前往后枚举当前物品,可以将当前物品重复放入背包中,求取局部最优解即可。而这种情况刚好是01背包的反方向,两者注意区别。

例题

模板题

洛谷 P1616 疯狂的采药
acwing 3. 完全背包问题

//>>>Qiansui
#include<bits/stdc++.h>
using namespace std;
const int maxm=1e3+5;
int n,v,t[maxm],w[maxm],dp[maxm];
void solve(){
cin>>n>>v;
for(int i=1;i<=n;++i){
cin>>t[i]>>w[i];
}
for(int i=1;i<=n;++i){
for(int j=t[i];j<=v;++j){
dp[j]=max(dp[j],dp[j-t[i]]+w[i]);
}
}
cout<<dp[v]<<'\n';
return ;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _=1;
// cin>>_;
while(_--){
solve();
}
return 0;
}

多重背包问题

01背包变式
与 0-1 背包的区别在于每种物品有 mi 个,而非一个
如果说朴素的将视为01背包,时间复杂度较高
下面说说优化
依旧是01背包的思想,但是如果说所有的物品就单独计算,时间复杂度过高。这里采用了二进制拆分的原理。就是说,我们对于每一个物品的个数mi,考虑其二进制表示,可以知道的是任何一个十进制数都可以通过2的倍数等相加得到,那么我们可以按照二进制将每一类物品分组,而组数只有logmi个,相较于 m 更加的快捷有效。而且,0~mi中的任意数也可以通过01背包取或不取的思想实现。(解释的不好,可以再看看其他人的讲解)
在二进制拆分时,我们需要从小到大拆,最后一个数小于等于最大倍数的余数,这样保证所有的数之和不会超过mi。时间复杂度为O(Vlog2mi)
所以,最终的解法就是:利用二进制拆分将多重背包问题转化为01背包问题

还有一种更优的解法,利用单调队列优化,实现O(NV)的时间复杂度
待学习[ ]

例题

模板题

普通多重背包 https://www.acwing.com/problem/content/4/
需二进制拆分优化 https://www.acwing.com/problem/content/5/
需单调队列优化 https://www.acwing.com/problem/content/6/
洛谷 P1776 宝物筛选

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
using namespace std;
/*
利用二进制拆分实现log级别的优化
*/
const int maxm=1e5+5;
int n,t,v,w,m;
int nn,nv[maxm],nw[maxm];
void solve(){
cin>>n>>t;
//利用二进制拆分将多重背包转化成01背包
nn=0;
for(int i=1;i<=n;++i){
cin>>v>>w>>m;
for(int j=1;j<=m;j<<=1){
m-=j;
nv[++nn]=v*j;
nw[nn]=w*j;
}
if(m){
nv[++nn]=v*m;
nw[nn]=w*m;
}
}
//下为01背包
vector<int> dp(t+5,0);
for(int i=1;i<=nn;++i){
for(int j=t;j>=nw[i];--j){
dp[j]=max(dp[j],dp[j-nw[i]]+nv[i]);
}
}
cout<<dp[t]<<'\n';
return ;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _=1;
// cin>>_;
while(_--){
solve();
}
return 0;
}

混合背包

上面三种背包的混合形式
混合背包就是将前面三种的背包问题混合起来,有的只能取一次,有的能取无限次,有的只能取 m 次
这种题目看起来很吓人,可是只要领悟了前面几种背包的中心思想,并将其合并在一起就可以了。下面给出伪代码:

for (循环物品种类) {
if (是 0 - 1 背包)
套用 0 - 1 背包代码;
else if (是完全背包)
套用完全背包代码;
else if (是多重背包)
套用多重背包代码;
}

有种九九归一的感觉

例题

模板题

洛谷 P1833 樱花
https://www.acwing.com/problem/content/7/

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
using namespace std;
#define pll pair<ll,ll>
const int maxm=1e3+5;
ll n,t,v,w,s;
vector<ll> dp(maxm,0);
void pack_01(){//01背包从后往前
for(int j=t;j>=v;--j){
dp[j]=max(dp[j],dp[j-v]+w);
}
return ;
}
void pack_complete(){//完全背包从前往后
for(int j=v;j<=t;++j){
dp[j]=max(dp[j],dp[j-v]+w);
}
return ;
}
void pack_multiple(){//多重背包先二进制优化再从后往前01背包
vector<pll> q;
for(int i=1;i<=s;i<<=1){
s-=i;
q.push_back({i*v,i*w});
}
if(s){
q.push_back({s*v,s*w});
}
for(auto a:q){
for(int i=t;i>=a.first;--i){
dp[i]=max(dp[i],dp[i-a.first]+a.second);
}
}
return ;
}
void solve(){
cin>>n>>t;
for(int i=0;i<n;++i){
cin>>v>>w>>s;
if(s==-1){//1次
pack_01();
}else if(s==0){//无限次
pack_complete();
}else{//s次
pack_multiple();
}
}
cout<<dp[t]<<'\n';
return ;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _=1;
// cin>>_;
while(_--){
solve();
}
return 0;
}

二维费用背包

背包的考虑问题多了一个维度,但其实本质依旧是01背包问题

例题

模板题

https://www.acwing.com/problem/content/8/
洛谷 P1855 榨取kkksc03

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
using namespace std;
#define pll pair<ll,ll>
const int maxm=2e2+5;
int n,m,t,v,w,dp[maxm][maxm];
void solve(){
cin>>n>>m>>t;
for(int i=1;i<=n;++i){
cin>>v>>w;
for(int j=m;j>=v;--j){
for(int k=t;k>=w;--k){
dp[j][k]=max(dp[j][k],dp[j-v][k-w]+1);
}
}
}
cout<<dp[m][t]<<'\n';
return ;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _=1;
// cin>>_;
while(_--){
solve();
}
return 0;
}

分组背包

01背包变式,就是所有的物品被分到了不同的组,每组只能选择一个。
其实是从 在所有物品中选择一件 变成了 从当前组中选择一件 ,于是就对每一组进行一次 0-1 背包就可以了

例题

模板题

https://vjudge.net/problem/HDU-1712
https://www.acwing.com/problem/content/9/
洛谷 P1757 通天之分组背包

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long
using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*
*/
const int maxm=1e3+5,maxn=1e2+5,inf=0x3f3f3f3f,mod=998244353;
int n,m;
vector<vector<pll>> q(maxn,vector<pll>());
vector<ll> dp(maxm,0);
void solve(){
cin>>m>>n;
ll v,w,p;
for(int i=0;i<n;++i){
cin>>v>>w>>p;
q[p].push_back({v,w});
}
for(auto a:q){
if(a.size())
for(int i=m;i>=0;--i){
for(auto x:a){
if(i>=x.first)
dp[i]=max(dp[i],dp[i-x.first]+x.second);
}
}
}
cout<<dp[m]<<'\n';
return ;
}
signed main(){
// ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _=1;
// cin>>_;
while(_--){
solve();
}
return 0;
}

有依赖的背包

此类背包物品之间有牵制关系,可以将其视作分组背包
如果是多叉树的集合,则要先算子节点的集合,最后算父节点的集合。

例题

模板题

  • 洛谷 P1064 [NOIP2006 提高组] 金明的预算方案
    考虑依赖背包,即背包物品有主附件,只有选了主件才可以选择附件
    那么我们可以另开一个DP数组存将主件放入后的临时值,再对主件 i 的所有附件做一次 01 背包,得到所有花费时的最大价值,再和原DP数组对应位取大更新,即得新的DP数组
//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << '\n'
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n'
//#define int long long
using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*
*/
const int N = 2e5 + 5, inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f, mod = 998244353;
void solve(){
int n, m;
cin >> n >> m;
vector<int> dp(n + 1, 0), a(n + 1, 0);
vector<pii> h[m + 1];
for(int i = 1; i <= m; ++ i){
int q;
pii t;
cin >> t.first >> t.second >> q;
t.second *= t.first;
if(q == 0) h[i].push_back(t);
else h[q].push_back(t);
}
for(int i = 1; i <= m; ++ i){
if(h[i].size() == 0) continue;
int len = h[i].size(), vv = h[i][0].first;
for(int j = 0; j + vv <= n; ++ j){// 放入主件
a[j + vv] = dp[j] + h[i][0].second;
}
for(int j = 1; j < len; ++ j){// 对所有附件跑 01 背包
for(int k = n; k >= vv + h[i][j].first; -- k){
a[k] = max(a[k], a[k - h[i][j].first] + h[i][j].second);
}
}
for(int j = vv; j <= n; ++ j){// 取大更新 dp 数组
dp[j] = max(dp[j], a[j]);
}
}
cout << dp[n] << '\n';
return ;
}
signed main(){
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
int _ = 1;
// cin >> _;
while(_ --){
solve();
}
return 0;
}

杂项(待补充)

待补充,详见oi wiki

posted on   Qiansui  阅读(95)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2022-07-10 这里是浅碎呀!!!
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示