【算法基础20】背包问题大盘点(01背包、完全背包、多重背包、分组背包)

 一、01背包问题

        问题描述:给定n件物品,容量为m的背包,在每件物品最多只能装一个的情况下,求背包所能装下的最大价值。

        主要思想:用数组v[ i ]存储物品 i 的价值,数组w[ i ] 存储物品 i 的重量,数组 f[ i ][ j ] 表示在只考虑前 i 件物品,背包容量为 j 的情况下,背包所能装下的最大价值。f[ i ][ j ]在 f[ i-1 ][ j ]的基础上考虑,有装和不装两种情况:要么第 i 件物品不装进背包,f[ i ][ j ] = f[ i-1 ][ j ];要么第 i 件物品装进背包,f[ i ][ j ] = max(f[ i ][ j ],f[ i-1 ][ j - v[ i ]] + w[ i ])。通过这种递归关系可以求出范围内各种组合的最大价值,f[ n ][ m ]即为最终答案

        代码:

        ①二维表示

#include<iostream>
#include<algorithm>
using namespace std;

const int N=1010;
int v[N],w[N],f[N][N];

int main(){
	int n,m;
	cin>>n>>m;
	
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			f[i][j]=f[i-1][j];
			if(j>=v[i]) //第i件物品可以装进再执行
                f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);//取装和不装两种结果的最大价值
		}
	}
	
	cout<<f[n][m]<<endl;
	
	return 0;
}

        ②一维表示

#include<iostream>
#include<algorithm>
using namespace std;

const int N=1010;
int v[N],w[N];
int F[N];

int main(){
	int n,m;
	cin>>n>>m;
	
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	
	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;
} 

        注:由于一维数组用到了滚动数组的概念(即新一层的内容只和上一层有关,可以只定义两层,每次进行滚动覆盖),若是正序遍历从左往右更新,左边的F已经被更新成第 i 层,而递归公式中用到的是第 i-1层的值。逆序遍历从右往左更新,能够保证左边的F还没有被更新,仍然是第 i-1 层的值。 j < v[ i ]时不需要做改变,可以不用遍历,循环条件只到 j >= v[ i ]

二、完全背包问题

        问题描述:给定n件物品,容量为m的背包,在每件物品能装无数个的情况下,求背包所能装下的最大价值。

        主要想思:f[ i ][ j ]在 f[ i-1 ][ j ]的基础上考虑,第 i 个物品可以装0个、1个、2个……k个,取k种装法中价值最大的一种,即 f[ i ][ j ] = max( f[ i ][ j ] , f[ i-1 ][ j - v[ i ] * k ] + w[ i ] * k ),朴素做法为增加一层循环,遍历k能取的范围,但三重循环计算时间太大。由f[ i ][ j ] = f[ i ][ j-v[ i ] ] + w[ i ]的递归关系,可以得出f[ i ][ j ] = max( f[ i ][ j ] , f[ i ][ j - v[ i ] ] + w[ i ] )。

        注意完全背包递推公式与01背包递推公式的区别,完全背包递推公式是在第 i 层里找,01背包递推公式是在第 i-1 层里找

 

        代码:

        ①二维表示

#include<iostream>
#include<algorithm>
using namespace std;

const int N=1010;
int v[N],w[N],f[N][N];

int main(){
	int n,m;
	cin>>n>>m;
	
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	
	for(int i=1;i<=n;i++){
		for(int j=1;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]);	//注意是在第 i 层找		
		}
	}
	
	cout<<f[n][m]<<endl;
	
	return 0;
}

        ②一维表示

#include<iostream>
#include<algorithm>
using namespace std;

const int N=1010;
int v[N],w[N];
int F[N];

 int main(){
	int n,m;
	cin>>n>>m;
	
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	
	for(int i=1;i<=n;i++){
		for(int j=v[i];j<=m;j++){//注意是正序遍历
			F[j]=max(F[j],F[j-v[i]]+w[i]);			
		}
	}
	
	cout<<F[m]<<endl;
	
	return 0;
}

        注:由于完全背包递推公式是在第 i 层里找最大,只需要正序遍历即可,建议与01背包一维表示的逆序遍历对比理解。

三、多重背包问题

        问题描述:给定n件物品,容量为m的背包,在每件物品最多装s[ i ]个的情况下,求背包所能装下的最大价值。

        主要思想:多重背包问题相当于限制了k最大为s[ i ]的完全背包问题,可以在朴素完全背包问题解法的基础上增加限制求解,但三重循环容易超时。而由于限制了k的最大值,多重背包问题中f[ i ][ j ]与f[ i ][ j-v[ i ] ]的项数不匹配,不能直接套用完全背包问题的优化方法。

        可以采用二进制优化,把s[ i ]拆分成由1、2、4……2^{k}、c(c < 2^{k+1},为剩余的数)个物品组成的logs[ i ]组每组物品抽象成01背包问题,只能不拿或者全拿,不同拿法可以涵盖0到s[ i ]的所有个数。经过这样优化,可以从遍历s[ i ]次减少到遍历logs[ i ]次,能够把时间复杂度从NVS降低到NVlogS

        代码:

        ①限制k的朴素解法           ​​​​​​​        ​​​​​​​        

#include<iostream>
#include<algorithm>
using namespace std;

const int N=1010;
int v[N],w[N],s[N],f[N][N];

int main(){
	int n,m;
	cin>>n>>m;
	
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i];
	
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			for(int k=0;k<=s[i]&&k*v[i]<=j;k++)//限制k最大为s[ i ]
				f[i][j]=max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);		
		}
	}
	
	cout<<f[n][m]<<endl;
	
	return 0;
}

        ②二进制优化的一维表示

#include<iostream>
#include<algorithm>
using namespace std;

const int N=1010;
int v[N],w[N],s[N];
int F[N];

int main(){
	int n,m;
	cin>>n>>m;
	
	int cnt=0;//cnt最大为n*logs
	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;
			k*=2;//k为组内物品个数
		}

		if(s>0){//存由c个物品组成的最后一组
			cnt++;
			v[cnt]=a*s;
			w[cnt]=b*s;
		}
	}
	
	n=cnt;//一共有cnt组,相当于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]);//01背包问题的一维解法
		}
	}
	
	cout<<F[m]<<endl;
	
	return 0;
}

 

四、分组背包问题

        问题描述:给定n组物品,容量为m的背包,在同一组物品最多只能选一个的情况下,求背包所能装下的最大价值。

 

        主要思想:枚举第 i 组中选第几个物品时价值最大,f[ i ][ j ] = max( f[ i-1 ][ j ] , f[ i-1 ][ j-v[ i , k ] ] + w[ i , k ])

        代码:

#include<iostream>
#include<algorithm>
using namespace std;

const int N=1010;
int v[N][N],w[N][N],s[N];
int F[N];

int main(){
	int n,m;
	cin>>n>>m;
	
	for(int i=1;i<=n;i++){
		cin>>s[i];//存第i组的物品数量
		for(int k=1;k<=s[i];k++)
			cin>>v[i][k]>>w[i][k];
	}
	
	for(int i=1;i<=n;i++){
		for(int j=m;j>=0;j--){//在第i-1层找,逆序遍历
			for(int k=1;k<=s[i];k++){//遍历第i组的物品
				if(v[i][k]<=j)
					F[j]=max(F[j],F[j-v[i][k]]+w[i][k]);
			}
		}
	}
	
	cout<<F[m]<<endl;
	
	return 0;
}

参考资料:ACWing算法基础

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值