背包问题

背包问题

1、01背包

题目大意:

​ 有n个物品,一个容量为m的背包,每个物品拿或者不拿,问拿到的最大总价值是多少。

思路:

dp三步走:dp数组含义 + 状态转移 + 初始化

​ 首先,我们将dp[i][j]定义为在前i件物品中,用容量为j的背包去装东西,能装到的最大价值。其次考虑状态转移:如果不拿第i件物品,我们就直接将前i-1件物品的状态转移过来;如果要拿第i件物品,我们就需要在背包中预留出第i件物品的体积(这样才能装得下)代码如下:

for(int i = 1 ; i <= n ; i ++ ){//枚举物品
	    cin>> v>> w ;
	    for(int j = 1 ; j <= m ; j ++ ){//枚举背包体积
	        dp[i][j] = dp[i - 1][j] ;//装不下直接转移上一个物品的状态
	        if(j >= v)dp[i][j] = max(dp[i][j] , dp[i - 1][j - v] + w) ;//装得下
	    }
	       
	}
cout<<dp[n][m] ;

​ 我们观察01背包的过程,发现其枚举每一个i,都要用到i-1对应的所有状态,而且对每一种容量j都只用到j-v的容量状态。于是我们就可以进行一维数组滚动优化 。我们只要从后往前枚举j即可(每一次遍历i对应的容量j都只要用前一层的数据,而前一次的数据在这之前已经算好了):

for(int i = 1 ; i <= n ; i ++ ){
    cin >> v >> w ;
    for(int j = m ; j >= v ; j -- )//一维滚动优化
        dp[j] = max(dp[j] , dp[j - v] + w) ;//直接转移(不拿i) 和 拿i 两种状态
}
cout<<dp[m] ;

2、完全背包

题目大意:

​ 有n个物品,一个容量为m的背包,每个物品可以选无限次,问拿到的最大总价值是多少。

思路:

dp三步走:dp数组含义 + 状态转移 + 初始化

​ 1、dp数组含义:dp[i][j]表示在前i个(包括i)物品里选若干个把容量为j的背包塞到塞不下所拿到的最大价值。

​ 2、初始化:前0个物品里拿0的容量显然dp[0][0] = 0 。

​ 3、状态转移:在拿第i个物品时可以取0件、2件、3件等等,直到背包塞不下为止,所以在取k个第i个物品时要先预留出k * v的空间。所以:

dp[j] = max(dp[j] , dp[j - k * v] + k * w) ;

有01背包代码:

    for(int i = 1 ; i <= n ; i ++ ){
        cin >> v >> w ;
        for(int j = m ; j >= v ; j -- )//一维滚动优化
            dp[j] = max(dp[j] , dp[j - v] + w) ;
    }
    cout<<dp[m] ;

​ 根据01背包的取和不取 ,我们可以将完全背包的每一个物品分成取0件、2件、3件等等,直到背包塞不下为止。这样朴素的想法就可以让我们写出完全背包的朴素解法(n^3):

 for(int i = 1 ; i <= n ; i ++ ){
        cin>>v>>w ;
        for(int j = m ; j >= v ; j -- )
            for(int k = 1 ; j - k * v >= 0 ; k ++ )
                dp[j] = max(dp[j] , dp[j - k * v] + k * w) ;
    }
    cout<<dp[m] ;

n^2优化:

​ 该解法对于前i个物品容量为j,有下图中第一个式子。对于前i个物品,容量为j-v时有下图中第二个式子。两个式子联立就变成了完全背包的状态转移方程(绿色字)。

​ 于是乎,代码进化了:

for(int i = 1 ; i <= n ; i ++ ){
    cin>>v>>w ;
    for(int j = v ; j <= m ; j ++ )
        dp[j] = max(dp[j] , dp[j - v] + w) ;
}

3、多重背包

题目大意:

​ 有n个物品,一个容量为m的背包,每个物品最多选s次,问拿到的最大总价值是多少。

思路:

​ 和多重背包的朴素做法思路一致,只是枚举每一件物品的结束条件发生了变化,从一直拿拿到拿不下变成一直拿拿到拿完s件或者拿不下。结合完全背包的思路,朴素做法代码就很好写:

for(int i = 1 ; i <= n ; i ++ ){
    cin>>v>>w>>s ;
    for(int j = m ; j >= v ; j -- )
    for(int k = 1 ; k * v <= j && k <= s ; k ++ )//拿不下或者拿完
    	dp[j]=max(dp[j] , dp[ j - v * k ] + w * k );
}
cout<<dp[m];

​ 朴素做法在时间复杂度上是很劣的,物品数 * 体积 * 件数 ,n^3的复杂度是比较难受的,所以需要对件数进行二进制优化 。下面提出一个问题:

给定数字x,如果要确定n个数字,并用其子集的和来表示[0,x]这个区间内的所有数,n最少是多少?

