文章目录
- 一、多重背包问题特点
- 1.1、多重背包问题的特征
- 1.2、解决多重背包问题的基本方法
- 典型例题:AcWing——多重背包问题I
- 1.3、二进制优化
- 1.3.1、二进制优化的思想
- 1.3.2、多重背包问题的二进制优化
一、多重背包问题特点
多重背包问题是背包问题的又一变种,它在0-1背包和完全背包问题的基础上增加了一个限制:每种物品i
除了有一个重量w[i]
和价值v[i]
外,还有一个最大可用数量n[i]
。这意味着每种物品i
可以被选取的次数最多是n[i]
次,而不是只有一次(0-1背包问题)或无限次(完全背包问题)。
1.1、多重背包问题的特征
- 有限的物品数量:每种物品有一个最大数量限制,不能无限制地选择。
- 背包容量限制:存在一个容量限制,所有选取的物品的总重量不能超过这个限制。
- 优化目标:目标是在不超过背包容量的前提下,最大化背包内物品的总价值。
- 复杂度:时间和空间复杂度取决于具体的实现方法,一般时间复杂度为
O(V*sum(n[i]))
。
1.2、解决多重背包问题的基本方法
解决多重背包问题的基本思路是利用动态规划,其中最直观的方法是使用二维DP数组dp[i][j]
,表示考虑前i
种物品,在不超过重量j
的情况下的最大价值。和0-1背包问题的区别在于物品i
能取n[i]
次,因此状态转移方程可以写为:
for(int k=0;k<=n[i];++k)
if(j-k*weight[i]>=0)
dp[i][j]=max(dp[i][j],dp[i-1][j-k*weight[i]]+k*value[i]);
这里,k
是从0
到n[i]
的整数,表示选择第i
种物品k
次的可能性。
由于这种方法会导致较高的时间复杂度,时间复杂度为O(V*sum(n[i]))
,特别是当n[i]
的值很大时,常常需要使用其他技巧。
典型例题:AcWing——多重背包问题I
AcWing:多重背包问题I
按照以上思路,并且按照0-1背包一样的思路,进行降维优化。
for(int i=0;i<N;++i)
for(int j=V;j>=volume[i];--j)
for(int k=0;k<=n[i];++k)
if(j-k*volume[i]>=0)
dp[j]=max(dp[j],dp[j-k*volume[i]]+k*value[i]);
本题可以书写代码为:
#include<bits/stdc++.h>
using namespace std;
int dp[101];
int main(void){
ios_base::sync_with_stdio(false);
cin.tie(0);
int N,V;
cin>>N>>V;
vector<int> volume(N);
vector<int> value(N);
vector<int> n(N);
for(int i=0;i<N;++i){
cin>>volume[i]>>value[i]>>n[i];
}
for(int i=0;i<N;++i)
for(int j=V;j>=volume[i];--j)
for(int k=0;k<=n[i];++k)
if(j-k*volume[i]>=0)
dp[j]=max(dp[j],dp[j-k*volume[i]]+k*value[i]);
cout<<dp[V];
return 0;
}
1.3、二进制优化
1.3.1、二进制优化的思想
对于任何一个数,我们可以将其进行二进制拆分。即任何一个10进制数都能写成二进制数的形式。
比如:3=1+2=11B、10=2+8=1010B。
如果我们对于一个数,例如10,取出小于其的所有二进制位,即1B,10B,100B,1000B,那么我们必然能够选出其中的某些位来凑成10,并且对于小于10的任何一个正整数也可以被凑成,即选取其中若干位有:1B、10B、11B、100B、101B、110B、111B、1000B、1001B···。即,如果我们有一个物品,它能够被选择小于等于n次,我们可以通过选择其中的某些位来实现选择的所有情况。
那么我们把一个物品可以最多选择n次的问题,按照未优化的多重背包问题需要进行O(V*sum(n[i]))
次计算取最大值 变成了
考虑在logn个物品中选择其中的某些物品,取最大值 的问题了。按到理来说,logn个物品要么被选要么不被选,一共是2的logn次方种情况,也就是n种情况;但是如果我们把这个问题转化成0-1背包问题
,即利用动态规划来考虑,就变成了O(V*sum(logn[i]))
次了。当n[i]<=2000时
,logn[i]<=3.301
,减少了一千倍计算量。
因此,我们对于n[i]
,可以进行二进制拆分,将n[i]
个物品按照二进制拆分,变成若干堆,每次选择只能一次全选,这样使得我们需要考虑的物品变成logn[i]
个,多重背包问题
变成了一个0-1背包问题
,即这logn[i]
个物品,要么被选,要么不被选,能够取得的最大值。我们通过上述叙述可以知道logn[i]
个物品的所有被选择的情况,刚好构成了0~n[i]
中的所有数字。
由于一个物品的数量n不一定是2的整数倍,因此我们在考虑二进制的时候,因为我们的目的是凑成0~ n,因此我们在考虑二进制的时候,我们考虑到小于等于n/2位即可,它们能满足0~ k种情况(k<=n/2),然后剩余的n-k部分直接单独成一块就行,这样,能满足n-k~ k+n-k种情况,合并起来就是0~n中情况。
1.3.2、多重背包问题的二进制优化
通过二进制拆分,我们将m
个物品每个最多选n[i]
次的多重背包问题
,转换成了 sum(logn[i])
个物品的0-1背包问题
。相当于进行了问题转换。
代码:
#include<bits/stdc++.h>
using namespace std;
int dp[2002];
int main(void){
ios_base::sync_with_stdio(false);
cin.tie(0);
int N,V;
cin>>N>>V;
vector<int> value;
vector<int> volume;
for(int i=0;i<N;++i){//构建二进制拆分物品
int w,v,num;
cin>>w>>v>>num;//w单物品体积,v单物品价值,num可选数量
int s=num;
int b=1;
while(b<=s){//注意b必须是小于num,且不能是最高位 如1010B,b不能是1000B
value.push_back(b*v);
volume.push_back(b*w);
s-=b;
b*=2;
}
if(s>0){
value.push_back(s*v);
volume.push_back(s*w);
}
}
N=value.size();//更新物品数量
for(int i=0;i<N;++i)
for(int j=V;j>=volume[i];--j)
dp[j]=max(dp[j],dp[j-volume[i]]+value[i]);
cout<<dp[V];
return 0;
}
位数关系即:
int s=num;
int b=1;
num=log2(num);
num=pow(2,num-1);
while(b<=num){
value.push_back(b*v);
volume.push_back(b*w);
s-=b;
b*=2;
}
if(s>0){
value.push_back(s*v);
volume.push_back(s*w);
}
int s=num;
int b=1;
num=num>>1;
while(b<=num){
value.push_back(b*v);
volume.push_back(b*w);
s-=b;
b*=2;
}
if(s>0){
value.push_back(s*v);
volume.push_back(s*w);
}
官方:
#include<iostream>
using namespace std;
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N]; //逐一枚举最大是N*logS
int f[M]; // 体积<M
int main()
{
cin >> n >> m;
int cnt = 0; //分组的组别
for(int i = 1;i <= n;i ++)
{
int a,b,s;
cin >> a >> b >> s;
int k = 1; // 组别里面的个数
while(k<=s)
{
cnt ++ ; //组别先增加
v[cnt] = a * k ; //整体体积
w[cnt] = b * k; // 整体价值
s -= k; // s要减小
k *= 2; // 组别里的个数增加
}
//剩余的一组
if(s>0)
{
cnt ++ ;
v[cnt] = a*s;
w[cnt] = b*s;
}
}
n = cnt ; //枚举次数正式由个数变成组别数
//01背包一维优化
for(int i = 1;i <= n ;i ++)
for(int j = m ;j >= v[i];j --)
f[j] = max(f[j],f[j-v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}