贪心算法

本文介绍了贪心算法的概念,通过【NOIP2007】守望者的逃离和【USACO Dec07】书架等题目,阐述了贪心算法的应用和解题思路。强调贪心算法的特点是局部最优解也是全局最优解,并提供了相关题目解题示例,帮助读者理解贪心策略。

从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解 满足局部最优解是全局最优解的DP 可以用数学证明。
这是贪心的定义。
但是贪心怎么说呢,这个算法比较神奇,不能像递推一样讲什么什么思路,还是具体情况具体分析吧。先来从一道比较经典的贪心题讲起吧。
[NOIP2007] 守望者的逃离
恶魔猎手尤迪安野心勃勃.他背叛了暗夜精灵,率深藏在海底的那加企图叛变:守望者在与尤迪安的交锋中遭遇了围杀.被困在一个荒芜的大岛上。为了杀死守望者,尤迪安开始对这个荒岛施咒,这座岛很快就会沉下去,到那时,岛上的所有人都会遇难:守望者的跑步速度,为17m/s, 以这样的速度是无法逃离荒岛的。庆幸的是守望者拥有闪烁法术,可在1s内移动60m,不过每次使用闪烁法术都会消耗魔法值10点。守望者的魔法值恢复的速度为4点/s,只有处在原地休息状态时才能恢复。

现在已知守望者的魔法初值M,他所在的初始位置与岛的出口之间的距离S,岛沉没的时间T。你的任务是写一个程序帮助守望者计算如何在最短的时间内逃离荒岛,若不能逃出,则输出守望者在剩下的时间内能走的最远距离。注意:守望者跑步、闪烁或休息活动均以秒(s)为单位。且每次活动的持续时间为整数秒。距离的单位为米(m)。
【输入】
输入文件escape.in仅一行,包括空格隔开的三个非负整数M,S,T。
【输出】
输出文件escape.out包含两行:
第1行为字符串”Yes”或”No” (区分大小写),即守望者是否能逃离荒岛。
第2行包含一个整数,第一行为”Yes” (区分大小写)时表示守望着逃离荒岛的最短时间
第一行为”No” (区分大小写) 时表示守望者能走的最远距离。
【输入输出样例1】
escape.in
39 200 4

escape.out
No
197
【输入输出样例2】
escape.in
36 255 10

escape.out
Yes
6
【限制】
30%的数据满足: 1 <= T<= 10, 1 <=S<= 100
50%的数据满足: 1 <= T <= 1000, 1 <= S <= 10000
100%的数据满足: 1 <= T <= 300000, 0 <= M<=1000 1 <=S <= 10^8

这一道题是这几年来noip出的不多的几道贪心可以拿满的题(当然动态规划好像是正解)。大家看着道题有没有想要懵逼的节奏。这道题的贪心攻略有点复杂,所以我们先从简单的题学起,大概看完这篇博客你就可以解开上面这道题。
下面就是华丽丽的入门贪心题。(我认为的,求不打脸)
[USACO Dec07] 书架
Farmer John最近为奶牛图书馆购买了一个书架,书架的下层很快装满了书,现在只剩下了顶层书架有空间。
在 N (1 ≤ N ≤ 20,000)头牛中,第i头牛的身高为 Hi (1 ≤ Hi ≤ 10,000)。书架的高度为 B (1 ≤ B ≤ 2,000,000,007),且 B 小于所有奶牛的身高之和。
书架的顶层高于最高的牛的身高,但是若干个奶牛可以站成一摞,这样总高度就是它们的身高之和。总高度应大于等于书架的高度,奶牛才能取到书。
但是越多的奶牛站成一摞,它们就越危险。你的工作就是找到一个集合,使得尽量少的奶牛站成一摞,当然,它们要能取到顶层的书。
输入
第 1 行: 两个整数: N , B
第 2..N+1 行: 第 i+1 行包含一个整数 Hi
输出
第 1 行: 一个整数,这个使得尽量少的奶牛站成一摞取得顶层的书的集合中奶牛的个数。
样例输入
6 40
6
18
11
13
19
11
样例输出
3

事先说明,这是一道万年老水题……这道题的贪心攻略很简单,就是每次都让当前最低的一头奶牛搭上去,记录当前的高度和牛的个数,当牛的高度大于书架高度的时候,就输出牛的个数。然后这道题就AC掉了。
题很水吧,不过我主要想讲的是这道题的贪心思路。贪心就是这样,用句俗话说就是怎么贪怎么来。(想知道具体定义的参见文章第一句话)
试着敲一下吧(想看对不对的孩子们可以随便找网站,USBCO的题还是很好找到的)
怎么样,找到那种感觉了吗?
如果你说好简单,贪心都这么简单吗?我只想告诉你,是的。
next one
对了贴下上一道题的标程。

