• 【读书笔记/解题报告/复健向】《算法竞赛入门经典训练指南》及《挑战程序设计竞赛》贪心算法


    《算法竞赛入门经典训练指南》例题一(UVa11292)

    基础贪心,没什么需要多讲的,直接放上代码。有一点要注意的是我第一遍写的时候竟然犯了两个错误。

    错误点

    1. 将dragon、knight与i、j对应错误了,仔细一想人有先后对应的天性,下次设置i、j时还是老老实实根据输入顺序来,避免出错
    2. 第23行遗漏了(j<n)这个条件,使得在龙已经全被砍头的情况下却雇佣了剩余的骑士。

    本题重点

    砍龙头的时候设置两个指针,分别移动,使用频率挺高的一个小技巧,不难,但是挺重要的。

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm> 
     4 using namespace std;
     5 
     6 const int MAXN=20000;
     7 int dragon[MAXN];
     8 int knight[MAXN];
     9 
    10 int main()
    11 {
    12     int n,m;
    13     while (scanf("%d%d",&n,&m))
    14     {
    15         int sum=0;
    16         if ((n==m) && (n==0)) break;
    17         for (int i=0;i<n;i++) scanf("%d",&dragon[i]);
    18         for (int i=0;i<m;i++) scanf("%d",&knight[i]);
    19         sort(dragon,dragon+n);
    20         sort(knight,knight+m);
    21         int i=0,j=0;
    22 
    23         while ((i<m) && (j<n))
    24         {
    25             if (knight[i]>=dragon[j])
    26             {
    27                 j++;
    28                 sum+=knight[i];
    29             }
    30             i++;
    31         }
    32         if (j<n) cout<<"Loowater is doomed!"<<endl;
    33         else cout<<sum<<endl;
    34     }
    35     return 0;
    36 }
    View Code

     

     《算法竞赛入门经典训练指南》例题二(UVa11729)

    较基础贪心,关键是证明“执行时间长的先交代,总时间最短”这个命题以及“在执行时间相同的情况下如何安排次序”这个问题。当然事实证明执行时间相同的情况下,如何安排次序是无所谓的。书上已经讲得很清楚了,这里不进行过多阐述。(详见P4图解)注意的是书上的图(b)下方有误,应为B[Y]+B[X]+J[Y]。

     

    对于书中标程的一些说明

    1. 书上用到了<vector>,我也不知道比赛的时候能不能用(哪个神犇来告诉我一下),也觉得没有必要用,写程序的时候就没有用到<vector>了。
    2. 这里有个非常重要的struct比较大小的简便写法,刚开始学C++要重点学习一下。其中Rec表示我们设置的记录。
      1 struct Rec
      2 {
      3     int b,j;
      4     bool operator < (const Rec& x) const
      5     {
      6         return j<x.j;
      7     }
      8 };
    3. 书中循环的判断条件中出现的scanf(“%d”,&n) == 1的含义即为输入的n为一个数字。因为scanf判断输入正确时返回值为数字,若非法则返回值为0,循环中    止。在这里其实可以不必用的,但在诸如:最后一位用字符表示中止循环条件时,非常好用。

     

    错误点

    1. 第一遍的时候全然忘记sort是从小到大排序的了,于是耍了一个小聪明把return j<x.j中的小于号改为大于号即可,觉得自己超机智的时候发现树上就是这么干的。或者老老实实地把循环敲为for (int i=n-1;i>=0;i++)。个人推荐后者,因为(个人实践表明)耍小聪明总是没有好下场的。

    本题重点

    对于贪心的证明是个重点,代码28-33行的小技巧在执行任务类的题目里非常常见,要重点牢记

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm>
     4 using namespace std;
     5 const int MAXN=1000;
     6 int b[MAXN];
     7 int j[MAXN];
     8 
     9 struct task
    10 {
    11     int b,j;
    12     bool operator < (const task& x) const
    13     {
    14         return j>x.j;//因为要从大到小排所以这里变动一下 
    15     }
    16 };
    17 
    18 int main()
    19 {
    20     int cases=0,n;
    21     while (scanf("%d",&n))
    22     {
    23         cases++;
    24         if (n==0) break;
    25         task kase[MAXN];
    26         for (int i=0;i<n;i++) scanf("%d%d",&kase[i].b,&kase[i].j);
    27         sort(kase,kase+n);
    28         int m=0,ans=0;
    29         for (int i=0;i<n;i++)
    30         {
    31             m+=kase[i].b;
    32             ans=max(ans,m+kase[i].j);
    33         }
    34         cout<<"Case "<<cases<<": "<<ans<<endl;
    35     }
    36     return 0;
    37 }
    View Code

     


     

    以下为《挑战程序设计竞赛》中贪心部分

    2.2.1硬币问题(UVa11292)

    没什么需要说的,秒杀题。

    错误点

    1. 一开始犯了一个小错,注意17行为a-v[i]>=0需要取等,否则余额刚好等于当前面值时就不会算了 
     1 #include<iostream>
     2 #include<cstdio>
     3 using namespace std;
     4 
     5 int a;
     6 int v[6]={1,5,10,50,100,500};
     7 int c[6];
     8 
     9 int main()
    10 {
    11     scanf("%d%d%d%d%d%d%d",&c[0],&c[1],&c[2],&c[3],&c[4],&c[5],&a);
    12     int i=5;
    13     int sum=0;
    14     while ((a>0) && (i>=0))
    15     {
    16         int j=0;
    17         while ((0<=a-v[i]) && (j<c[i]))    //这里是0<=a-v[i] 
    18         {
    19             j++;
    20             a-=v[i];
    21         }
    22         sum+=j;
    23         i--; 
    24     }
    25     cout<<sum<<endl;
    View Code

    顺便说一下虽然是一道极其简单的题,但是看了书中的标程,觉得自己的程序写得好丑(╯>д<)╯于是按照标程的方式又写了一个简洁的

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<cmath> 
     4 using namespace std;
     5 
     6 int a;
     7 int v[6]={1,5,10,50,100,500};
     8 int c[6];
     9 
    10 int main()
    11 {
    12     scanf("%d%d%d%d%d%d%d",&c[0],&c[1],&c[2],&c[3],&c[4],&c[5],&a);
    13     int sum=0;
    14     for (int i=5;i>=0;i--)
    15     {
    16         sum+=min((a/v[i]),c[i]);
    17         a-=min(a/v[i],c[i])*v[i];
    18     }
    19     cout<<sum<<endl;
    20 }
    View Code

     


     2.2.2区间调度问题

    较难贪心,但题目本身非常标准。重点在于对选取方法的否定与证明。

    对书中内容的补充关于区间调度问题,这里推荐一个CSDN的博文,以后我会细谈的,请点击对于本体的命题的证明方法请戳,因为与本体相关,把证明部分在此引用一下

    命题1.1  当i>=1时,该算法所接受的第i个区间的右端点坐标fi<=某最优解中的第i个区间的右端点坐标gi。

    该命题可以运用数学归纳法来证明。对于i=1,命题显然为真,因为算法第一个选择的区间拥有最小右端点坐标。令i>1,假定论断对i-1为真,即fi-1<=gi-1。则最优解的第i个可选区间所组成的集合包含于执行该算法时第i个可选区间所组成的集合;而当算法选择第i个区间时,选的是在可选区间中右端点坐标最小的一个,所以有fi<=gi。证毕。

    设该算法选出了k个区间,而最优解选出了m个区间。

    命题1.2  最优解选出的区间数量m=该算法选出的区间数量k。

    假设m>k,根据命题1.1,有fk<=gk。由于m>k,必然存在某区间,在gk之后开始,故也在fk之后开始。而该算法一定不会在选了第k个区间后停止,还会选择更多的区间,产生矛盾。所以m<=k,又因为m是最优解选出区间个数,所以m=k。

    综上所述,算法选出的区间是最优解。

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm>
     4 using namespace std;
     5 
     6 const int MAXN=100000;
     7 struct task
     8 {
     9     int s,t;
    10     bool operator < (const task& x) const
    11     {
    12         return t<x.t;
    13     }
    14 };
    15 
    16 int main()
    17 {
    18     int n,last=0,ans=0;
    19     task kase[MAXN];
    20     scanf("%d",&n);
    21     for (int i=0;i<n;i++) scanf("%d",&kase[i].s);
    22     for (int i=0;i<n;i++) scanf("%d",&kase[i].t);
    23     sort(kase,kase+n);
    24     for (int i=0;i<n;i++)
    25     {
    26         if (kase[i].s>last)
    27         {
    28             ans++;
    29             last=kase[i].t;
    30             cout<<"task "<<i+1<<endl;
    31          } 
    32     }
    33     cout<<ans<<endl;
    34 }
    View Code

    2.2.3字典序最小问题(POJ 3617)

    这道题的样例十分良心,本题唯一一个陷阱就在那儿了,然而我还是一脚踩了下去。

    错误点

    1. 输入的时候第一次把&忘记了导致程序崩溃,数组输入用scanf的时候最容易遗忘&了要注意
    2. 当两边字符相同的时候可能会出现分歧,需要判断。我一开始没有考虑到,所以写出来的程序是这个样子的
       1 #include<iostream>
       2 #include<cstdio>
       3 #include<cmath>
       4 using namespace std;
       5 const int MAXN=2000;
       6 
       7 int main()
       8 {
       9     int n;
      10     scanf("%d",&n);
      11     char s[MAXN];
      12     for (int i=0;i<n;i++) 
      13     {
      14         getchar();
      15         scanf("%c",&s[i]);    //不要忘记& 
      16     }
      17     int i=0,j=n-1;
      18     while (i<=j)
      19     {
      20         cout<<s[i]<<' '<<s[j]<<endl; 
      21         if (s[i]<=s[j]) 
      22         {
      23             cout<<s[i];i++;
      24         }
      25         else
      26         {
      27             cout<<s[j];j--;
      28         }
      29     }
      30     cout<<endl;
      31     return 0; 
      32 }
      Wrong Code

      样例就挂掉了之后才发现_(:зゝ∠)_

    3. 好,再来说说我写好之后的错误。21行是i+k<=j而不是i+k<=j-k,光想着后面了这里就写错了。
    4. 还有28行一定要写s[i+k]>s[j-k] ,否则比较到相同时就会自动退出!这个比较就没有意义了。
    5. 就是这样一道水题,让我吃了两个WA(上面两点)和人生中头两个PE。 一直没弄明白到底哪里出错了,打开别人的解题报告发现没输出80个字符会换一行,去看了一下题目发现竟然有这样一句话“Every line (except perhaps the last one) contains the initials of 80 cows ('A'..'Z') in the new line.”然而我没有读懂这句话的意思,之前写题的时候看书翻译里也并没有这句话。所以大家要好好看题好好学习英语啊!

    本题重点

    对于字典序这类的题目,当两边相同的时候难以比较大小,此时用一个特定的方法。引用一下书中的原句:“按照字典序比较S和将S反转后的字符串S' ”。这个处理方法非常常见,要牢记

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<cmath>
     4 using namespace std;
     5 const int MAXN=2000;
     6 
     7 int main()
     8 {
     9     int n;
    10     scanf("%d",&n);
    11     char s[MAXN];
    12     for (int i=0;i<n;i++) 
    13     {
    14         getchar();
    15         scanf("%c",&s[i]);    //不要忘记& 
    16     }
    17     int t=0;
    18     int i=0,j=n-1;
    19     while (i<=j)//通过反转字符串来比较,避免字典序相同时带来的困扰 
    20     {
    21         bool left=false;
    22         for (int k=0;i+k<=j;k++)//注意这里是i+k<=j而不是i+k<=j-k 
    23         {
    24             if (s[i+k]<s[j-k])
    25             {
    26                 left=true;
    27                 break;
    28             }
    29             else if (s[i+k]>s[j-k]) //s[i+k]>s[j-k] 
    30             {
    31                 left=false;
    32                 break;
    33             }
    34         }
    35         if (left) cout<<s[i++];
    36         else cout<<s[j--];
    37         t++;
    38         if (t%80==0) cout<<endl;
    39     }
    40     //cout<<endl;
    41     return 0; 
    42 }
    View Code 

     

    2.2.4 其它例题

    1.Saruman's Army(POJ3069)

    水题,不多讲了,没有任何难点。我就说一句:不要忘了排序,我写太兴奋第一次就忘掉了..

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm>
     4 using namespace std;
     5 
     6 const int MAXN=1000;
     7 int x[MAXN];
     8 
     9 int main()
    10 {
    11     int r,n;
    12     while (scanf("%d%d",&r,&n))
    13     {
    14         if ((r==n) && (r==-1)) break;
    15         for (int i=0;i<n;i++) scanf("%d",&x[i]);
    16         sort(x,x+n);
    17         int j=x[0],i=0,sum=0;
    18         while (i<n)
    19         {
    20             while (j+r>=x[i]) i++;
    21             sum++;
    22             int temp=x[i-1]+r;
    23             while (temp>=x[i]) i++;
    24             j=x[i]; 
    25         }
    26         cout<<sum<<endl; 
    27     }
    28     return 0;
    29 }
    View Code

     2.Fence Repari(POJ3253)

    经典题,但很容易因为固化思维跳坑,真的很容易!笔者在之前写过两次都跳坑了,这次傻歪歪又跳坑了……

    错误点

    1.先放一个错误版的程序,不能按照排序后由小到大来分割

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm>
     4 using namespace std;
     5 const int MAXN=20000;
     6 int l[MAXN];
     7 
     8 int main()
     9 {
    10     int n;
    11     scanf("%d",&n);
    12     for (int i=0;i<n;i++) scanf("%d",&l[i]);
    13     sort(l,l+n);
    14     int ans=0;
    15     for (int i=0;i<n;i++)
    16     {
    17         ans+=l[i]*(n-i);
    18     }
    19     ans-=l[0];
    20     cout<<ans;
    21 }
    Wrong Code

    这里应该使用的是Huffman树,开销的合计等于各叶子节点的木板长度*深度。这里用的是简单粗暴的方法。

    2.在插入排序的时候不要忘记了在范围内这个条件,否则如果合并后交换到最后,会出现0

    3.ans一定要用__int64,顺便int64的下划线是两个,不是一个!

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm>
     4 using namespace std;
     5 const int MAXN=20000;
     6 int l[MAXN];
     7 
     8 int main()
     9 {
    10     int n;
    11     scanf("%d",&n);
    12     for (int i=0;i<n;i++) scanf("%d",&l[i]);
    13     sort(l,l+n);
    14     __int64 ans;
    15     ans=0;  
    16     for (int i=0;i<n-1;i++)
    17     {
    18         l[i+1]=l[i]+l[i+1];
    19         ans+=l[i+1];
    20         int j=i+1;
    21         while ((l[j]>l[j+1]) && (j+1<n))    //不要忘记j+1<n这个条件,否则如果交换到最后就换变成0 
    22         {
    23             int temp=l[j];
    24             l[j]=l[j+1];
    25             l[j+1]=temp;
    26             j++;
    27         }
    28     }
    29      cout<<ans<<endl;
    30 }
    View Code

    对于书中标程的一些说明

    书中的标程的作法是先将前两个数据排序,让指针i,j指向前两个数据。再与后面的数据比较,移动指针使得每次指向的两个数据都是最小的两个数据。将两个数据合并,加到结果里。最后用这L[i]+L[j]替换L[i],将最后一位前移用L[N-1]替换L[j]。

    要重点强调的是,在替换数据前需要判断,如果指针i指向的位置是倒数第二位,即N-1,则交换i和j,否则L[i]+L[j]会被移动到最后一位忽略掉,而L[N-1]则会保留下来,这是错误的。

    本题重点

    Huffman的知识点要注意。网上很多解题报告说要用STL优先队列、堆的,其实就这道题而言并不需要,不过我可能都打算写一写。以后有空回来补上这两种解法的(2015-7-4更新:已补上,详见POJ3253解题报告)。

  • 相关阅读:
    code第一部分数组:第十七题 爬楼梯问题
    code第一部分数组:第十六题 数组表示数,加一操作
    code第一部分数组:第十五题 矩阵翻转(图像翻转)
    code第一部分:数组 第十四题 雨水问题
    code第一部分数组:第十题:移除数组中目标元素
    [java 2020-04-24] Maven多模块项目+SpringBoot,编译失败:程序包xxx不存在
    [java 2020-04-24] springboot生成的jar包没有主类和依赖jar包
    [java 2019-12-16] SimpleDateFormat格式化跨年显示格式错误
    [2019-11-22]马说
    [linux 2019-10-23] linux可以创建多少个用户
  • 原文地址:https://www.cnblogs.com/iiyiyi/p/4596148.html
Copyright © 2020-2023  润新知