常规类型问题描述
有多种物品,和一个最大承重量为w的背包,每种物品有weight和value,问将这些物品放入背包中(每种物品可以放多个),背包的最大价值是多少。
与0-1背包不同的是,0-1背包每件物品只能放一次,而完全背包每种物品可以无限多地放。
解决方法
状态转移方程:dp[i , j] = max{dp[i-1 , j] , dp[i , j - weight[i]] + value[i]}
优化后:dp[j] = max{dp[j] , dp[j - weight[i]] + value[i]}
注意与0-1背包不同的是方程右边是dp[i , j - weight[i]] + value[i]。
上代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26#include<iostream>
using namespace std;
int n,W;
int w[1000],v[1000];
int dp[10000];
int main()
{
freopen("input.txt","r",stdin);
//freopen("output.txt","w",stdout);
cin>>n>>W;
for(int i=0;i<n;i++){
cin>>w[i]>>v[i];
}
for(int i=0;i<n;i++){
for(int j=w[i];j<=W;j++){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
for(int j = 0 ; j<=W ; ++j){
cout<<dp[j]<<' ';
}
cout<<endl;
}
cout<<dp[W]<<endl;
return 0;
}
完全背包的内层循环是正序的,而0-1背包是逆序的,为什么呢?
我们先来理解一下完全背包的状态转移方程:dp[i , j] = max{dp[i-1 , j] , dp[i , j - weight[i]] + value[i]},在放i物品时,我们还是要考虑放还是不放?在什么基础上放?
如果是0-1背包,那么在放i物品时,由于一个物品只能放一次,所以它的基础是前面i-1个物品所更新好的背包,即j背包前面的背包的状态必须是i-1;
而如果是完全背包,在背包j里放物品i时,它的基础却是状态i的背包,即比j容量小的背包是已经被i更新过的,为什么呢?
比如容量10的背包,放weight=3的物品,那么我们只需要考虑在容量为7的背包里放weight=3的物品就好了,但不是前面说是一种物品可以放多个么,容量10的背包可以放3个weight=3的物品吧,为什么只放了一个呢?但是,由于内层循环是正序的,所以背包7的基础是4,背包4的基础是1,我们在背包4里放了一个3,在背包7里又放了一个,而背包7的基础是4,所以相当于放了2个3,所以到背包10时,想当于放了3个3,这就是为什么完全背包要正序的原因。
例题:hdu 2159 FATE
http://acm.hdu.edu.cn/showproblem.php?pid=2159
我们把耐久度当成weight,经验值当成value,求满足经验值大于n,次数小于s的最小耐久度1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
int main()
{
//freopen("input.txt","r",stdin);
//freopen("output.txt","w",stdout);
int n , m , k , s;
while(cin>>n>>m>>k>>s){
int weight[110] , value[110] , dp[110] , num[110];
memset(weight , 0 , sizeof(weight));
memset(value , 0 , sizeof(value));
memset(dp , 0 , sizeof(dp));
memset(num , 0 , sizeof(num));
for(int i = 0 ; i<k ; ++i){
cin>>value[i]>>weight[i];
}
for(int i = 0 ; i<k ; ++i){
for(int j = weight[i] ; j<=m ; ++j){
if(dp[j] < dp[j - weight[i]] + value[i]){
dp[j] = dp[j - weight[i]] + value[i];
num[j] = num[j - weight[i]] + 1;
}
}
}
int flag = 0;
for(int i = 0 ; i<=m ; ++i){
if(num[i] <= s && dp[i] >= n){
cout<<m - i<<endl;
flag = 1;
break;
}
}
if(!flag){
cout<<-1<<endl;
}
}
}
其实还可以用二维完全背包做,把耐久度和次数都当做weight,经验当做value,每个怪物的次数这个weight都是1,题目要求输出的是最小耐久度,所以我们最终输出最大的次数,最小的>n的经验值的情况下的最小耐久度,其中最大次数保证了最小耐久度下能获得的最大经验值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
int main()
{
//freopen("input.txt","r",stdin);
//freopen("output.txt","w",stdout);
int n , m , k , s;
while(cin>>n>>m>>k>>s){
int weight[110] , value[110] , dp[110][110] , num[110];
memset(weight , 0 , sizeof(weight));
memset(value , 0 , sizeof(value));
fill(dp[0] , dp[0] + 110 * 110 , 0);
memset(num , 0 , sizeof(num));
for(int i = 0 ; i<k ; ++i){
cin>>value[i]>>weight[i];
}
for(int i = 0 ; i<k ; ++i){ //一种物品一种物品地放
for(int j = weight[i] ; j<=m ; ++j){ //考虑耐久度
for(int h = 1 ; h<=s ; ++h){ //考虑次数
dp[j][h] = max(dp[j][h] , dp[j - weight[i]][h-1] + value[i]);
}
}
}
if(dp[m][s] >= n){
for(int i = 1 ; i<=m ; ++i){
if(dp[i][s] >= n){
cout<<m-i<<endl;
break;
}
}
}
else cout<<-1<<endl;
}
}
求装特定重量的装法总数
- 给一个特定的容量,必须装满
- 与装的顺序无关
- 每个物品可以装无限多次
1 | dp[0] = 1 ;必须初始化,其他为0 |
其中$dp[i][j]$表示考虑前$i$件物品而且装满容量$j$的背包的装法总数。
所以状态转移函数为
也就是说,装法总数 = 我不装这个物品时的装法总数 + 装这个物品时的装法总数。
变种:背包需要装满
常规的完全背包是只要背包能装得下就往里面装,现在是背包必须装满,我们设$dp(i,j)$表示前$i$个物品来装满容量为$j$的背包的最大价值,那么我们的递推公式:
就必须保证$j - weight[i]$的背包是满的。
1 | int main() |
