先码一个看起来总结得非常完整的背包问题,附背包九讲下载地址,请点击
2.3.1(POJ3624/NOIP2004采药问题)
最基础的01背包问题,标程性质,又二维和一维两种写法。
1 #include<iostream>
2 #include<cstring>
3 #include<cstdio>
4 #include<cmath>
5 using namespace std;
6 const int MAXN=3403;
7 int w[MAXN];
8 int v[MAXN];
9 int W;
10 int f[MAXN][MAXN];
11
12 int main()
13 {
14 int n;
15 scanf("%d%d",&n,&W);
16 memset(f,0,sizeof(f));
17 for (int i=1;i<=n;i++) scanf("%d%d",&w[i],&v[i]);
18 for (int i=1;i<=n;i++)
19 for (int j=1;j<=W;j++)
20 {
21 f[i][j]=f[i-1][j];
22 if (j>=w[i]) f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);
23 }
24 cout<<f[n][W]<<endl;
25 }
1 #include<iostream>
2 #include<cstdio>
3 #include<cmath>
4 #include<cstring>
5 using namespace std;
6 const int MAXN=12890;
7 int f[MAXN];
8
9 int main()
10 {
11 int N,M;
12 scanf("%d%d",&N,&M);
13 memset(f,0,sizeof(f));
14 for (int i=0;i<N;i++)
15 {
16 int w,d;
17 scanf("%d%d",&w,&d);
18 for (int j=M;j>=w;j--)
19 if ((f[j-w]+d) > f[j]) f[j]=f[j-w]+d;
20 }
21 cout<<f[M]<<endl;
22 return 0;
23 }
解释一下笔者第一次学01背包时容易遇到的困扰:
✿为什么一维中要从后往前?因为一维并没有限制取到哪一个背包,从后往前防止再累加过一遍当前物品的基础上再次累加。
✿为什么二维中需要if (j<w[i]) f[i][j]=f[i-1][j]?这个语句而一维中不需要类似语句?因为没有限制当前取到哪一个背包,取前一个背包代价为j时的情况已经记录了下来。
✿二维背包能从后往前做吗?可以,对f[i][j]产生影响的只有它自身和f[i-1]中数据,前后次序无关。
✿为什么一维中输出的时候输出f[M]或F[n][M]即可?背包问题中F[n][M]并不代表恰好取前n个包,总价值恰好为M,而是在这个范围内的最大值。不理解的话想一想以下的情况:若第一个物品价值为1,f[1][2]=f[0][1]+1,但是此时取到的总价值只有1,第二个下标却为2。
✿二维背包能由当前位置推向后面吗?可以,见下面给出的程序
1 int dp()
2 {
3 for (int i=0;i<n;i++)
4 {
5 for (int i=0;j<=W;j++)
6 {
7 f[i+1][j]=max(f[i+1,j],f[i][j]);
8 if (j+w[i]<=W) f[i+1][j+w[i]]=max(f[i+1][j+w[i]],f[i][j]+v[i]);
9 }
10 }
11 cout<<f[n][W]<<endl; \因为往后递推,最终数据保存在了f[n]中
12 }
明白了上述三个问题,01背包就基本可以算是理解透彻了。《挑战程序设计竞赛》在2.3.1中有对记忆化搜索的阐述,也可以关注一下。
至此,最基本的01背包问题就讲解结束了。
《挑战程序设计竞赛》2.3.1最长公共子序列(POJ1458)
01背包问题问题的拓展应用。思路非常简单,如下:
f[i][j]表示s1取到第i位,s2取到第j位时的最长公共子序列长度。如果s1[i]≠s2[j]在,则f[i,j]=max(f[i-1,j],f[i,j-1]),否则再增加一个比较对象f[i-1][j-1]+1
虽然思路是秒杀的,但是POJ1458涉及到字符串的读取,麻烦死了,几乎每次碰到字符串我都要跪,参考了他人的程序,折腾了好长时间,不过总算一遍就AC了。
1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 #include<cmath>
5 using namespace std;
6 const int MAXN=1001;
7 char s1[MAXN];
8 char s2[MAXN];
9 int f[MAXN][MAXN];
10
11 int main()
12 {
13
14 while (scanf("%s%s",s1+1,s2+1)!=EOF)
15 {
16 int len1=strlen(s1+1),len2=strlen(s2+1);
17 memset(f,0,sizeof(f));
18 for (int i=1;i<=len1;i++)
19 for (int j=1;j<=len2;j++)
20 {
21 f[i][j]=max(f[i-1][j],f[i][j-1]);
22 if (s1[i]==s2[j]) f[i][j]=f[i-1][j-1]+1;
23 }
24 cout<<f[len1][len2]<<endl;
25 }
26 return 0;
27 }
《挑战程序设计竞赛》2.3.2完全背包问题
完全背包问题和01背包问题的区别在于:每种物品可以挑选任意多件。完全背包问题和01背包问题一样,都有二维和一维的两种写法
1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 using namespace std;
5 int const MAXN=100;
6 int f[MAXN][MAXN];
7 int w[MAXN];
8 int v[MAXN];
9
10 int main()
11 {
12 int n,d;
13 scanf("%d%d",&n,&d);
14 for (int i=1;i<=n;i++) scanf("%d%d",&w[i],&v[i]);
15 memset(f,0,sizeof(f));
16 for (int i=1;i<=n;i++)
17 for (int j=1;j<=d;j++)
18 for (int k=0;k*w[i]<=j;k++)
19 {
20 f[i][j]=max(f[i][j],f[i-1][j-k*w[i]]+k*v[i]);
21 }
22 cout<<f[n][d]<<endl;
23 }
1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 using namespace std;
5
6 int const MAXN=100;
7 int f[MAXN][MAXN];
8 int w[MAXN];
9 int v[MAXN];
10
11 int main()
12 {
13 int n,d;
14 scanf("%d%d",&n,&d);
15 for (int i=1;i<=n;i++) scanf("%d%d",&w[i],&v[i]);
16 memset(f,0,sizeof(f));
17 for (int i=1;i<=n;i++)
18 for (int j=0;j<=d;j++)
19 {
20 f[i][j]=f[i-1][j];
21 if (j>=w[i]) f[i][j]=max(f[i][j],f[i][j-w[i]]+v[i]); //f[i][j]中选择k个情况(k>=1)个的清醒,与f[i][j-w[i]]中选择(k-1)个的情况相同
22 }
23 cout<<f[n][d]<<endl;
24 }
解释一下二重循环版:
1.f[i][j]中选择k个情况(k>=1)个的清醒,与f[i][j-w[i]]中选择(k-1)个的情况相同
2.为什么不需要与f[i-1][j-w[i]]+v[i]比较?因为它等价于f[i][j-w[i]]+v[i]。
1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 using namespace std;
5
6 const int MAXN=500;
7 int f[MAXN];
8
9 int main()
10 {
11 int n,d;
12 scanf("%d%d",&n,&d);
13 memset(f,0,sizeof(f));
14 for (int i=0;i<n;i++)
15 {
16 int w,v;
17 scanf("%d%d",&w,&v);
18 for (int j=w;j<=d;j++)
19 f[j]=max(f[j],f[j-w]+v);
20 }
21 cout<<f[d]<<endl;
22 return 0;
23 }
POJ1384Piggy-Bank
完全背包问题的变形,求出最小的情况。注意区分背包问题中“不超过”和“恰巧取到的问题”,之后会总结两者的区别。
错误点:要注意数据范围,当数组下标不够的时候POJ会显示为RE,其实不是真正意义上的超时,而是数组溢出。INF设置的不够大时会WA
1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 #include<cmath>
5 using namespace std;
6 const int MAXN=10000+10;
7 const int INF=25000000;
8 int f[MAXN];
9
10 int main()
11 {
12 int t;
13 scanf("%d",&t);
14 for (int kase=0;kase<t;kase++)
15 {
16 int E,F,n,d;
17 scanf("%d%d",&E,&F);
18 scanf("%d",&n);
19 d=F-E;
20 for (int i=1;i<=d;i++) f[i]=INF;
21 f[0]=0;
22 for (int i=0;i<n;i++)
23 {
24 int w,v;
25 scanf("%d%d",&v,&w);
26 for (int j=w;j<=d;j++)
27 {
28 f[j]=min(f[j],f[j-w]+v);
29 }
30 }
31 if (f[d]==INF) cout<<"This is impossible."<<endl;
32 else cout<<"The minimum amount of money in the piggy-bank is "<<f[d]<<'.'<<endl;
33 }
34 return 0;
35 }
另:使用memset在int中初始一个极其大的值,用memset(dis,0x3f3f3f3f, sizeof(dis))
01背包问题之二(当w极其大的情形)
笔者见识短浅,这种方法还是第一次碰到,故全题标注为荧光黄。因为一个小错折腾了将近一个小时,终于发现缘由了。
这里dp[i][j]的定义为前i个物品中挑选出价值总和为j时总重量的最小值(不存在时就是一个重发大的数值INF)。初值为dp[0][0]=0,dp[0][j]=INF
最终答案是令dp[n][j]<=W的最大的J
错误点:起初循环的终止条件均为MAXN而非MAXN-1,但是dp[MAXN]的实际范围是0~MAXN-1,故dp[MAXN]这个位置必定为0。
1 #include<iostream>
2 #include<cstdio>
3 #include<cmath>
4 using namespace std;
5 const int INF=1000000001;
6 const int MAXN=10000+5;
7 int dp[100+1][MAXN];
8 int w[101];
9 int v[101];
10
11 int main()
12 {
13 int n,d;
14 scanf("%d%d",&n,&d);
15 for (int i=1;i<=n;i++) scanf("%d%d",&w[i],&v[i]);
16 for (int i=1;i<=MAXN-1;i++) dp[0][i]=INF;
17 dp[0][0]=0;
18 for (int i=1;i<=n;i++)
19 for (int j=0;j<=MAXN-1;j++)
20 {
21 dp[i][j]=dp[i-1][j];
22 if (j>=v[i]) dp[i][j]=min(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
23 }
24 for (int i=MAXN-1;i>=0;i--) if (dp[n][i]<=d)
25 {
26 cout<<i<<endl;
27 break;
28 }
29 return 0;
30 }
多重部分和问题(多重背包问题)
先来看一个简单粗暴的解法,复杂度较高。代码中f[i][j]后要用|=的缘由是当k比当前值小的时候,f[i][j]可能已经为true
1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 using namespace std;
5 const int MAXN=100+5;
6 int n,k;
7 int a[MAXN];
8 int m[MAXN];
9 bool f[MAXN][100000];
10
11 int main()
12 {
13 scanf("%d%d",&n,&k);
14 for (int i=1;i<=n;i++) scanf("%d%d",&a[i],&m[i]);
15 memset(f,false,sizeof(f));
16 f[0][0]=true;
17 for (int i=1;i<=n;i++)
18 for (int j=0;j<=k;j++)
19 for (int k=0;k<=m[i] && k*a[i]<=j;k++)
20 {
21 f[i][j]|=f[i-1][j-k*a[i]];
22 }
23 if (f[n][k]) cout<<"Yes"<<endl;
24 else cout<<"No"<<endl;
25 }
再看看优化之后的算法,如后描述,程序便呼之欲出了。这里的dp[i][j]表示前i中数加得到j时第i种数最多剩余几个(不能加和得到i的情况下为-1)递推式为:
dp[i][j]=mi(dp[i-1][j]≥0,即前i-1种数就能达到数字j)
=-1(j<ai 或者 dp[i][j-ai]≤0,即再加上一个第i种数也无法达到j 或者 当前和小于当前数)
=dp[i][j-ai]-1(可以达到的情况)
1 #include<iostream>
2 #include<cstdio>
3 #include<cstring>
4 using namespace std;
5 const int MAXN=100;
6 int a[MAXN];
7 int m[MAXN];
8 int dp[100000+5];
9 int n,k;
10
11 int main()
12 {
13 scanf("%d%d",&n,&k);
14 for (int i=1;i<=n;i++) scanf("%d",&a[i]);
15 for (int i=1;i<=n;i++) scanf("%d",&m[i]);
16 memset(dp,-1,sizeof(dp));
17 dp[0]=0;
18 for (int i=1;i<=n;i++)
19 for (int j=0;j<=k;j++)//j一定要从零开始
20 {
21 if (dp[j]>=0) dp[j]=m[i];
22 else
23 {
24 if (j<a[i] || dp[j-a[i]]<=0) dp[j]=-1;//dp[j-a[i]]等于0时耶不能再取一次a[i]
25 else dp[j]=dp[j-a[i]]-1;
26 }
27 }
28 if (dp[k]>=0) cout<<"Yes"<<endl;
29 else cout<<"No"<<endl;
30 return 0;
31 }
这里我一开始碰到了一个理解上的问题,脑海中的疑问大致如下:dp[j-a[i]]=0时就默认为不能加得到i是否会误判,因为其中可能保留的是之前的数据
实际上是不会的,因为:每个dp[i][j]只与dp[i][<j]和dp[i-1]相关,dp[j-a[i]]此时已经更新,如果仍未0,则必然不可以
此处待更新:背包九讲、最长上升子序列、有关计数问题的DP和DP部分的总结