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

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

    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部分的总结

  • 相关阅读:
    购物车程序
    python学习第二节 数据类型、字符编码、文件处理
    while实现2-3+4-5+6...+100 的和
    给文件加权限
    查询数据插入新表格
    归档程序错误。在释放之前仅限于内部连接
    查看Linux环境变量
    查找文件命令
    ORACLE导入导出操作篇
    oracle中使用minus进行数据排除(类似SqlServer except函数)
  • 原文地址:https://www.cnblogs.com/iiyiyi/p/4602851.html
Copyright © 2011-2022 走看看