一、01背包
传送门
二、完全背包
传送门
三、多重背包
1、多重背包问题描述:
给N件物品和一个容量为C的包,每件的价值和重量分别为v[i]和w[i]且每件物品的数量为num[i] ,问怎么拿可以使得包中物品的价值最大
其实这个问题和之前的01背包和完全背包比较类似,所以如果没有上面两个基础的话,可以先去看下01背包和完全背包。
2、重复背包dp状态和过程
未经优化的最朴素的dp过程
我们只需要仿照完全背包的过程就可以,只不过第三层循环
是每件物品的数量所以我们可以得到状态转移方程
dp[j] = max(dp[j],dp[j - k * w[i]] + k * v[i]) k>= 1&&k=min(num[i],c / w[i])
由此我们可以得到伪代码
for i: 1 - n//物品数量
for j: j - w[i]//枚举背包容量
for k: 1 - num[i]
dp[j] = max(dp[j],dp[j - k * w[i]] + k * v[i]
我们会发现这种暴力跑法,复杂度为O(n c ∑ num[i]) 一旦∑ num[i]的数量级变大一定是会超时的,所以又出现了利用二进制的优化方式
二进制优化
上面的暴力跑法在数据量过大时会超时,所以我们要想办法去降低物品的总数量和我们可以利用二进制进行优化,比如当num[i] = 1时,我们可以将其拆分为 1,21,22 ,13 - 23 + 1这样13就被拆分成了1 2 4 6分别乘以w[i],v[i],的4件物品,而且这4件物品也能够组成1-13中的任意一个,所以我们就可以将13件物品组合为4件进行遍历
综上就是,我们可以将一个num[i] = n 的物品拆分为 21,22…2^k - 1^,n - 2k + 1
k为使n - 2k + 1 > 0的最大的一个值,具体证明这里略 我也不会(gou tou)
这样我们就可以把暴力的时间复杂度降低到O(n c ∑lognum[i])
优化后伪代码为
for i: 1 - n
for j: c - w[i]
k = num[i],q = 1
while(q >= num)
k -= q
q = q * 2
dp[j] = max(dp[j],dp[j - q * w[i]] + q * v[i])
其实网上还可以使用单调队列进行进一步优化,但是二进制就够用了已经
有兴趣可以去看下 不会说我不会
例题 luogu 1776
这个题没啥好说的,就是利用二进制优化的模板题
code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 4e4 + 5;
int dp[maxn];
int v[maxn],w[maxn],num[maxn];
int n,bag;
void ZeroOnePack(int weight,int value)//01背包
{
for(int i = bag; i>= weight; i--){
dp[i] = max(dp[i],dp[i - weight] + value);
}
}
void ComlpetePack(int weight,int value)//完全背包
{
for(int i = weight; i <= bag; i++){
dp[i] = max(dp[i],dp[i - weight] + value);
}
}
void MultiplePack(int weight,int value,int number)//多重背包
{
//如果总容量比背包容量小,那么就相当于完全背包
if(bag <= number * weight){
ComlpetePack(weight,value);
return ;
}
//否则就表示所有的物品都可以拿走即转化为01背包
int k = 1;
//二进制优化
while(k <= number){
ZeroOnePack(k * weight,k * value);
number -= k;
k = k * 2;
}
ZeroOnePack(number * weight,number * value);
}
int main()
{
scanf("%d%d",&n,&bag);
for(int i = 1; i <= n; i++){
scanf("%d%d%d",v + i,w + i,num + i);
}
for(int i = 1; i <= n; i++){
MultiplePack(w[i],v[i],num[i]);//多重背包
}
printf("%d\n",dp[bag]);
return 0;
}
四、分组背包
传送门
五、混合背包
1、问题描述:
N个物品,背包容量为C,每一件物品的重量为w[i],价值为v[i],且有些物品只能取一次,有些能取无数次,有些可以取num[i]次,求背包怎么装能到达最大价值
2、思路:
其实很简单,如果这个物品只能取一次,就当01背包处理,如果能取无数次就当完全背包处理,最后一种情况就当做多重背包处理即可
例题 luogu p1833
code
#include <bits/stdc++.h>
#define ss system("pause");
using namespace std;
const int maxn = 1e4 + 5;
const int maxc = 1e3 + 5;
int dp[maxc],v[maxn],T[maxn];
int h1,m1,h2,m2,n;
void ZeroOnePack(int val,int weight,int C)
{
for(int i = C; i>= weight; i--){
dp[i] = max(dp[i],dp[i - weight] + val);
}
}
void CompletePack(int val,int weight,int C)
{
for(int i = weight; i <= C; i++){
dp[i] = max(dp[i],dp[i - weight] + val);
}
}
void MultiplePack(int val,int weight,int num,int C)
{
int k = 1;
while(k <= num){
num -= k;
ZeroOnePack(k * val,k * weight,C);
k = k * 2;
}
ZeroOnePack(num * val,num * weight,C);
}
int main()
{
scanf("%d:%d %d:%d %d",&h1,&m1,&h2,&m2,&n);
int C = m2 - m1 + 60 + (h2 - h1 - 1) * 60;
cout<<C<<endl;
for(int i = n; i <= n; i++){
int t,v,p;
cin>>t>>v>>p;
if(!p) ZeroOnePack(v,t,C);
else if(t * p >= C) CompletePack(v,t,C);
else MultiplePack(v,t,p,C);
}
cout<<dp[C]<<endl;
//ss
return 0;
}
六、二维费用背包问题
1、问题描述:
对于某件物品,如果取这一件物品,就要付出两种代价,这两种代价都有一个最大值(背包容量),问怎么样选取物品使其价值最大化 设这两种代价分别为代价1和代价2 ,第 i 件物品所需的代价分别为w1[i],w2[i], 价值为v[i],这两种代价可付出的最大值分别为T,V
2、解题思路:
费用增加一维,只需要状态增加一维就可以
dp[i,j,k]表示第i件物品在两个最大代价为j ,k 的时候可以得到的最大价值
那么我们可以根据上面的讲解写出状态转移方程
dp[i,j,k] = max(dp[i - 1,j,k] ,dp[i - 1,j - w1[i],k - w2[i]] + v[i] )
跟一维背包没什么太大变化,只不过是在枚举状态的时候枚举二维状态 而且当然可以利用二维数组省空间解决
伪代码
for i: 1 - n //物品数
for j: T - w1[i]
for k: V - w2[i]
dp[j,k] = max(dp[j - w1[i]][k - w2[i]] + v[i])
关于二维费用背包,其实只是状态要多枚举一层,其余做法和变式与一位费用背包并没有太大差别
例题:
- luogu p1507
这个题就是一个模板题,没啥好说的直接上代码
#include <bits/stdc++.h>
#define ss system("pause");
using namespace std;
const int maxn = 5e1 + 5;
const int maxc = 4e2 + 5;
int dp[maxc][maxc],v[maxn],w1[maxn],w2[maxn];
int C1,C2,n;
void Solve()
{
for(int i = 1; i <= n; i++){
for(int j = C1; j >= w1[i]; j--){
for(int k = C2; k >= w2[i]; k--){
dp[j][k] = max(dp[j][k],dp[j - w1[i]][k - w2[i]] + v[i]);
}
}
}
}
int main()
{
cin>>C1>>C2;
cin>>n;
for(int i = 1; i <= n; i++) cin>>w1[i]>>w2[i]>>v[i];
Solve();
cout<<dp[C1][C2]<<endl;
//ss
return 0;
}
2.hdu 2159
这个题也是一个标准的二维费用背包问题,但是这是一个完全背包问题,但是要求找消耗最小的耐心值,所以只需要在找的过程中找到>=所需升级经验所消耗的最小耐心值就可以了
dp状态 dp[n,m] 恰好消耗n耐心值时 并打了 m只怪物的所得的最大经验值
我看网上题解都是直接不考虑这个包(耐心)是不是完全装满,但是我认为这是一个恰好装满的二维费用完全背包问题,(很绕是吧)因为耐心值肯定是我打这个怪才消耗,而且我在比较的过程中比较的就是消耗的耐心值所以这个包(耐心值)肯定是恰好装满的,而比较时只比较有效状态,所以我认为把这个题当作恰好装满更为严谨一些
PS:这个题的两个代价分别是耐心值和打佰怪物个数,循环顺序和维数顺序不重要
code
#include <bits/stdc++.h>
#define ss system("pause");
using namespace std;
const int maxn = 1e2 + 5;
const int maxc = 1e2 + 5;
const int inf = 1e9 + 7;
int dp[maxc][maxc],v[maxn],p[maxn];//经验 耐心
int minp;
void Solve(int bag,int number,int k,int allval)
{
memset(dp,-inf,sizeof dp);
dp[0][0] = 0;
minp = inf;
for(int i = 1; i <= k; i++){
for(int j = p[i]; j <= bag; j++){
for(int num = 1; num <= number; num++){
dp[j][num] = max(dp[j][num],dp[j - p[i]][num - 1] + v[i]);
if(dp[j][num] < 0) dp[j][num] = -inf;
//只有当这个包全满时,才能进行比较,因为只有打怪才会降低耐心
else if(dp[j][num] >= allval) minp = min(minp,j);
}
}
}
if(minp >= inf) cout<<-1<<endl;
else cout<<bag - minp<<endl;
}
int main()
{
int n,m,k,s;
while(cin>>n>>m>>k>>s){
for(int i = 1; i <= k; i++) cin>>v[i]>>p[i];
Solve(m,s,k,n);
}
ss
return 0;
}
七、关于装满某个容量有多少种取法
1、问题描述:
就是把背包问题又装满某个容量获得的最大价值改为共有多少种取法,由于例题是完全背包的,所以就用完全背包描述一下,思想是相近的
2、思路:
- dp状态
dp[i,j] 到i个物品容量为j时共有多少种取法
- 状态转移方程:
可以想到,对于当前物品,有两种策略,一种是拿(先不论拿多少),一种是不拿,那么对于当前解就为不拿这个物品有多少种取法 + 拿这个物品有多少种取法 (图片转载自
传送门)
那么可以推得状态转移方程
dp[i,j]=dp[i - 1, j] + dp[i - 1, j - v] + dp[i - 1, j - 2 * v] + ... + dp[i - 1,j-k * v]
这个状态方程是用三层循环求解的,则导致复杂度会很高,但是有了上面完全背包的递推公式转化,可以推广一下
推导:
f[i,j] = f[i - 1, j] + f[i - 1, j - v] + f[i - 1, j - 2 * v] + ... + f[i - 1,j - k * v] (1)
f[i, j - v] = f[i - 1, j - v] + f[i - 1, j - 2 * v] + ... + f[i - 1, j - k * v] (2)
将(2) 带入(1) 可得:
f[i,j] = f[i - 1, j] + f[i, j - v];
利用图片会更直观一些(转载自传送门)
由此就得到了类似于求最大价值时的完全背包问题的状态转移方程,将三层循环优化至两层循环
例题 货币系统
#include <iostream>
using namespace std;
typedef long long ll;
const int maxn = 1e4 + 5;
ll dp[maxn], w[30];
void Solve(int v, int n)
{
dp[0] = 1;
for(int i = 1; i <= v; i++) {
for(int j = w[i]; j <= n; j++) {
dp[j] = dp[j] + dp[j - w[i]];
}
}
cout << dp[n] << endl;
}
int main()
{
int v, n;
cin >> v >> n;
for(int i = 1; i <= v; i++) cin >> w[i];
Solve(v, n);
return 0;
}
这类问题还可以利用记忆化搜索解决,可以参考这篇文章
传送门