【C++算法模板】背包九讲(上):01背包、完全背包、多重背包

本文详细讨论了01背包、完全背包和多重背包问题的解决方案,重点介绍了使用二维数组和一维数组的优化,以及公式优化和单调队列在时间复杂度上的改进。通过一维滚动数组和二进制优化技巧降低空间复杂度和时间复杂度至O(n^2)和O(n^2logn)。
摘要由CSDN通过智能技术生成

1)01背包

1:二维数组

时间复杂度: O ( n 2 ) O(n^2) O(n2),空间复杂度: O ( n 2 ) O(n^2) O(n2)

  • 非常熟悉和基础,没什么可讲的
#include<bits/stdc++.h>
#define x first
#define y second

using namespace std;

typedef long long ll;
typedef pair<int,int> PII;

// 解题思路: 

const int N=1e3+5;
const int M=1e3+5;

int n,m;
int v[N],w[N];
int f[N][M];

int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		scanf("%d%d",&v[i],&w[i]); // v:重量,w:价值
	}
	// 枚举物品
	for(int i=1;i<=n;i++) {
		// 枚举背包容量
		for(int j=0;j<=m;j++) {
			f[i][j]=f[i-1][j];
			// 如果能拿物品i
			if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]); // v:体积,w:价值
		}	
	}
	int res=0;
	// 当取n个物品时最大总价值,其实也就是f[n][m]
	for(int i=0;i<=m;i++) res=max(res,f[n][i]);
	cout<<res<<endl;
	cout<<f[n][m]<<endl;
	return 0;
}

2:一维数组

时间复杂度: O ( n 2 ) O(n^2) O(n2),空间复杂度: O ( n ) O(n) O(n)

  • 因为 f [ i ] [ j ] f[i][j] f[i][j] 只与 f [ i − 1 ] [ j ] f[i-1][j] f[i1][j] f [ i − 1 ] [ j − v [ i ] ] + w [ i ] f[i-1][j-v[i]]+w[i] f[i1][jv[i]]+w[i] 有关,即只与上一个状态(上一次选择)有关,那么我们只需要开一个一维数组记录上一次选择即可,这个数组名为滚动数组
  • 由于滚动数组已经表示了上一次选择的状态,所以代码 f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j]=f[i-1][j] f[i][j]=f[i1][j] 可以去掉
  • 枚举背包体积时必须从大到小,原因已经在代码中解释,如果还是 f o r ( i n t   j = 0 ; j < = m ; j + + ) for(int\ j=0;j<=m;j++) for(int j=0;j<=m;j++) ,那么用到的实际上还是 f [ i ] f[i] f[i] 这个状态而不是 f [ i − 1 ] f[i-1] f[i1] 这个状态
  • 如果把所有的 f [ i ] f[i] f[i] 都初始化为 0 0 0,那么 f [ m ] f[m] f[m] 表示的是当背包体积 < = m <=m <=m 时的最大价值是多少;如果只把 f [ 0 ] f[0] f[0] 初始化为 0 0 0 ,而其他初始化为 − I N F -INF INF,那么 f [ m ] f[m] f[m] 表示的是体积刚好等于 m m m 时的最大价值,所以全部初始化为 0 0 0 的话 f [ m ] f[m] f[m] 就是答案
#include<bits/stdc++.h>
#define x first
#define y second

using namespace std;

typedef long long ll;
typedef pair<int,int> PII;

// 解题思路: 

const int N=1e3+5; // 最大物品数
const int M=1e3+5; // 最大容量
int n,m;
int f[M]; // f[i]:物品容量为i时的背包最大容量
int v[N],w[N];

int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		scanf("%d%d",&v[i],&w[i]);	
	}
	for(int i=1;i<=n;i++) {
		// 因为f[i][j]只与f[i-1][j]/f[i-1][j-v[i]]+w[i],只与上一个状态有关
		// 所以二维数组中有很多空间被浪费,计算完就可以丢掉了
		// 所以用滚动数组,但是在枚举背包体积的时候必须从大到小排序,若顺序的话再用到上一个状态时就已经被更新过了
		for(int j=m;j>=v[i];j--) 
			f[j]=max(f[j],f[j-v[i]]+w[i]);
	}
	int res=0;
	for(int i=0;i<=m;i++) res=max(res,f[i]);
	cout<<res<<endl;
	cout<<f[m]<<endl;
	return 0;
}