#include<iostream>
#include<iomanip>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<memory>
#include<algorithm>
#include<string>
#include<set>
#include<map>//把可能用到的库都敲出来,这是一个好习惯。
using namespace std;
int a[100000];
int main()
{
    freopen("shelf.in","r",stdin);  
    freopen("shelf.out","w",stdout);
    int n,gao,zhi=0;
    cin>>n>>gao;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    sort(a+1,a+n+1);//排序
    for(int i=n;i>=1;i--)//循环记录高度和牛的个数
    {
        gao-=a[i];
        zhi++;
        if(gao<=0)//如果够高,跳出循环。
            break;
    }
    cout<<zhi<<endl;
    return 0;
}

[NOIP2007] 纪念品分组
【题目描述】

元旦快到了,校学生会让乐乐负责新年晚会的纪念品发放工作。为使得参加晚会的同学所获得的纪念品价值相对均衡,他要把购来的纪念品根据价格进行分组,但每组最多只能包括两件纪念品,并且每组纪念品的价格之和不能超过一个给定的整数。为了保证在尽量短的时间内发完所有纪念品,乐乐希望分组的数目最少。
你的任务是写一个程序,找出所有分组方案中分组数最少的一种,输出最少的分组数目。

【输入】
输入文件group.in包含n+2行:
第1行包括一个整数w,为每组纪念品价格之和的上限。
第2行为一个整数n,表示购来的纪念品的总件数。
第3~n+2行每行包含一个正整数pi(5<=pi<=w),表示所对应纪念品的价格。

【输出】
输出文件group.out仅一行,包含一个整数,即最少的分组数目。

【输入样例】

100
9
90
20
20
30
50
60
70
80
90

【输出样例】
6

【限制】
50%的数据满足:l<=n<=15
100%的数据满足:1<=n<=30000,80<=w<=200

有时候我会在做贪心的时候脑海中会突然蹦出来一句话“要贪心,先排序”,感觉所有贪心都是这样。这道题很简单的就可以看出来,做这道题肯定要让每一组的和都要尽量逼近最大值,因此贪心攻略就出来了。去求现有数据的最大值和最小值的和,如果和小于上限,那就把最小值和最大值分到一组中,然后把最小值和最大值踢出数列。如果大于,就把最大值分到一组,之后把最大值踢出数列。记录次数,之后AC全剧终。
由于我的程序比较慢,所以贴出大神的。

  #include<iostream>
    #include<cstdio>
    #include<algorithm>
    int a[30001];
    using namespace std;
    int wjh()
    {
    freopen("group.in","r",stdin);
    freopen("group.out","w",stdout);
    int w,n,s=0,j,k=1;
    cin>>w>>n;
    j=n;
    for(int i=1;i<=n;i++)
    cin>>a[i];
    sort(a+1,a+n+1);
    while(k<j)  
    {
    if(a[k]+a[j]<=w)
    {
    k++;s++;j--;
    }
    else
    {s++;j--;}
    }
    if(k==j&&a[k]<w)
    s++;
    cout<<s;
    return 0;
    }
    int W=wjh();
    int main(){;}

好好体会。
如果你说你有些看不懂,看来我只能放出大技了。
来看我的白痴代码

#include<iostream>
#include<iomanip>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<memory>
#include<algorithm>
#include<string>
#include<set>
#include<map>//再次强调提前把库尽量加载,这是个好习惯。
int a[30001];//超过10000开全局,这个不必解释了吧?
using namespace std;
int main()
{
    freopen("group.in","r",stdin);
    freopen("group.out","w",stdout);//文件输入输出
    int n,sum=0,zong,zhi=1,zhen;
    cin>>zong>>n;
    zhen=n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];//输入
    }
    sort(a+1,a+n+1);//排序
    while(!(zhi>zhen) )  //循环的思路就是从开头和结尾同时开始做,在理想情况下只需付正常一半的代价,最差也会比正常循环一遍少很多计算量。      
    {
        if(a[zhi]+a[zhen]<=zong)//如果最大加最小小于等于极限。
        {
            sum++;
            zhi++;
            zhen--;


        }
        else//反之
        {
            sum++;
            zhen--;
        }

    }

    cout<<sum<<endl;
}

