当前位置: 首页 > news >正文

多重背包讲解

假设有一道题目:

题目描述

给定 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 1n,m,wi100 1 ≤ c i ≤ 50 1\le c_i \le 50 1ci50 1 ≤ v i ≤ 1 0 5 1\le v_i \le 10^5 1vi105

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1 0 3 1\le n \le 10^3 1n103 1 ≤ w i , m ≤ 1 0 4 1\le w_i,m \le 10^4 1wi,m104 1 ≤ c i ≤ 100 1\le c_i \le 100 1ci100 1 ≤ v i ≤ 1 0 9 1\le v_i \le 10^9 1vi109

对于前 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(dpi1,j,dpi1,js×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 i1 层有关。

于是状态改为 d p i dp_i dpi 表示可用空间为 i i i 的情况下最大能够获得的最大价值。

为何要倒序枚举可用空间?

动态规划中有个很重要的思想就是「无后效性」。

简单来说,就是后面的答案不会影响前面的答案。

抛硬币就是一个极其典型的「无后效性」,无论你之前抛了多少次,正面朝上和反面朝上的概率永远相等,不会因为你前面 9 9 9 次是正面第 10 10 10 次一定就是正面。

手动模拟的话就会知道,如果正序枚举的话,会使得后面的第 i i i 层状态访问的不是第 i − 1 i-1 i1 层,而是第 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)

相关文章:

  • 使用TensorFlow时需掌握的Pandas核心知识点
  • JDK15开始偏向锁不再默认开启
  • Qt开发——问界M9空调
  • 强化学习的一些概念
  • 运维面试题(三)
  • Java虚拟机面试题:内存管理(中)
  • 【java】集合练习2
  • Chapter 4-11. Troubleshooting Congestion in Fibre Channel Fabrics
  • Nest系列:在 NestJS 中使用 Joi 进行环境变量验证与配置管理-03
  • Navicat如何查看密码
  • Chrome 浏览器的很多扩展不能用了
  • 数字签名与非对称加密的区别
  • LLM论文笔记 24: A Theory for Length Generalization in Learning to Reason
  • AJAX PHP:深入理解与实际应用
  • 【WEB APIs】DOM-节点操作
  • 本地部署Deep Seek-R1,搭建个人知识库——笔记
  • Spring Boot使用线程池创建多线程
  • 人工智能驱动数字孪生城市的实践探索
  • 《AI生成文章SEO 长尾关键词下拉词相关词抓取工具 SEO 裂变工具:高效驱动网站流量增长》
  • qq音乐 webpack 补环境
  • 广西百色通报:极端强对流天气致墙体倒塌,3人遇难7人受伤
  • 咸宁市委常委、市纪委书记官书云调任湖北省司法厅副厅长
  • 上海杨浦:优秀“博主”购房最高可获200万补贴
  • 吉林市马拉松5月18日开赛,奖牌、参赛服公布
  • 马上评|让“贾宝玉是长子长孙”争议回归理性讨论
  • 普雷沃斯特当选新一任天主教罗马教皇