2)完全背包

1:朴素做法

时间复杂度: O ( n 3 ) O(n^3) O(n3),空间复杂度: O ( n 2 ) O(n^2) O(n2)

  • 时间复杂度和空间复杂度都较高,手动枚举每个物品可选择数量
#include<bits/stdc++.h>
#define x first
#define y second

using namespace std;

typedef long long ll;
typedef pair<int,int> PII;

// 解题思路: 

const int N=1e3+5;
const int M=1e3+5;
int f[N][M];
int n,m;
int v[N],w[N];

int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		scanf("%d%d",&v[i],&w[i]);
	}
	// 朴素做法,枚举物品
	for(int i=1;i<=n;i++) {
		// 枚举背包容量
		for(int j=0;j<=m;j++) {
			// 枚举物品可挑选数量
			for(int k=0;k*v[i]<=j;k++) {
				f[i][j]=max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);
			}
		}	
	}
	cout<<f[n][m]<<endl;
	return 0;
}

2:公式优化

时间复杂度: O ( n 2 ) O(n^2) O(n2),空间复杂度: O ( n ) O(n) O(n)

  • f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w ,   f [ i − 1 , j − 2 v ] + 2 w ,   f [ i − 1 , j − 3 v ] + 3 v ,   . . . ) f[i,j]=max(f[i-1,j],f[i-1,j-v]+w,\ f[i-1,j-2v]+2w,\ f[i-1,j-3v]+3v,\ ...) f[i,j]=max(f[i1,j],f[i1,jv]+w, f[i1,j2v]+2w, f[i1,j3v]+3v, ...),① f [ i , j − v ] = m a x ( f [ i − 1 , j − v ] , f [ i − 1 , j − 2 v ] + w ,   f [ i − 1 , j − 3 v ] + 2 w ,   f [ i − 1 , j − 4 v ] + 3 w ,   . . . ) f[i,j-v]=max(f[i-1,j-v],f[i-1,j-2v]+w,\ f[i-1,j-3v]+2w,\ f[i-1,j-4v]+3w,\ ...) f[i,jv]=max(f[i1,jv],f[i1,j2v]+w, f[i1,j3v]+2w, f[i1,j4v]+3w, ...)②,
  • 可以看出②式是①式第二项起 + w +w +w 的结果,所以 f [ i , j ] = m a x ( f [ i − 1 , j ] ,   f [ i , j − v ] + w ) f[i,j]=max(f[i-1,j],\ f[i,j-v]+w) f[i,j]=max(f[i1,j], f[i,jv]+w),即转移时与上一个状态无关,所以我们就可以顺序枚举物品容量
#include<bits/stdc++.h>
#define x first
#define y second

using namespace std;

typedef long long ll;
typedef pair<int,int> PII;

// 解题思路: 

const int N=1e3+5;
const int M=1e3+5;
int n,m;
int v[N],w[N];
int f[N][M];

int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		scanf("%d%d",&v[i],&w[i]);	
	}
	for(int i=1;i<=n;i++) {
		for(int j=0;j<=m;j++) {
			f[i][j]=f[i-1][j];
			// 至少能装下一个
			if(j>=v[i]) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]); // 和01背包的区别就在于可以与当前状态有关
		}	
	}
	cout<<f[n][m]<<endl;
	return 0;
}

3:再优化一维数组

时间复杂度: O ( n 2 ) O(n^2) O(n2),空间复杂度: O ( n ) O(n) O(n)

  • 注意注释中是如何分析当 f f f 数组是一维时状态转移的是当前状态还是上一次的状态
  • 因为滚动数组本身代表上一次的状态,所以 f [ i , j ] = f [ i − 1 , j ] f[i,j]=f[i-1,j] f[i,j]=f[i1,j] 直接优化成 f [ j ] = f [ j ] f[j]=f[j] f[j]=f[j],无意义,所以不用写
#include<bits/stdc++.h>
#define x first
#define y second

using namespace std;

typedef long long ll;
typedef pair<int,int> PII;

// 解题思路: 

