背包问题
//0 1背包
#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
int main()
{
int i,j,n,v,f[1100],w[1100],p[1100];
scanf("%d",&t);
while(t--)
{
memset(f,0,sizeof(f));
scanf("%d%d",&n,&v);
for(i=0;i<n;i++)
scanf("%d",&p[i]);
for(i=0;i<n;i++)
scanf("%d",&w[i]);
for(i=0;i<n;i++)
{
for(j=v;j>=w[i];j--)
f[j]=f[j]>(f[j-w[i]]+p[i])?f[j]:(f[j-w[i]]+p[i]);
}
printf("%d\n",f[v]);
}
return 0;
}
//完全背包
#include<stdio.h>
#define max 999999
int main()
{
int i,j,n,v,f[10010],w[10010],p[10010],t,a,b;
scanf("%d",&t);
while(t--)
{
scanf("%d%d%d",&a,&b,&n);
v=b-a;
for(i=1;i<=v;i++)
f[i]=max;
f[0]=0;
for(i=0;i<n;i++)
scanf("%d%d",&p[i],&w[i]);
for(i=0;i<n;i++)
for(j=w[i];j<=v;j++)
if(f[j]>f[j-w[i]]+p[i])
f[j]=f[j-w[i]]+p[i];
if(f[v]==max) printf("This is impossible.\n");
else
printf("The minimum amount of money in the piggy-bank is %d.\n",f[v]);
}
return 0;
}
01背包是指每个物品只能用一次
完全背包指的是每个物品都能无限使用
没什么技巧,DP类的明确了状态后就写起来快了,写多了程序自然也就有经验了,有经验就能快速的推出状态,总之就是多写题
程序的不同很简单
完全背包
for i:=1 to n do //枚举1-N的物品
for j:=a[i] to m do //枚举1-M的能背出来的范围
f[j]:=f[j] or f[j-a[i]];
01背包
for i:=1 to n do //枚举1-N的物品
for j:=m downto a[i] do //枚举1-M的能背出来的范围
f[j]:=f[j] or f[j-a[i]];
注意,之前要先f[0]:=true;
可以看出,区别只在第二个循环的正倒,F是一个布尔数组,F[i]表示i这个数字能够被组合出来
为什么循环的正倒会导致这样的区别呢?
我们举个例子:
假设我们现在组合出了3,现在讨论的物品体积是1,那么,即a[i]=1, f[3]:=true;
当正循环时
当枚举到4的时候,f[4]=f[4] or f[4-1]=true,因为f[3]=true;
当枚举到5的时候,f[5]=f[5] or f[5-1]=true,因为f[4]=true;
显然,这个“1”会被无限的使用下去直到到达M的上限
逆循环则相反
复制的
标注:下文n即为物品件数,c[i]表示第i件物品的耗费(体积),V为背包容量,a[i]表示第i件物品的价值 dp[]数组存放的即为最优解。
一、01背包
最简单的背包,每件物品选或者不选。
1 for(int i = 1; i <= n; ++i){ 2 for(int j = V; j >= c[i]; --j){ 3 dp[j] = max(dp[j],dp[j-c[i]]+a[i]); 4 } 5 }
二、完全背包
每件物品可以选无限次
1 for(int i = 1; i <= n; ++i){ 2 for(int j = c[i]; j <= V; ++j){ 3 dp[j] = max(dp[j],dp[j-c[i]] + a[i]; 4 } 5 }
注意:初始化方面的细节,如果要求恰好装满背包,那么在初始化时除了dp[0]为0其它dp[1..V]均设为-∞(求的是最大解,如果求的是最小解,则为∞) 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。
三、多重背包
每件物品可以选有限次 设每件物品的个数为count[i];
O(V*Σlog count[i])写法(二进制优化)
1 for(int i = 1; i <= n; ++i){ 2 int k; 3 for(k = 1; k*2 < count[i] + 1; k *= 2){ 4 for(int j = V; j >= k*c[i] ; --j){ 5 dp[j] = max(dp[j],dp[j-k*c[i]] + k*a[i]); 6 } 7 } 8 k = count[i] + 1 - k; 9 for(int j = V; j >= k*c[i]; --j){ 10 dp[j] = max(dp[j],dp[j-k*c[i]] + k*a[i]); 11 } 12 13 }
O(VN)的写法
/*此种方法求最大值不确定可否,但判断是否能将1-V内的背包装满可以*/
----------------------------------------------------------------------
1 int used[MAXN]; 2 for(int i = 1; i <= N; ++i){ 3 memset(used,0,sizeof(used)); 4 for(int j = c[i]; j <= V; ++j){ 5 if(used[j-c[i]] < count[i] && dp[j] < dp[j-c[i]] + a[i]){ 6 dp[j] = dp[j-c[i]] + a[i]; 7 used[j] = used[j-c[i]] + 1; 8 } 9 } 10 } //有待验证,估计不对,有兴趣的可以验证下
----------------------------------------------------------------------
现在dp[i]用来表示容量为i的背包能否被所给物品恰好装满 上面的01背包和完全背包以及上面一种O(V*Σlog count[i])的写法也可以做这样的改变
1 memset(dp,false,sizeof(dp)); 2 dp[0] = true; 3 for(int i = 1; i <= N; ++i){ 4 memset(used,0,sizeof(used)); 5 for(int j = c[i]; j <= V; ++j){ //注意这里是顺序的(和完全背包相同),其实多重背包也可以理解成被限制了的完全背包 6 if(!dp[j] && dp[j-c[i]] && used[j-c[i]] < count[i]){ //注意这里的!dp[j]不能少 7 dp[j] = true; 8 used[j] = used[j-c[i]] + 1; 9 } 10 } 11 }
四、混合背包
以上三种背包的混合,有的物品只能取一次,有的物品能取无限次,有的物品能取有限次
算法伪代码:
for i=1..n
if 第i件物品是01背包
for j=V...c[i]
dp[j]=max(); //同上面01背包代码
else if 第i件物品是完全背包
for j=c[i]...V
dp[j]=max(); //同上面完全背包
else if 第i件物品是多重背包
MultiplePack(c[i],a[i],count[i]) //同上多重背包
五、二维费用的背包
如问题:每件物品都有各自的体积v[i]和重量w[i],一个背包最多可以装的体积为V,最多可以装的重量为W,问最多能装的价值。即有两种属性限制的背包问题。再比如,一个一般的01背包问题,再加上个数限制,即最多拿K件物品,也可以理解为每件物品的个数耗费为1,背包再加一个容量为K的属性,也是二维费用背包。对于二维费用的背包,只需对dp数组加一维即可,01背包倒序,完全背包正序
以01背包为例:
for i=1...n
for j=V..v[i]; //若为完全背包则写为for j=v[i]..V for k=w[i]..W;
for k=W..w[i];
dp[j][k] = max(dp[j][k],dp[j-v[i]][k-w[i]] + a[i]);
六、分组背包的问题
将n件物品分为若干组,每组中只能选择一件物品,问可以得到的最大价值。
for 所有的组k
for v=V..0
for 所有的i属于组k
dp[v]=max{dp[v],dp[v-c[i]]+a[i]}
七、有依赖的背包问题
要购买一件物品,必须先购买另一件物品。这就是有依赖关系的背包问题 背包九讲里讲的很清楚了,我就不多说了。
下面两个部分暂且放下。以后遇到具体的问题时再补充。
八、泛化物品的背包
九、背包问题问法的变化
下面就说几个比较经典的题目:01背包 完全背包就不说了,混合背包也没必要说
一、【POJ1014】【TOJ1034】【HDU1059】【ZOJ1149】Dividing——多重背包问题
一般在几个OJ上同时出现并且长久不衰的题都是经典题目,比如这题,是一道关于多重背包的题目
【题目大意】6种价值分别为1 2 3 4 5 6的石头,给出每种石头的数目,问能不能将这些石头分为价值相等的两堆。
【解析】经典的多重背包问题,a[]数组即为1 2 3 4 5 6,count数组题目给出,怎么做呢?上面提到一点,设dp[i]表示容量为i的背包能不能被所给物体装满。如果dp[sum/2]==1,那么就说明可以分为两堆(sum是所有石头的总价值),直接套用上面的模板即可。
1 #include <cstdio> 2 #include <cstring> 3 using namespace std; 4 5 int a[10]; 6 int dp[120100]; 7 int main(){ 8 int cas = 0; 9 while( ++ cas ){ 10 int sum = 0; 11 for(int i = 1; i <= 6; ++i){ 12 scanf("%d",&a[i]); 13 sum += a[i]*i; 14 } 15 if(sum == 0)break; 16 17 memset(dp,0,sizeof(dp)); 18 printf("Collection #%d:\n",cas); 19 20 if(sum % 2 == 1){ 21 puts("Can't be divided."); 22 puts(""); 23 continue; 24 } 25 26 dp[0] = 1; 27 //O(NV)的方法 28 int dpt[120000]; 29 for(int i = 1; i <= 6; ++i){ 30 memset(dpt,0,sizeof(dpt)); 31 for(int j = i; j <= sum/2; ++j){ 32 if (!dp[j] && dp[j-i]&& dpt[j-i] < a[i]) { 33 dpt[j] = dpt[j-i] + 1; 34 dp[j] = 1; 35 } 36 } 37 } 38 /* 39 //二进制优化的方法 40 41 for(int i = 1;i <= 6;++i){ 42 int k; 43 for(k = 1;k*2 < a[i]+1;k *= 2) 44 for(int v = sum/2;v >= k*i;--v) 45 if(dp[v-k*i]) 46 dp[v]=true; 47 k = a[i]+1-k; 48 for(int v = sum/2;v > k*i;--v) 49 if([v-k*i]) 50 dp[v] = true; 51 52 } 53 */ 54 if(dp[sum/2]){ 55 puts("Can be divided."); 56 } 57 else {puts("Can't be divided.");}; 58 puts(""); 59 60 } 61 return 0; 62 }
二、【TOJ3540】Consumer有依赖关系的背包
【题目大意】给若干组物品,每组物品都有一个箱子(箱子自身也有cost),然后就是物品的cost和value,要买某个物品必须也要买装这个物品的箱子,给一定钱数,问能获得的最大价值。
【题目解析】可以参照背包九讲第六讲和第七讲里的内容解决该问题(相当对口,这题就是根据背包九讲来的)
有一种比较简单的写法,两次背包即可。
1 #include<iostream> 2 using namespace std; 3 const int MAXN = 100000; 4 int main() 5 { 6 int n,total,boxcost,goodnum,cost,value,i,j,t; 7 int dpbox[MAXN],dptotal[MAXN]; 8 while(scanf("%d%d",&n,&total) != EOF) 9 { 10 memset(dptotal,0,sizeof(dptotal)); 11 for(i = 0; i < n; i++) 12 { 13 scanf("%d%d",&boxcost,&goodnum); 14 memcpy(dpbox,dptotal,sizeof(dptotal)); 15 for(j = 0; j < goodnum; j++) 16 { 17 scanf("%d%d",&cost,&value); 18 for(t = total - boxcost; t >= cost; t--) 19 { 20 if(dpbox[t] < dpbox[t - cost] + value) 21 dpbox[t] = dpbox[t-cost] + value; 22 } 23 } 24 for(t = total; t >= boxcost; t--) 25 if(dptotal[t] < dpbox[t-boxcost]) 26 dptotal[t] = dpbox[t-boxcost]; 27 } 28 printf("%d\n",dptotal[total]); 29 } 30 return 0; 31 }
这题源自多校联合赛,是一个比较难的背包综合题
【题目大意】一个人有T分钟去做工作,共有n组工作,每一组有若干工作,每个工作消耗的时间和得到的快乐值已知,每一个组有一个标识位,标识位为0,则该组中至少应该选取一样工作完成,标识位为1,则该组中至多有一个工作被选择,标识位为2,无限制。
【分析】对标识位为2的工作组,因为无限制条件,每个工作可做可不做,即用简单的01背包即可处理。对标识位为1的工作组,因为至多选择一个工作,所以本次操作只对dp数组选取最优值更新一次即可。对至少选择一个的工作组,还不是太理解,只是看别人代码写的,还请大家集思广益,有想法可以留言评论,以后理解透彻了以后会做补充。
1 #include <cstdio> 2 #include <algorithm> 3 #include <cstring> 4 #include <cmath> 5 #include <queue> 6 #include <vector> 7 #include <map> 8 9 using namespace std; 10 11 const int maxt = 100 + 10; 12 13 int dp[maxt]; 14 int td[maxt], n, m, t; 15 16 void dp0() { //至少取一个 17 memcpy (td, dp, sizeof(td)); 18 memset (dp, -1, sizeof(dp)); 19 for (int i = 0, a, b; i < m; ++i) { 20 scanf ("%d%d", &a, &b); 21 for (int j = t; j >= a; --j) {//注意怎样保证至少取一个?? 22 if (dp[j-a] != -1) { 23 dp[j] = max(dp[j], dp[j-a] + b); 24 } 25 if (td[j-a] != -1) { 26 dp[j] = max(dp[j], td[j-a] + b); 27 } 28 } 29 } 30 } 31 32 void dp1() { //最多取一个,用上次得到的dp值更新最优值,只更新一次 33 memcpy(td, dp, sizeof(td)); 34 for (int i = 0, a, b; i < m; ++i) { 35 scanf ("%d%d", &a, &b); 36 for (int j = t; j >= a; --j) { 37 if (td[j-a] != -1) { 38 dp[j] = max(dp[j], td[j-a] + b); 39 } 40 } 41 } 42 } 43 44 void dp2() { //无条件限制,01背包,更新多次 45 for (int i = 0, a, b; i < m; ++i) { 46 scanf ("%d%d", &a, &b); 47 for (int j = t; j >= a; --j) { 48 if (dp[j-a] != -1) { 49 dp[j] = max(dp[j], dp[j-a] + b); 50 } 51 } 52 } 53 } 54 55 void init() { 56 memset (dp, -1, sizeof(dp)); 57 dp[0] = 0; 58 for (int i = 0, s; i < n; ++i) { 59 scanf ("%d%d", &m, &s); 60 if (s == 0) dp0(); 61 else if (s == 1) dp1(); 62 else if (s == 2) dp2(); 63 } 64 } 65 66 void solve() { 67 int ans = -1; 68 for (int i = 0; i <= t; ++i) 69 ans = max(ans, dp[i]); 70 printf ("%d\n", ans); 71 } 72 73 int main() { 74 //freopen("ar.txt","r",stdin); 75 while (scanf ("%d%d", &n, &t) != EOF) { 76 init(); 77 solve(); 78 } 79 return 0; 80 }