动态规划-背包问题

背包问题

相关资料

整体介绍与讲解可见如下链接
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[i-1][j] $保持不变
  • 反之,判断当前物品装入能否取到更大的价值,则$dp[i][j]=max(dp[i-1][j],dp[i-1][j-t[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 背包的区别在于每种物品有 \(m_i\) 个,而非一个
如果说朴素的将视为01背包,时间复杂度较高
下面说说优化
依旧是01背包的思想,但是如果说所有的物品就单独计算,时间复杂度过高。这里采用了二进制拆分的原理。就是说,我们对于每一个物品的个数\(m_i\),考虑其二进制表示,可以知道的是任何一个十进制数都可以通过2的倍数等相加得到,那么我们可以按照二进制将每一类物品分组,而组数只有\(\log m_i\)个,相较于 m 更加的快捷有效。而且,0~\(m_i\)中的任意数也可以通过01背包取或不取的思想实现。(解释的不好,可以再看看其他人的讲解)
在二进制拆分时,我们需要从小到大拆,最后一个数小于等于最大倍数的余数,这样保证所有的数之和不会超过\(m_i\)。时间复杂度为\(O(V\sum \log_2{m_i})\)
所以,最终的解法就是:利用二进制拆分将多重背包问题转化为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 2023-07-10 22:20  Qiansui  阅读(75)  评论(0编辑  收藏  举报