背包问题
背包问题
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总的讲解,帮助我解决了很多疑惑。

浙公网安备 33010602011771号