​ 分两种情况讨论:

​ 1、x是2的几次方:例如x==32 ,那么我们只要用1,2,4,8 ,16,32就能表示[0,32]的所有数了。

​ 2、x不是2的几次方:例如x==29,那么我们只要用1,2,4,8,14就能表示[0,29]的所有数了。

所以,我们只要对“1,2,4,8,14”这五个数做一次01背包就能做出[0,29]一个一个遍历的效果。这个思想体现到代码上就是对s件进行分块,分成ceil(log2(s))块 。分好块后对这些所有块进行01背包。

//因为pair的写法有定义所以放了完整代码
#include<bits/stdc++.h>
#define v first
#define w second
using namespace std ;
const int N = 2 * 1e3 + 50 ;

vector<pair<int ,int> >a ;
int n , m ;
int dp[N] ;

int main(){
	cin>>n>>m ;
	
	for(int i = 1 ; i <= n ; i ++ ){
		int tmpv , tmpw , tmps ;
		cin>>tmpv >> tmpw >> tmps ;
		for(int j = 1 ; j <= tmps ; j <<= 1){//二进制优化
			a.push_back({tmpv * j , tmpw * j}) ;//将第i种商品分块,若干个打包在一起
			tmps -= j ;
		}
		if(tmps)a.push_back({tmpv * tmps , tmpw * tmps}) ;
	}
	
	
	for(auto x : a)//分块后进行01背包
	for(int j = m ; j >= x.v ; j -- ){
		dp[j] = max(dp[j] , dp[j - x.v] + x.w) ;
	}
	
	cout<<dp[m] ;
}

4、分组背包

题目大意:

​ 有n组物品,容量为m的背包,每一组物品有s个,一组里最多拿一个,问拿到的最大价值是多少。

思路:

​ 在做01背包的时候,我们对每一种背包容量j做了拿或不拿第i个物品的决策:

dp[j] = max(dp[j] , dp[j - v] + w) ;

​ 而在分组背包中 ,我们则是要对每一种容量j,对拿第i组里的第k个做决策。那就把每一种情况拿出来比较一下:

dp[j] = max(dp[j] , dp[j - v[k]] + w[k]) ;

​ 因为是对每一种容量j做决策,所以在对于每一个j,最多是拿了该组中的一个物品(也可能没有拿),这是符合分组背包“一组拿一个”的要求的。

for(int i = 1 ; i <= n ; i ++ ){
		
	cin>>s ;
	for(int j = 1 ; j <= s ; j ++ )cin>> v[j] >> w[j] ;
		
	for(int j = m ; j >= 0 ; j -- )//枚举容量
		for(int k = 1 ; k <= s ; k ++ )
            //对前i组物品,每一种容量,拿第几个做决策
			if(j >= v[k])
                dp[j] = max(dp[j] , dp[j - v[k]] + w[k]) ;
	}
cout<<dp[m] ;

分组背包题外话:

​ 对着上面的代码想一想,上面是先枚举容量再枚举组中的第k个物品的。如果我们先枚举第i组中的物品再去枚举容量j,会出现什么情况?

​ 那就是说,如果我们在拿了第i组物品中的第1个物品后,还是可以拿第i组物品的第2个物品的。这样就是把题中分的n组物品打散了变成n*s[i]个物品的01背包问题。

梳理和小结:

有点啰嗦

题意:

0 1背包:n个物品拿或不拿

完全背包:n个物品拿无限次

多重背包:n个物品最多拿s次

分组背包:n组物品,一组里最多拿一个

四种背包的数组意义都是:前i个(组)物品中,用容量为j的背包去装,最多装的价值。而做的过程都是枚举容量j,然后根据题目要求去比较拿或不拿或拿若干次谁比较优。

过程和小细节:


0 1背包 : 对前i个物品,枚举容量j。对容量j,枚举拿和不拿第i个物品谁比较优。(一维数组滚动优化

完全背包:对前i个物品,枚举容量j。对容量j,枚举拿k个第i个物品谁比较优,k一直取到装满背包。

多重背包:对前i个物品,枚举容量j。对容量j,枚举拿k个第i个物品谁比较优,k取到s或者装满背包(二进制优化

分组背包:对前i组物品,枚举容量j。对容量j,枚举第i组中拿第k个或不拿谁比较优。
相信聪明的你已经发现了,对i和j的枚举都是一样的,关键是对物品的决策。这一点需要紧扣题意去完成。

总结:

​ 动态规划问题要紧扣题意,确定好数组意义,状态转移 和 初始化的设置。而且总是从小状态转移到大状态,这和记忆化搜索递归过程的“归”过程比较相似。最后感谢y总的讲解,帮助我解决了很多疑惑。

posted @ 2021-10-24 22:33  tyrii  阅读(124)  评论(0)    收藏  举报