const int N=1e3+5;
const int M=1e3+5;
int n,m;
int v[N],w[N];
int f[M];

int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		scanf("%d%d",&v[i],&w[i]);	
	}
	for(int i=1;i<=n;i++) {
		// f[i][j]=f[i-1][j]直接删掉一维,即f[j]=f[j](一维滚动数组直接代表上一次的状态,所以不需要这句代码)
		// j是从小到大枚举的
		for(int j=v[i];j<=m;j++) {
			// j-v[i]是<j的,所以算f[j]的时候f[j-v[i]]已经被算过了,所以是第i层的f[i][j-v[i]]
			f[j]=max(f[j],f[j-v[i]]+w[i]);
		}	
	}
	cout<<f[m]<<endl;
	return 0;
}

3)多重背包

1:朴素做法

时间复杂度: O ( n 3 ) O(n^3) O(n3),空间复杂度: O ( n 2 ) O(n^2) O(n2)

  • 类似于完全背包的朴素做法,直接第三层去枚举物品的个数,不过多了一个限制条件, k < = s [ i ] k<=s[i] k<=s[i]
#include<bits/stdc++.h>
#define x first
#define y second

using namespace std;

typedef long long ll;
typedef pair<int,int> PII;

// 解题思路: 

const int N=1e2+5;
const int M=1e2+5;
int n,m;
int v[N],w[N],s[N];
int f[N][M];

int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		scanf("%d%d%d",&v[i],&w[i],&s[i]);	
	}
	// 枚举物品
	for(int i=1;i<=n;i++) {
		for(int j=0;j<=m;j++) {
			// 能拿的个数从0~s[i],且k*v[i]能被背包装下
			for(int k=0;k<=s[i] && k*v[i]<=j;k++) {
				f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
			}
		}	
	}
	cout<<f[n][m]<<endl;
	return 0;
}

2:二进制优化

时间复杂度: O ( n 2 l o g n ) O(n^2log^n) O(n2logn),空间复杂度: O ( n ) O(n) O(n)

  • 如果按照类似于完全背包从状态转移方程式入手的角度来优化是不可行的,因为完全背包问题不会多最后一项出来,多了一个 i f if if 的判定条件,最后一项会因为超出了 j j j 而被舍去

  • 如何优化呢?若 s = 1023 s=1023 s=1023,那么 1023 1023 1023 能由其 s u m ( 2 n ) < = 1023 sum(2^n)<=1023 sum(2n)<=1023 的这 n n n 2 2 2 的指数组成( n = 9 n=9 n=9),把 1023 1023 1023 个物品当作是 1023 1023 1023 01 01 01 背包,再通过二进制优化的方式把从枚举 1023 1023 1023 次转换为只需要枚举 9 9 9 次(倍增法思想)

  • 比如 s = 200 s=200 s=200 ∑ i = 1 n   2 i < = 200 \sum_{i=1}^n\ 2^i<=200 i=1n 2i<=200 可得 { 1 ,   2 ,   4 ,   8 ,   16 ,   32 ,   64 } \{1,\ 2,\ 4,\ 8,\ 16,\ 32,\ 64\} {1, 2, 4, 8, 16, 32, 64},这几个数字之和为 127 127 127,则只需要再来一个 73 73 73 就可以凑出 [ 0 , 200 ] [0,200] [0,200] 的数字;即对于任意一个 s s s ,我们可以凑成 { 1 ,   2 ,   4 ,   8 ,   . . . ,   2 k ,   c } \{1,\ 2,\ 4,\ 8,\ ...,\ 2^k,\ c\} {1, 2, 4, 8, ..., 2k, c},其中 c < 2 k + 1 c<2^{k+1} c<2k+1

#include<bits/stdc++.h>
#define x first
#define y second

using namespace std;

typedef long long ll;
typedef pair<int,int> PII;

// 解题思路: 

const int N=2e4+5; // 1000*log(2000),2w4左右
const int M=2e3+5;
int n,m;
int v[N],w[N];
int f[N];

