01背包
这是最简单的一种背包,因为对于每一件物品都只有放和不放两种情况,故叫01背包。
所以状态转换方程
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + v[i]);
dp[i][j]指有i个物品,在j个空间中能存放的最大价值
dp[i - 1][j]指第i个物品不放进背包,即和i- 1的情况相同
dp[i - 1][j - c[i]] + v[i]指第i个物品放进背包,那么就是j个空间中减去第i个物品所占的空间c[i],这种情况下在再加上它的价值v[i]
例题:
描述
辰辰是个很有潜能、天资聪颖的孩子,他的梦想是称为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入
输入的第一行有两个整数T(1 <= T <= 1000)和M(1 <= M <= 100),T代表总共能够用来采药的时间,M代表山洞里的草药的数目。接下来的M行每行包括两个在1到100之间(包括1和100)的的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出
输出只包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
样例输入
70 3
71 100
69 1
1 2
样例输出
3
代码
1 #include<cstdio> 2 #include<iostream> 3 using namespace std; 4 int dp[1005][1005], t[1005], v[1005]; 5 int T, M; 6 int main() 7 { 8 scanf("%d%d", &T, &M); 9 for(int i = 1; i <= M; ++i) 10 scanf("%d%d", &t[i], &v[i]); 11 for(int i = 0; i <= T; ++i) dp[0][i] = 0; //初始化 12 for(int i = 1; i <= M; ++i) 13 { 14 dp[i][0] = 0; //初始化 15 for(int j = T; j >= 0; --j) 16 { 17 dp[i][j] = dp[i - 1][j]; 18 if(j - t[i] >= 0) //如果不判断,数组下标会出负数 19 dp[i][j] = max(dp[i][j], dp[i - 1][j - t[i]] + v[i]); 20 } 21 } 22 printf("%d\n", dp[M][T]); 23 return 0; 24 }
以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V),即改成一维数组
#include<cstdio> #include<iostream> using namespace std; int dp[1005], t[1005], v[1005]; int T, M; int main() { scanf("%d%d", &T, &M); for(int i = 1; i <= M; ++i) scanf("%d%d", &t[i], &v[i]); for(int i = 1; i <= M; ++i) for(int j = T; j >= t[i]; j--) dp[j] = max(dp[j], dp[j - t[i]] + v[i]); printf("%d\n",dp[T]); return 0; }
其中内层循环最好要从后往前,因为dp[j]一定是从前面更新来的。若是正着来,那么一个物品就可能被放进去多次。
多重背包
多重背包就是每一件物品都有多个(但是并不是无限的),求最大价值。
思路:转换为01背包。如:有容量为3,价值为5的物品12个,那么就可看成有12个物品。但时间复杂度会相当的大。
改进:
先看一道题:一个数n=63,将n分成6个数之和,使这6个数可以组合成从0到n之间任意的一个数,问这6个数是多少。
分析:我们发现,2^6-1=63,即63的二进制为111111,所以6个数分别为1,2,4,8,16,32。根据二进制运算,这几个数可组成0到111111之间任意一个数。
那么若n=60怎么办呢?
其实就是把最后一个数32-3。
所以对于多重背包问题,相同种类的物品就可进行合并:个数不断减2^n(n++),直到不够减为止,则剩下的就是最后一个数。
如:有容量为3,价值为5的物品12个。因为12=1+2+4+5,所以可转换为4个容量和价值分别为(3,5),(6,10),(12,20),(15,25)的物品。
代码
1 #include<cstdio> 2 #include<iostream> 3 using namespace std; 4 const int maxn = 10005; 5 int dp[maxn], c[maxn], v[maxn]; //c为容量,v为价值 6 int m, n, q = 0; //m为背包体积,n为物品种类, q记录合并后的物品数 7 void hebin(int C, int V,int GS) 8 { 9 int x = 1; 10 while(GS - x > 0) 11 { 12 c[++q] = x * C; 13 v[q] = x * V; 14 GS -= x; 15 x *= 2; 16 } 17 c[++q] = GS * C; 18 v[q] = GS * V; 19 return; 20 } 21 int main() 22 { 23 scanf("%d%d", &m ,&n); 24 for(int i = 1; i <= n; ++i) 25 { 26 int cc, vv, gs; //gs为一种物品的个数 27 scanf("%d%d%d", &cc, &vv, &gs); 28 hebin(cc, vv, gs); 29 } 30 for(int i = 1; i <= q; ++i) 31 { 32 for(int j = m; j >= 0; --j) 33 { 34 if(j - c[i] >= 0) 35 dp[j] = max(dp[j], dp[j - c[i]] + v[i]); 36 } 37 } 38 printf("%d\n", dp[m]); 39 return 0; 40 }
如此就将第i种物品分成了O(logn[i])种物品,将原问题转化为了复杂度为O(V*Σlog n[i])的01背包问题,是很大的改进。
完全背包
其实就是每个物品有无限个,求最大价值。
思路:转化为多重背包,进而01背包。
可用所给的背包最大容量来确定每个物品的最大数量。
如:背包容量为10,有一个物品所占容量为3,那么它最多有10/3=3个。
这样就转化成了多重背包。
代码
1 #include<cstdio> 2 #include<iostream> 3 using namespace std; 4 const int maxn = 10005; 5 int dp[maxn], c[maxn], v[maxn]; 6 int m, n, q = 0; 7 void hebin(int C, int V, int M) //就这和多重背包不一样,M是总容量 8 { 9 int x = 1; 10 while(M - x * C >= 0) 11 { 12 c[++q] = x * C; 13 v[q] = x * V; 14 M -= x * C; 15 x *= 2; 16 } 17 if(M >= C) //处理余下的同类物品 18 { 19 c[++q] = M / C * C; 20 v[q] = M / C * V; 21 } 22 return; 23 } 24 int main() 25 { 26 scanf("%d%d", &m, &n); 27 for(int i = 1; i <= n; ++i) 28 { 29 int cc, vv; 30 scanf("%d%d", &cc, &vv); 31 hebin(cc, vv, m); 32 } 33 for(int i = 1; i <= q; ++i) 34 printf("c = %d v = %d\n", c[i], v[i]); 35 for(int i = 1; i <= q; ++i) 36 { 37 for(int j = m; j >= 0; --j) 38 { 39 if(j - c[i] >= 0) 40 dp[j] = max(dp[j], dp[j - c[i]] + v[i]); 41 } 42 } 43 printf("%d\n", dp[m]); 44 return 0; 45 }
其实基本思路并不是这个。
不过别担心,这种解法比基本思路要快。但是为了完整性,我还是讲讲基本思路吧。
先上段伪代码
1 for(i...n) 2 for(j...m) //m是背包容量 3 { //c[]是物品容量,v[]是物品价值 4 if(j - c[i] > 0) 5 dp[j] = max(dp[j], dp[j - c[i]] + v[i]) 6 } 7 printf("%d\n", dp[m]);
觉得熟悉?
没错,它和01背包就差个内层循环,一个正着,一个倒着。那为什么这么一改,就从01背包转化为完全背包了呢?
首先我们要清楚的是,因为dp[j] = dp[j] 或 dp[j - c[i]] + v[i] ,所以每一次dp,要么保持不变,要么用前面的更新后面的。如果从0到m循环,也就可能导致在dp前i个物品时第i个物品被放进去了多次,那就不满足每一个物品只有1个,即01背包了。相反的,这不就是完全背包的解法吗?
水一段代码
#include<cstdio> #include<iostream> using namespace std; int dp[1005], c[1005], v[1005]; int m, n; int main() { scanf("%d%d", &m, &n); for(int i = 1; i <= n; ++i) scanf("%d%d", &c[i], &v[i]); for(int i = 1; i <= n; ++i) //是的,就这改了 { for(int j = 0; j <= m ; ++j) { if(j - c[i] >= 0) dp[j] = max(dp[j], dp[j - c[i]] + v[i]); } } printf("%d\n",dp[m]); return 0; }
分组背包
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。
思路:对于每一组,只有选其中一件,或什么都不选两种选择。
不选: dp[v] = dp[v]
选一组中的第i个:dp[v] = dp[v - c[i]] + w[i]
将两者比较,取最大
(v指当前容量为v的情况下)
伪代码:
1 for 所有的组k 2 for v=V..0 3 for 所有的i属于组k 4 f[v]=max{f[v],f[v-c[i]]+w[i]}
例题:P1757 通天之分组背包
题目描述
自01背包问世之后,小A对此深感兴趣。一天,小A去远游,却发现他的背包不同于01背包,他的物品大致可分为k组,每组中的物品相互冲突,现在,他想知道最大的利用价值是多少。
输入输出格式
输入格式:
两个数m,n,表示一共有n件物品,总重量为m
接下来n行,每行3个数ai,bi,ci,表示物品的重量,利用价值,所属组数
输出格式:
一个数,最大的利用价值
上代码
1 #include<cstdio> 2 #include<iostream> 3 #include<cmath> 4 #include<algorithm> 5 using namespace std; 6 const int maxn = 1005; 7 int m, n, maxk = -1; //m为背包容量,n为物品数 8 int dp[maxn], c[105][maxn], v[105][maxn], zu[105]; 9 int main() //zu[]记录每一组中有多少个物品 10 { 11 scanf("%d%d", &m, &n); 12 for(int i = 1; i <= n; ++i) 13 { 14 int ci, vi, k; 15 scanf("%d%d%d", &ci, &vi, &k); 16 c[k][++zu[k]] = ci; v[k][zu[k]] = vi; 17 if(k > maxk) maxk = k; //记录最大组数 18 } 19 for(int i = 1; i <= maxk; ++i) 20 for(int j = m; j >= 0; --j) 21 for(int q = 1; q <= zu[i]; ++q) 22 if(j - c[i][q] >= 0) 23 dp[j] = max(dp[j], dp[j - c[i][q]] + v[i][q]); 24 printf("%d\n", dp[m]); 25 return 0; 26 }