这样是不是就很好看懂了?
怎么样,有没有发现贪心题做着很舒服,我要开始加大难度了。
[NOIP2004] 合并果子
【问题描述】
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有3种果子,数目依次为1,2,9。可以先将1、2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。
【输入文件】
输入文件fruit.in包括两行,第一行是一个整数n(1<=n<=10000),表示果子的种类数。第二行包含n个整数,用空格分隔,第i个整数ai(1<=ai<=20000)是第i种果子的数目。
【输出文件】
输出文件fruit.out包括一行,这一行只包含一个整数,也就是最小的体力耗费值。输入数据保证这个值小于231。
【样例输入】
3
1 2 9
【样例输出】
15
【数据规模】
对于30%的数据,保证有n<=1000:
对于50%的数据,保证有n<=5000;
对于全部的数据,保证有n<=10000。
(其实这道题的难度和守望者的逃离差不了多少,但是本人认为守望者的细节更复杂但策略比较好想,如果你可以AC掉这道题的话,那么守望者就没问题了。)
这道题是有点难度的,难就难在这道题是有两个策略的,到最后需要把两个策略所需代价相比较,哪一个更小就使用哪个。
老规矩,要贪心先排序。
建议自己思考,如果真的不会的话再来看题解

还不会?好吧好吧,看这个
设集合 n=i+j+k;F(n)表示对集合n的权值(即果子重量), Fruit(n)为体力耗费。
决策1
Fruit(n)=F(k)+F(i)+F(k)+F(i)+F(j);//先合并i,再合并j。
决策2
Fruit(n)=F(k)+F(j)+F(k)+F(i)+F(j);//先合并j,在合并i。
2种方式对比会发现仅含不同项F(i),F(j),只许比较i,j大小选取小的先合并便可保证从某一相同初始状态转换至相同末状态的花费最小。
这样就可以得出答案了。
来我们先膜拜大牛的程序。

#include<cstdio>
#include<iostream>
#include<queue>
std::priority_queue<long long int>q;
inline long long int read(){
    long long int k=0,c=0;
    do{c=getchar();}while(c<'0'||c>'9');
    do{k=k*10+c-'0';c=getchar();}while(c>='0'&&c<='9');
    return k;
}
int doing(){
    long long int ans=0,ppt=0;
    int n=read();
    for(int i=1;i<=n;i++)q.push(-1*read());
    while(--n){
        ppt=0;
        //printf(">>>>>>>>>>>>>%lld ",q.top()*-1);
        ppt+=(-1)*q.top();q.pop();
        //printf("%lld ",q.top()*-1);
        ppt+=(-1)*q.top();q.pop();
        ans+=ppt;q.push(ppt*(-1));
        //printf("%lld",ans);
    }
    printf("%lld",ans);
    return 1;
}
FILE*_=freopen("fruit.in","r",stdin);
FILE*__=freopen("fruit.out","w",stdout);
int a=doing();
int main(){;}

这个时间复杂度很低很低,跑一遍数据不超过0.0001秒。有兴趣的童鞋可以来推一下。
来上我的程序(优化好多,可能会有些看不懂)

#include <iostream>
 #include <cstdio>
 #include <cstdlib>
 #include <cmath>
 using namespace std;
 int n,a[30001];
 void heapdown(int k,int n)
 {
     int i;
     while (k+k<=n)   
     {
        i=k+k;
        if (i<n&&a[i]>a[i+1])

            i++;
        if (a[k]>a[i])
         {
             swap(a[i],a[k]);
             k=i;
         }
         else
             return;
    }
 }
 void init()
 {
     int i;
    scanf("%d",&n);
     for (i=1;i<=n;i++)
         scanf("%d",&a[i]);
 }
 void slove()
 {
     int i,sum=0;
     for (i=n/2;i>0;i--)
         heapdown(i,n);
     while (n>1)
     {
         swap(a[1],a[n]);
         heapdown(1,n-1);
         n--;
         swap(a[1],a[n]);
         heapdown(1,n-1);
         a[n]+=a[n+1];
         sum+=a[n];
     }
     printf("%d\n",sum);
 }
 int main()
 {
     freopen("fruit.in","r",stdin);
    freopen("fruit.out","w",stdout);
    init();
     slove();
     fclose(stdin);
     fclose(stdout);
    return 0;
 }

这个就不给提示了,看不懂的话留言,我再详细说明。
还有守望者的逃离就当做练习题吧。下一篇关于深搜的文章会在最后给出题解和标程。
关于贪心的题型还有好多,这些只是一个小类型,在NOIP和NOI是会有符合某个贪心策略的数据的,俗话说到好,贪心只能过样例,贪心在联赛上好像就是用来骗分的(一不小心说错真话,不过这几年的纯贪心好像真的不多,有也只会是T1。)话不多说,贪心就这样结束了,期待我的下一篇深搜博客吧。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值