动态规划-背包问题
背包问题
相关资料
整体介绍与讲解可见如下链接
https://oi-wiki.org/dp/knapsack/
背包问题不难,只要你理解了01背包的思想,掌握了动态规划的思想及用法及优化方法,合理构造状态和状态转移方程,那么问题就有解了!
0/1背包
总空间为V的背包,一共有N个物品,每个物品都有自己的价值w和占用空间t,问你用这样的背包装物品所能得到的最大价值是多少?
由于每个物体只有两种可能的状态(取与不取),对应二进制中的 0 和 1,这类问题便被称为「0-1 背包问题」
解法:
定义二维
那么,遍历所有物品 i ~[1,N],遍历背包空间 j ~[0,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]); } }
为了防止二维空间占用过大,可以利用滚动数组来实现空间优化
在滚动数组时注意,第二维应当从后往前遍历!不然会出现同一件商品重复放入的问题
则最终的答案为
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背包的过程中维护相应的信息即可
注意: 在记录路径时就不可以使用滚动数组的方法了,这样会丢失信息
在二维数组
在跑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 背包的区别在于每种物品有
如果说朴素的将视为01背包,时间复杂度较高
下面说说优化:
依旧是01背包的思想,但是如果说所有的物品就单独计算,时间复杂度过高。这里采用了二进制拆分的原理。就是说,我们对于每一个物品的个数
在二进制拆分时,我们需要从小到大拆,最后一个数小于等于最大倍数的余数,这样保证所有的数之和不会超过
所以,最终的解法就是:利用二进制拆分将多重背包问题转化为01背包问题
还有一种更优的解法,利用单调队列优化,实现
待学习[ ]
例题
模板题
普通多重背包 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数组存将主件放入后的临时值,再对主件 的所有附件做一次 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
本文来自博客园,作者:Qiansui,转载请注明原文链接:https://www.cnblogs.com/Qiansui/p/17542527.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
2022-07-10 这里是浅碎呀!!!