- 01背包
问题:若干个物体,每个价值为c[i]重量为w[i],数量均为1,背包最大容量为W,问怎样取物体才能最大化收益?
解法:dp[i][j]以j为容量为放入前i个物品(按i从小到大的顺序)的最大价值。有递推关系式:dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+c[i])。这里的第一维度可以省略。
注意这里的内存重量循环j为W到0,递减关系(保证每个物体取一遍)(后面的不会修改前面的)
当背包空间j少于当前物体的重量时不选该物体这一步可以省略,对后续没有影响。
最终写法为:
1 for(int i=1; i<=m; i++) //物品 2 for(int j=W; j>=0; j--) //容量 3 { 4 if(j >= w[i]) 5 dp[i][j] = max(dp[i-1][j-w[i]]+val[i], dp[i-1][j]); 6 else //只是为了好理解 7 dp[i][j] = dp[i-1][j]; 8 }
空间优化:
1 for(i=1;i<=m;i++){ //尝试放置每一个物品 2 for(j=t;j>=w[i];j--){//倒叙是为了保证每个物品都使用一次 3 f[j]=max(f[j-w[i]]+v[i],f[j]); 4 //在放入第i个物品前后,检验不同j承重量背包的总价值,如果放入第i个物品后比放入前的价值提高了,则修改j承重量背包的价值,否则不变 5 } 6 }
- 完全背包
问题:若干个物体,每个价值为c[i]重量为w[i],数量均为无限,背包最大容量为W,问怎样取物体才能最大化收益?
解法:dp[i][j]以j为容量为放入前i个物品(按i从小到大的顺序)的最大价值。有递推关系式:dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+c[i])。这里的第一维度可以省略。
注意这里的内存重量循环j为w[i]到W,递增关系(保证每个物体取一遍)(后面的不会修改前面的)
最终写法为:
1 for(int i=1; i<=n; i++) 2 for(int j=w[i]; j<=W; j++)//注意此处,与0-1背包不同,这里为顺序,0-1背包为逆序 3 f[j]=max(f[j],f[j-w[i]]+c[i]);
例题:POJ2229
题意:
找一些2^x(0<=x),使它们的和为N。比如,N=7:
1) 1+1+1+1+1+1+1
2) 1+1+1+1+1+2
3) 1+1+1+2+2
4) 1+1+1+4
5) 1+2+2+2
6) 1+2+4
(1 <= N <= 1,000,000).
解法:直接完全背包即可,注意关系式更新的时候为“拿”的情况加上“不拿”的情况;注意初始化dp[0]=11 int N; 2 int num[25]; 3 int dp[1000000 + 5]; 4 int main() { 5 // freopen("input.txt", "r", stdin); 6 scanf("%d", &N); 7 num[1] = 1; 8 dp[0] = 1; 9 for (int i = 2; i <= 25; i++) num[i] = num[i - 1] * 2; 10 for (int i = 1; i <= 21; i++) { 11 for (int j = num[i]; j <= N; j++) { 12 dp[j] = (dp[j] + dp[j - num[i]]) % MOD; 13 } 14 } 15 printf("%d ", dp[N]); 16 return 0; 17 }
- 多重背包
多重背包里面每个物品的数量是有限的,即k个,因此基本的求解思路是将其转换为01背包再求解;或者枚举k的个数。
v[i],w[i],与c[i],分别表示这种物品的体积、价值和件数。
1 for (int i = 1; i <= n; i++) 2 for (int j = m; j >= c[i]; j--) 3 for (int k = 0; k <= num[i]; k++) 4 if (j - c[i] * k >= 0) 5 f[j] = max(f[j], f[j - c[i] * k] + v[i] * k);
当然直接这样做一定会超时,其复杂度为$O(Vsum_{}^{}n[i])$。
优化方法第一种是改变分割方法(二进制),即:将第 i 种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 $1 , 2 , 4 ,..., 2^{k-1}, n[i]-2^{k}+1$,且k 是满足 n[i] - 2^{k}+1>0 的最大整数。例如,如果 n[i] 为13,就将这种物品分成系数分别为 1 , 2 , 4 , 6 的四件物品。
分成的这几件物品的系数和为 n[i],表明不可能取多于 n[i] 件的第 i 种物品。另外这种方法也能保证对于 0 .. n[i] 间的每一个整数,均可以用若干个系数的和表示,这个证明可以分 0.. 2^{k}-1 和 2^{k}...n[i] 两段来分别讨论得出。
这样就将第i种物品分成了 O(logn[i]) 种物品,将原问题转化为了复杂度为 O(V*∑log n[i]) 的01背包问题。1 for (int i = 1; i <= n; i++) { 2 int x = read(), y = read(), z = read(); 3 int base = 1; 4 while (z >= base) { 5 c[++tot] = x * base; 6 v[tot] = y * base; 7 z -= base; 8 base <<= 1; 9 } 10 if (z > 0) { 11 c[++tot] = z * x; 12 v[tot] = z * y; 13 } 14 } 15 for (int i = 1; i <= tot; i++) 16 for (int j = m; j >= c[i]; j--) 17 f[j] = max(f[j], f[j - c[i]] + v[i]);
优化方法第二种是采用优先队列的方法,推导方法及其神奇,看不懂了,挂个链接
复杂度为 O(n∗V)1 for(int i=1;i<=n;i++)//枚举物品种类 2 { 3 cin>>c[i]>>w[i]>>num[i];//c,w,num分别对应 体积,价值,个数 4 if(V/c[i] <num[i]) num[i]=V/c[i];//求lim 5 for(int mo=0;mo<c[i];mo++)//枚举余数 6 { 7 head=tail=0;//队列初始化 8 for(int k=0;k<=(V-mo)/c[i];k++) 9 { 10 int x=k; 11 int y=f[k*c[i]+mo]-k*w[i]; 12 while(head<tail && que[head].pos<k-num)head++;//限制长度 13 while(head<tail && que[tail-1].value<=y)tail--; 14 que[tail].value=y,que[tail].pos=x; 15 tail++; 16 f[k*c[i]+mo]=que[head].value+k*w[i]; 17 //加上k*w[i]的原因: 18 //我们的单调队列维护的是前i-1种的状态最大值. 19 //因此这里加上k*w[i]. 20 } 21 } 22 }
POJ1742
例题:
题意:有 n 种面额的硬币,面额个数分别为v[i]、num[i],求最多能搭配出几种不超过 m 的金额?
解法:没有时间、空间优化都会炸。所以采用多重背包加上规则定义求解,定义如下:
①首先来看看朴素的方法:
bool dp[i][j] := 用前 i 种硬币能否凑成j
递推关系式:
dp[i][j] = (存在 k 使得 dp[i – 1][j – k * v[i]] 为真,0 <= k <= C_i 下标合法)
然后三重循环 i j k 递推
这样的话复杂度为O(m*ΣC_i),肯定过不去。。
②然后我们想到将问题转化为01背包,并利用二进制(具体可以看背包九讲)来优化
复杂度为O(m*ΣlogC_i),仍然TLE。。
③我们优化dp的状态:
状态:dp[i][j] : = 用前 i 种硬币凑成 j 时第 i 种硬币最多能剩余多少个( - 1表示配不出来)
转移:
①若 $dp[i-1][j]>=0$,即前 $i-1$ 种可以配成 $j$,所以根本用不到第$i$种,所以剩余$nun[i]$种 $dp[i][j]=nun[i]$
②若$j<a[i] || dp[i][j-a[i]]<=0$,由于$dp[i-1][j]<0$,所以要想配成$j$起码得要有第$i$种,若$j<a[i]$则第$i$种用不到,所以前$i$种仍配不到$j$,若$dp[i][j-a[i]]<=0$,则说明配成$j-a[i]$时第$i$种已经无剩余或者甚至无法配成$j-a[i]$,更别说配成$j$了, $dp[i][j]=-1$
③其他情况,由于$a[i]$还有剩,所以$dp[i][j]$相当于在$dp[i][j-a[i]]$的基础上多使用了一个$a[i]$,此时 $ dp[i][j]=dp[i][j-a[i]]-1$
1 int v[106], num[106]; 2 int dp[MAXN]; 3 int n, m; 4 int main() { 5 // freopen("input.txt", "r", stdin); 6 while (scanf("%d%d", &n, &m) && n && m) { 7 CLR(v), CLR(num); 8 SET(dp); 9 REP(i, 1, n) v[i] = READ(); 10 REP(i, 1, n) num[i] = READ(); 11 dp[0] = 0; 12 int ans = 0; 13 REP(i, 1, n) REP(j, 0, m) { 14 if (dp[j] >= 0) 15 dp[j] = num[i]; 16 else if (j < v[i] || dp[j - v[i]] < 0) 17 dp[j] = -1; 18 else 19 dp[j] = dp[j - v[i]] - 1; 20 if (i == n && j > 0) ans += dp[j] >= 0; 21 } 22 int a = 1; 23 cout << ans << endl; 24 } 25 26 return 0; 27 }