int main() {
	cin>>n>>m;
	int cnt=0;
	for(int i=1;i<=n;i++) {
		int a,b,s;
		scanf("%d%d%d",&a,&b,&s);
		int k=1; // 从2^0开始,每组初始物品数
		while(k<=s) {
			// 每次把k个第i个物品打包在一起
			cnt++; // 编号++
			v[cnt]=a*k;
			w[cnt]=b*k;
			s-=k;
			k*=2;
		}
		// 补常数
		if(s>0) {
			cnt++; // 第i个物品的最后一次分组
			v[cnt]=a*s;
			w[cnt]=b*s;
		}
	}
	n=cnt; // 物品个数变成总的打包数
	for(int i=1;i<=n;i++) {
		// 至少能拿一个物品,所以j>=v[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;
}

3:单调队列优化

  • 看不懂,溜了溜了
  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: C 01 背包问题是一种经典的动态规划问题。它的基本思想是:给定一个容量为 C 的背包和 N 个物品,每个物品都有自己的体积和价值,求在满足背包容量限制的前提下,能够装入背包中的物品的最大价值总和。 解决该问题的常用模板为: 1. 定义状态:定义 dp[i][j] 表示考虑前 i 个物品,容量为 j 的背包能够装入物品的最大价值总和。 2. 状态计算:根据背包的容量限制和物品的体积和价值,使用递推公式进行状态转移。 - dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]) 其中,v[i] 和 w[i] 分别表示第 i 个物品的体积和价值。 3. 边界:考虑边界条件,dp[0][j]=0,dp[i][0]=0。 4. 计算结果:遍历整个 dp 数组,找到一个使得 dp[N][j] 最大的 j 值,即为答案。 ### 回答2: 01背包问题是指有一个背包,最多能装载一定重量的物品,现有一组物品,其重量和价值分别为wi和vi,求在背包容量限制下,如何选择物品,使得背包中物品的总价值最大化。 解决01背包问题的核心思想是动态规划。创建一个二维数组dp[n+1][W+1],其中n为物品的个数,W为背包的重量限制。dp[i][j]表示在前i个物品中选择,在背包容量为j时的最大总价值。 初始化dp数组的第一行和第一列为0,表示背包容量为0或没有物品可选时,总价值都为0。接下来,开始进行状态转移。 对于每一个物品i,可以选择将其放入背包中或不放入。如果将物品i放入背包中,则背包的容量会减少wi,总价值会增加vi。如果不放入物品i,则背包的容量和总价值都不变。因此,在计算dp[i][j]时,可以根据以下条件进行选择: - 如果j < wi,则无法将物品i放入背包中,此时dp[i][j] = dp[i-1][j]; - 如果j >= wi,则可以选择将物品i放入背包中,即dp[i][j] = max(dp[i-1][j], dp[i-1][j-wi] + vi)。 最终,dp[n][W]即为问题的解,表示在前n个物品中选择,在背包容量为W时的最大总价值。 通过动态规划算法,可以在时间复杂度为O(nW)的情况下解决01背包问题。这种算法适用于物品数量较小且背包容量较小的情况,效率较高。 ### 回答3: 01背包问题是一个经典的动态规划问题,用来求解在背包容量有限的情况下,如何选择物品放入背包使得总价值最大化。 问题可以描述为:给定n个物品,每个物品有一个重量和一个价值,以及一个容量为W的背包。要求在不超过背包容量的情况下,选取若干个物品放入背包,使得被选取的物品的总价值最大。 定义一个二维数组dp[n+1][W+1],其中dp[i][j]表示前i个物品中,背包容量为j时的最大总价值。 边界条件是dp[0][j] = 0,表示没有物品可选时,背包的总价值为0;和dp[i][0] = 0,表示背包容量为0时,无法选择任何物品,总价值也为0。 对于每一个物品i,有两种选择:放入背包或不放入背包。如果放入背包,则总价值为dp[i-1][j-w[i]] + v[i],其中w[i]是第i个物品的重量,v[i]是第i个物品的价值。如果不放入背包,则总价值为dp[i-1][j]。根据这两种选择,可以得到状态转移方程: dp[i][j] = max(dp[i-1][j-w[i]] + v[i], dp[i-1][j]) 最后,dp[n][W]即为问题的解,即前n个物品,在容量为W的背包中,所能达到的最大总价值。 综上所述,C 01背包问题模板的实现可以通过动态规划思想,并利用一个二维数组来保存状态值,最后输出dp[n][W]作为问题的解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值