【读书笔记/解题报告/复健向】《挑战程序设计竞赛》及《背包九讲》动态规划

先码一个看起来总结得非常完整的背包问题,附背包九讲下载地址,请点击

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 }
View Code(二维)
 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 }
View Code(一维)

解释一下笔者第一次学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 }
View Code(二维另版)

明白了上述三个问题,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 }
View Code

 

《挑战程序设计竞赛》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 }  
View Code(二维三重循环版)
 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 }  
View Code(二维二重循环版)

解释一下二重循环版:

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 } 
View Code(一维)

 

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 }
View Code

另:使用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 }
View Code

 

多重部分和问题(多重背包问题)

先来看一个简单粗暴的解法,复杂度较高。代码中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 }
View Code

再看看优化之后的算法,如后描述,程序便呼之欲出了。这里的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 }
View Code

这里我一开始碰到了一个理解上的问题,脑海中的疑问大致如下:dp[j-a[i]]=0时就默认为不能加得到i是否会误判,因为其中可能保留的是之前的数据

实际上是不会的,因为:每个dp[i][j]只与dp[i][<j]和dp[i-1]相关,dp[j-a[i]]此时已经更新,如果仍未0,则必然不可以

 

 此处待更新:背包九讲、最长上升子序列、有关计数问题的DP和DP部分的总结

posted @ 2015-06-26 17:33  iiyiyi  阅读(497)  评论(0编辑  收藏  举报