多重背包讲解
假设有一道题目:
题目描述
给定 n n n 种物品,现在我们有一个容量为 m m m 的背包。
第 i i i 种物品的体积为 v i v_i vi,价值为 w i w_i wi,其数量有 c i c_i ci 个。
求在物品体积之和不超过 m m m 的情况下最大能获得的价值。
输入格式
第一行输入两个数 n , m n,m n,m,分别表示物品种数和背包容积。
接下来 n n n 行,每行输入三个正整数 v i , w i , c i v_i,w_i,c_i vi,wi,ci,分别表示第 i i i 种物品的体积、价值、数量。
输出格式
共一行,输出能获得的最大价值。
输入输出样例 #1
输入 #1
5 10 2 3 1 4 6 3 5 4 2 7 8 9 4 1 3
输出 #1
15
输入输出样例 #2
输入 #2
10 73 4 5 2 9 7 6 5 3 1 7 6 4 3 9 2 10 3 5 3 6 2 7 4 9 6 4 3 9 10 1
输出 #2
85
说明/提示
对于前 30 % 30\% 30% 的数据, 1 ≤ n , m , w i ≤ 100 1\le n,m,w_i \le 100 1≤n,m,wi≤100, 1 ≤ c i ≤ 50 1\le c_i \le 50 1≤ci≤50, 1 ≤ v i ≤ 1 0 5 1\le v_i \le 10^5 1≤vi≤105。
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1 0 3 1\le n \le 10^3 1≤n≤103, 1 ≤ w i , m ≤ 1 0 4 1\le w_i,m \le 10^4 1≤wi,m≤104, 1 ≤ c i ≤ 100 1\le c_i \le 100 1≤ci≤100, 1 ≤ v i ≤ 1 0 9 1\le v_i \le 10^9 1≤vi≤109。
对于前 30 % 30\% 30% 的数据
发现 n × m × c i n \times m \times c_i n×m×ci 在我们可承受的范围之内。
所以我们采用非常暴力的方式,枚举物品数量 k k k,求出答案。
设 d p i , j dp_{i,j} dpi,j 表示对于前 i i i 个物品,可用空间为 j j j 的情况下最大能获得多少价值。
则状态转移为:
d
p
i
,
j
=
max
(
d
p
i
−
1
,
j
,
d
p
i
−
1
,
j
−
s
×
v
i
+
s
×
+
w
i
)
dp_{i,j}=\max(dp_{i-1,j},dp_{i-1,j-s\times v_i}+s\times+w_i)
dpi,j=max(dpi−1,j,dpi−1,j−s×vi+s×+wi)
其中 s s s 代表的是数量。
实现
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,dp[105][105];
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1,v,w,c;i<=n;i++){
cin>>v>>w>>c;
for(int k=0;k<=c&&k*v<=m;k++){
for(int j=k*v;j<=m;j++){
dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*v]+k*w);
}
}
}
cout<<dp[n][m];
return 0;
}
对于 100 % 100\% 100% 的数据
显然,再采用上面的代码计算次数会达到 1 0 9 10^9 109 左右的级别,显然会超时。
这时候考虑二进制优化。
为什么多重背包可以用二进制拆分优化?
多重背包问题可以用二进制拆分进行优化,主要原因在于二进制拆分能够将物品的数量分解为若干个 2 2 2 的幂次方之和,从而将问题转化为多个 01 背包问题。这种转化不仅保持了问题的最优解,还显著减少了需要处理的物品数量,从而降低了时间复杂度。
二进制优化
二进制优化的核心思想是将每个物品的数量分解为二进制形式,从而将多重背包问题转化为多个 01 背包问题,减少需要考虑的物品数量,以达到优化算法时间复杂度的目的。
我们将数量拆分为若干个 2 2 2 的幂次方之和,比如说有 7 7 7 个物品,我们可以将其拆分为 2 0 + 2 1 + 2 2 = 1 + 2 + 4 = 7 2^0+2^1+2^2=1+2+4=7 20+21+22=1+2+4=7。
当然,有的时候会剩余一些物品数量,假设有 10 10 10 个物品,在分解为 2 0 + 2 1 + 2 2 2^0+2^1+2^2 20+21+22 之后,还剩余 3 3 3,此时剩余部分重新划分为一组。
这样下来,原本 x x x 个物品,现在仅需处理 log ( x ) \log(x) log(x) 次,减少了时间复杂度。物品数量越多,效果越好。
二进制拆分为何不会漏掉正确的答案?
现在,同样是 10 10 10 个物品,将其拆分为 2 0 + 2 1 + 2 2 + 3 2^0+2^1+2^2+3 20+21+22+3。
将其进行组合:
取 1 1 1 个物品( 2 0 2^0 20)。
取 2 2 2 个物品( 2 1 2^1 21)。
取 3 3 3 个物品( 2 0 + 2 1 2^0+2^1 20+21)。
取 4 4 4 个物品( 2 2 2^2 22)。
取 5 5 5 个物品( 2 0 + 2 2 2^0+2^2 20+22)。
取 6 6 6 个物品( 2 0 + 2 1 + 3 2^0+2^1+3 20+21+3)。
取 7 7 7 个物品( 2 0 + 2 1 + 2 2 2^0+2^1+2^2 20+21+22)。
取 8 8 8 个物品( 3 + 2 0 + 2 2 3+2^0+2^2 3+20+22)。
取 9 9 9 个物品( 2 1 + 2 2 + 3 2^1+2^2+3 21+22+3)。
取 10 10 10 个物品( 2 0 + 2 1 + 2 2 + 3 2^0+2^1+2^2+3 20+21+22+3)。
通过这种方式,二进制拆分确保了所有可能的取法都被考虑到了。
证明 2 2 2:
假设对于任意数量的物品 s s s,通过二进制拆分生成的物品可以覆盖所有可能的取法。
对于所有的 s < k s<k s<k,二进制拆分可以覆盖所有可能的取法(假设 α \alpha α)。
对于 s = k s=k s=k,将其拆分为最大的 2 2 2 的幂次方 a a a 和剩余部分 b b b。根据假设 α \alpha α, b b b 可以被拆分为若干个 2 2 2 的幂次方之和。
所以, k = a + b k=a+b k=a+b 可以被覆盖。
假设成立。
空间优化
在不经过优化的情况下,空间复杂度 O ( n m ) O(nm) O(nm),在 MLE 的边缘。
需要将空间进行优化。
发现对于第 i i i 层状态仅仅只跟第 i − 1 i-1 i−1 层有关。
于是状态改为 d p i dp_i dpi 表示可用空间为 i i i 的情况下最大能够获得的最大价值。
为何要倒序枚举可用空间?
动态规划中有个很重要的思想就是「无后效性」。
简单来说,就是后面的答案不会影响前面的答案。
抛硬币就是一个极其典型的「无后效性」,无论你之前抛了多少次,正面朝上和反面朝上的概率永远相等,不会因为你前面 9 9 9 次是正面第 10 10 10 次一定就是正面。
手动模拟的话就会知道,如果正序枚举的话,会使得后面的第 i i i 层状态访问的不是第 i − 1 i-1 i−1 层,而是第 i i i 层。
「无后效性」被打破,后面的状态访问了不该访问的状态。
为了避免这个问题,我们选择从后往前枚举。
多重背包二进制优化为什么不能用二维数组存储?
最根本的原因是空间复杂度太高。
二进制优化最初的目的就是减少状态数量,应对大规模数据。
但是使用了二维数组之后,空间复杂度重新回到了 O ( n m ) O(nm) O(nm),就好比你辛辛苦苦把一个大包袱拆成几个小包袱,结果又把它们重新堆成一个更大的包袱,完全得不偿失。
而且,动态规划计算当前状态时,只需要依赖前一个状态(依赖很久以前产生的状态那是搜索干的事),而不需要保留所有历史状态。
问题就在于,二维数组它会完整地记下每个状态的值(我们并不需要)。
导致大量空间被浪费。
一维数组则充分利用动态规划的特性,减少了时间复杂度。
总的来说,二维数组是低效且不必要的选择。
实现
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,dp[10005];
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1,v,w,c;i<=n;i++){
cin>>v>>w>>c;
for(int k=1;k<=c;k*=2){ // 二进制拆分
int tmp=min(k,c);
c-=k;
for(int j=m;j>=tmp*v;j--){ // 状态转移
dp[j]=max(dp[j],dp[j-v*tmp]+w*tmp);
}
}
if(c>0){ // 处理剩余部分
for(int j=m;j>=c*v;j--){
dp[j]=max(dp[j],dp[j-v*c]+w*c);
}
}
}
cout<<dp[m];
return 0;
}
时间复杂度 O ( n m log c i ) O(nm\log c_i) O(nmlogci),空间复杂度 O ( m ) O(m) O(m)。