概念
实际上是求局部最优解的方法,而使用这一算法时我们总是希望它能通过求局部最优而达到整体最优。
学术地说,可利用贪心算法解决的问题必须具备最优子结构(一个问题的最优解包含其子问题的最优解)。且贪心每一步的选择都会对结果产生直接影响,无法回退。
一种可能只过样例的算法
它的算法特点决定了它最难的部分在于证明,所以我们常常采用显然法反证法。
基本思路
从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到算法中的某一步不能再继续前进时,算法停止。
例题
#171 智力大冲浪
元旦快到了,校学生会让乐乐负责新年晚会的纪念品发放工作。为使得参加晚会的同学所获得的纪念品价值相对均衡,他要把购来的纪念品根据价格进行分组,但每组最多只能包括两件纪念品, 并且每组纪念品的价格之和不能超过一个给定的整数。为了保证在尽量短的时间内发完所有纪念品,乐乐希望分组的数目最少。
你的任务是写一个程序,找出所有分组方案中分组数最少的一种,输出最少的分组数目
这题类似Paired up那题,此类排完序头尾相加处理即可。不多赘述。
#174 修理牛棚
在一个暴风雨的夜晚,农民约翰的牛棚的屋顶、门被吹飞了。 好在许多牛正在度假,所以牛棚没有住满。 有些牛棚里有牛,有些没有。 所有的牛棚有相同的宽度。 自顶遗失以后,农民约翰必须尽快在牛棚之上竖立起新的木板。 他的新木材供应者将会供应他任何他想要的长度,但是供应者只能提供有限数目的木板。 农民约翰想将他购买的木板总长度减到最少。 给出 M(1<= M<=50),可能买到的木板最大的数目;S(1<= S<=200),牛棚的总数;C(1 <= C <=S) 牛棚里牛的数目,和牛所在的牛棚的编号stall_number(1 <= stall_number <= S),计算拦住所有有牛的牛棚所需木板的最小总长度。 输出所需木板的最小总长度作为的答案。
逆向思维。假设一开始用一块巨大的木板把所有牛棚拦住。将相邻两头牛的距离
降序排序,挖掉m-1个空,就okay啦。
头尾边界需要稍作考虑。
#include<bits/stdc++.h>
using namespace std;
int m,s,c;
int a[205]={};
int b[205]={};
int ans=0;
int main()
{
cin>>m>>s>>c;
if(m>c) {
cout<<c;return 0;
}
ans=s;
for(int i=1;i<=c;i++) cin>>a[i];
sort(a+1,a+c+1);
for(int i=1;i<=c;i++) b[i]=a[i]-a[i-1]-1;
ans=ans-b[1]-(s-a[c]);//删去第一个牛棚~第一个有牛的牛棚&最后一个有牛的牛棚~最后一个牛棚。而且此处删除并不会影响它是一块(重音)巨大的木板。
sort(b+2,b+c+1);//b[1]不能排进去。
for(int i=1;i<m;i++) ans-=b[c-i+1];
cout<<ans;
return 0;
}
#175 [haoi2009]巧克力
有一块nm的矩形巧克力,准备将它切成nm块。巧克力上共有n-1条横线和m-1条竖线,你每次可以沿着其中的一条横线或竖线将巧克力切开,无论切割的长短,沿着每条横线切一次的代价依次为y1,y2,…,yn-1,而沿竖线切割的代价依次为x1,x2,…,xm-1。例如,对于下图6*4的巧克力,
我们先沿着三条横线切割,需要3刀,得到4条巧克力,然后再将这4条巧克力沿竖线切割,每条都需要5刀,则最终所花费的代价为y1+y2+y3+4*(x1+x2+x3+x4+x5)。 当然,上述简单切法不见得是最优切法,那么怎样切割该块巧克力,花费的代价最少呢?
显而易见,某一刀在后面切的代价绝不会低于当前切的代价,所以每一块都越早切越好。更显而易见的是,越大的越应该早切,以减小这一刀的实际代价。
#include<bits/stdc++.h>
using namespace std;
int m,n;
struct choco{
int v;
int f;
}a[20020];
int b[3]={1,1,1};
bool mycmp(choco a,choco b)
{
return(a.v>b.v);
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n+m-2;i++)
{
cin>>a[i].v;
a[i].f=i/n;
}//玄学的标记横or竖的方法(实则是懒得打两遍for循环)。
sort(a+1,a+n+m+1,mycmp);//管他横竖,一锅排完。
int s=0;
for(int i=1;i<=n+m-2;i++)
{
if(a[i].f==0){
s+=a[i].v*b[1];b[0]++;
}else
{
s+=a[i].v*b[0];b[1]++;
}
}//b数组分别记录横竖切了几刀。
cout<<s;
return 0;
}
#176 [usaco 2009 dec]游荡的奶牛
FJ 有 N (1 <= N <= 50,000)头牛,FJ的草地可以认为是一条直线。 每只牛只喜欢在某个特定的范围内吃草。第i头牛喜欢在区间(S_i, E_i)吃草, (1 <= S_i < E_i; S_i < E_i<= 100,000,000).
奶牛们都很自私,他们不喜欢和其他奶牛共享自己喜欢吃草的领域,因此FJ要保证任意两头牛都不会共享他们喜欢吃草的领域。如果奶牛i和奶牛j想要同时吃草,那么要满足: S_i >= E_j 或者 E_i <= S_j. FJ想知道在同一时刻,最多可以有多少头奶牛同时吃草?
下面的样例有5头奶牛:
这5头奶牛的范围分别是:(2, 4), (1, 12), (4, 5), (7, 10) (7, 8)。 显然,第1、3、4,共3只奶牛可以同时吃草,第1、3、5也可以。
经典线段覆盖问题。我采用右端点从小到大排序。以当前线段为基准,如果下一条线与它相交,那么s–,因为下一条线的右端点更靠右,可能会覆盖更多线段。如果没有相交,更新基准。……不想证明。
#include<bits/stdc++.h>
using namespace std;
int n;
struct cow{
int l;
int r;
}a[50020];
bool mycmp(cow a,cow b)
{
return(a.r<b.r);
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i].l>>a[i].r;
sort(a+1,a+n+1,mycmp);
int s=n;
int t=a[1].l,w=a[1].r;
for(int i=2;i<=n;i++)
{
if((a[i].l<=t&&a[i].r>=w)||(a[i].l>t&&a[i].l<w)||(a[i].r>t&&a[i].r<w)) s--;//区间重叠有三种情况。
else t=a[i].l,w=a[i].r;
}
cout<<s;
return 0;
#177 穿墙人
穿墙术是现代魔术表演中常见的一个节目。魔术中的穿墙人可以穿过预先设计好的若干道墙。所有的墙被安置在一个网格区域中,如图所示,‘?’表示墙所占据的网格,所有的墙都水平放置,宽度为1个单位,但长度可能不同。任何两道墙不会相互重叠,即任何一个‘?’不能同时属于两道或两道以上的墙。穿墙人的能量有限,每次表演至多只能穿过k道墙。 表演开始时,主持人让观众任选网格的某一列,然后穿墙人开始沿着此列从网格的上端穿过中间的每一道墙到达网格的底部。但如果观众所选择的那一列中有大于k道墙,则穿墙人的表演会失败。 例图所示,如果k=3,则穿墙人可以自上而下地穿过除第6列外的任何列,因为只有第6列需要穿越4道墙,但穿墙人在同一列上最多只能穿过3道墙。
当所有的墙给出之后,如果知道穿墙人当前的能量K,我们希望移去最少数目的墙,才能使得穿墙人能够穿越任何一列。 对于图中的例子,如果k=3,穿墙人无法穿越第6列,但是只要把第3行的墙移去(不唯一),则穿墙人就可以穿过任意一列。因此对于这个例子最少需要移去1道墙。
题面讲得很烦,一言以蔽之,线段覆盖的引申问题。仍然是右端点升序,后续处理更简单。
#include<bits/stdc++.h>
using namespace std;
int n,k;
struct wall{
int t,w;
}niu[105];
int f[105]={};//f记录该列已有的墙数。
int ans=0;
bool mycmp(wall a,wall b)
{
return(a.w<b.w||(a.w==b.w&&a.t>=b.t));//同理排序。
}
void work()
{
int l=niu[1].t;int r=niu[1].w;
for(int i=l;i<=r;i++) f[i]++;//先处理第一条。
for(int i=2;i<=n;i++)
{
for(int j=niu[i].t;j<=niu[i].w;j++)
if(f[j]<=k-1) f[j]++;else
{
ans++;break;
}//如果这一列已到k道墙,那就拆墙吧。
}
}
int main()
{
int a,b,c,d;
cin>>n>>k;
for(int i=1;i<=n;i++)
{
cin>>a>>b>>c>>d;
niu[i].t=min(a,c);
niu[i].w=max(a,c);//行并没什么卵用,权当不存在,题面最坑的地方在于输入的两个端点并没有按左右排序。
}
sort(niu+1,niu+n+1,mycmp);
work();
cout<<ans;
return 0;
}
#180 【noip2010普及】三国游戏
小涵很喜欢电脑游戏,这些天他正在玩一个叫做《三国》的游戏。 在游戏中, 小涵和计算机各执一方, 组建各自的军队进行对战。 游戏中共有 N位武将 (N为偶数且不小于 4) ,任意两个武将之间有一个“默契值” ,表示若此两位武将作为一对组合作战时,该组合的威力有多大。游戏开始前,所有武将都是自由的(称为自由武将,一旦某个自由武将被选中作为某方军队的一员,那么他就不再是自由武将了) ,换句话说,所谓的自由武将不属于任何一方。游戏开始,小涵和计算机要从自由武将中挑选武将组成自己的军队,规则如下:小涵先从自由武将中选出一个加入自己的军队,然后计算机也从自由武将中选出一个加入计算机方的军队。接下来一直按照“小涵→计算机→小涵→……”的顺序选择武将,直到所有的武将被双方均分完。然后,程序自动从双方军队中各挑出一对默契值最高的武将组合代表自己的军队进行二对二比武,拥有更高默契值的一对武将组合获胜,表示两军交战,拥有获胜武将组合的一方获胜。 已知计算机一方选择武将的原则是尽量破坏对手下一步将形成的最强组合,它采取的具体策略如下:任何时刻,轮到计算机挑选时,它会尝试将对手军队中的每个武将与当前每个自由武将进行一一配对,找出所有配对中默契值最高的那对武将组合,并将该组合中的自由武将选入自己的军队。 下面举例说明计算机的选将策略,例如,游戏中一共有 6个武将,他们相互之间的默契值如下表所示。
![]()
双方选将过程如下所示:
![]()
小涵想知道,如果计算机在一局游戏中始终坚持上面这个策略,那么自己有没有可能必胜?如果有,在所有可能的胜利结局中,自己那对用于比武的武将组合的默契值最大是多少? 假设整个游戏过程中,对战双方任何时候均能看到自由武将队中的武将和对方军队的武将。为了简化问题,保证对于不同的武将组合,其默契值均不相同。
题面好长………………
显然(又是显然),不管你多聪明,最最最大的值永远会被计算机抢走。基于贪心这一思想,我们应该很自然(并不自然)地想到退而求其次,即拿到次大值。
所以我们要做的事就变成了,从每个武将的次大值中找最大值。
由这道题可得,题目背景的大部分作用在于使题目看上去很难
注意输入的处理。
#include<bits/stdc++.h>
using namespace std;
int n;
int a[605][605]={};
int main()
{
cin>>n;
for(int i=1;i<n;i++)
{
for(int j=1;j<=n-i;j++)
{
int x;
cin>>x;
a[i][j+i]=a[j+i][i]=x;
}
}//其实处理还是蛮简单的,它给了我们表格的一半,对称填空即可。
int maxx=0;
for(int i=1;i<=n;i++)
{
sort(a[i]+1,a[i]+n+1);
maxx=max(maxx,a[i][n-1]);
}//没什么好说的了……
cout<<1<<endl<<maxx;
return 0;
}
#179 潜水
在马其顿王国的ohide湖里举行了一次潜水比赛。其中一个项目是从高山上跳下水,再潜水达到终点。这是一个团体项目,一支队伍由n人组成。在潜水时必须使用氧气瓶,但是每只队伍只有一个氧气瓶。最多两人同时使用一个氧气瓶,但此时两人必须同步游泳,因此两人达到终点的时间等于较慢的一个人单独游到终点所需要的时间。好在大家都很友好,因此任何两个人后都愿意一起游水。安排一种潜水的策略,使得最后一名选手尽量早的达到终点。ps:氧气瓶还必须潜水送回来^-^
从小学奥数开始接触的过河问题,终于会做了。
一般地,我们有两种方案可以选择:
例如,每个潜水员所需的时间为(已排序):1 2 2 2 2 2 7 8
(前两个人是搬运工,现在我们考虑怎么把第七、八个送过去。注此时搬运工都必须回来,因为他们的使命还没有结束!)
①第一个人带第八过去,自己回来,带第七过去,再回来。耗时:1+8+1+7+1=18.
等价于t=a[1]+a[i]+a[1]+a[i-1]=2*a[1]+a[i-1]+a[i];
②第一个人和第二个人过去,第一个人回来,第七个人和第八个人过去,第二个人回来。耗时:2+1+8+2=13.等价于t=a[2]+a[1]+a[i]+a[2]=a[1]+2*a[2]+a[i];
不能简单地认为②一定更优,因为你并不知道a[1]+a[i-1]与2*a[2]相比哪个比较大。
具体例子是下一对千里送的2 2,不详细说了。因为写到这里非常疲惫了。
所以每次送人要判断哪种方案更优。
#include<bits/stdc++.h>
using namespace std;
int n;
int a[1050]={};
int t=0;
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+n+1);
int i=n;
while(i>4)//少于4个人就特判,因为搬运工不一定要回去了。
{
int t1=2*a[1]+a[i]+a[i-1];
int t2=a[1]+2*a[2]+a[i];
t+=min(t1,t2);
i-=2;
}
if(i<4) t+=a[2]+a[1]+a[3];//nothing to say……
else t+=min(3*a[2]+a[1]+a[4],a[3]+a[4]+2*a[1]+a[2]);//仍然是两种方案。
cout<<t;
return 0;
}
#173 三值排序
排序是一种很频繁的计算任务。现在考虑最多只有三值的排序问题。一个实际的例子是,当我们给某项竞赛的优胜者按金银铜牌排序的时候。在这个任务中可能的值只有三种1,2和3。我们用交换的方法把他排成升序的。 写一个程序计算出,给定的一个1,2,3组成的数字序列,排成升序所需的最少交换次数。
哎,就喜欢这种干净利落的题面,不占版面。
两种解法。
暴力思想:先分别统计出1,2,3的个数a1,a2,a3。1~a1是属于1的区间,如果里面有2,那么就去2的区间找是1的让它们交换,如果2的区间没有1,那再去3的区间找;如果里面有3,那么先去3的区间找是1的让它们交换,如果没有,再去2的区间找1让它们交换。接下来处理第二个区间,此时1已经归位了,所以2的区间中凡不是2的都是3,直接去3的区间找,让它们交换。
保证此时最优。
该方法下我的代码真的巨无敌丑。
#include<bits/stdc++.h>
using namespace std;
int a[1010]={};
int b[3]={};
int n;
int main()
{
cin>>n;
int a1=0,a2=0,a3=0;
for(int i=1;i<=n;i++)
{
cin>>a[i];
if(a[i]==1) a1++;
else if(a[i]==2) a2++;
}
a3=a2+a1+1;
a2=a1+1;
a1=1;
int ans=0;
for(int i=a1;i<=a2-1;i++)
{
if(a[i]==2)
{
for(int j=a2;j<=n;j++)
if(a[j]==1) {
swap(a[i],a[j]);ans++;break;
}
}
if(a[i]==3)
{
for(int j=a3;j<=n;j++)
if(a[j]==1) {
swap(a[i],a[j]);ans++;break;
}
if(a[i]==3) {
for(int j=a2;j<=a3-1;j++)
if(a[j]==1) {
swap(a[i],a[j]);ans++;break;
}
}
}
}
for(int i=a2;i<=a3-1;i++)
{
if(a[i]!=2)
{
for(int j=a3;j<=n;j++)
if(a[j]==2){
swap(a[i],a[j]);ans++;break;
}
}
}
cout<<ans;
return 0;
}
甚至不想注释,就这样!
–
另一种,利用容斥原理。来自慕神,感谢赞助。
记录1的区间中不是1的个数x1,3的区间中不是3的个数x2,以及1的区间中3的个数y1,3的区间中1的个数y2.最少交换数=x1+x2-min(y1,y2).
最后-min(y1,y2)是因为那一部分被重复计算了2次。
#178 删数问题
输入一个高精度的正整数n(≤240位),去掉其中任意s个数字后剩下的数字按原左右次序将组成一个新的正整数。编程对给定的n和s,寻找一种方案,使得剩下的数字组成的新数最小。
好,仍然是我很喜欢的题面风格。
可惜我还是只会显然法,即如果从左到右扫一遍,如果这个数大于它右侧的数,那么这个数要被删掉。
int main()
{
string s;
cin>>s;
cin>>k;
while(k)
{
int flag=0;
if(s[0]=='0') {
s.erase(0,1);continue;
}//处理前导0.
for(int i=0;i<s.size();i++)
if(s[i]>s[i+1]) {
flag=1;s.erase(i,1);k--;break;
}
if(flag==0) break;
}
if(k!=0) s.erase(s.size()-k+1,k);
for(int i=0;i<s.size();i++) cout<<s[i];
return 0;
}
#181 赶吃花的牛
农夫John出去砍伐,让N头牛在草地上吃草。当他回来时吃惊的看到这些牛全部都跑到花园里在吃他的美丽花朵。他立即去把每头牛赶回它的牛栏。 i号牛每分钟要吃掉D_i朵花,距离自己的栏地要T_i分钟路程。不幸的是John每次只能赶一头牛回栏,再回到花园。请问这些牛最少要吃掉多少朵花?
数据范围
2 <= N <= 100,000
1 <= T_i <= 2,000,000
1 <= D_i <= 100
当初觉得很难,想不到贪心策略,被那一句同学一句性价比启发了……
(一定要注意,辣鸡的FJ赶完牛走回去还要耗费相同的时间!可恶啊)
比如说当前有两头牛A和B,如果先赶A,那么需要的时间是T=2*TA*VB,赶B则T=2*TB*VA。那么排序的标准就变成了TA*VB< TB*VA。
#include<bits/stdc++.h>
using namespace std;
int n;
struct cow
{
int t,v;
}a[100002];
bool mycmp(cow a,cow b)
{
return(b.t*a.v>a.t*b.v);
}
int main()
{
long long ans=0;
long long t=0;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i].t>>a[i].v;
sort(a+1,a+n+1,mycmp);
for(int i=2;i<=n;i++)
{
t+=a[i-1].t*2;
ans+=t*a[i].v;
}//这里暴力模拟算时间会TLE……所以换个方法。第i头牛总共要吃花的时间是它之前所有牛吃花时间*2的累加和。
cout<<ans;
return 0;
}
到这里吧。其实对贪心还是懵懵懂懂,很多时候想不到那个点上很容易GG啊。
本文介绍了贪心算法的基本概念和特点,强调了其在解决问题时寻找局部最优解以达到全局最优解的策略。通过一系列例题,如纪念品分组、修理牛棚、巧克力切割、奶牛吃草、穿墙人、三国游戏和潜水比赛等,阐述了贪心算法的应用,并提供了解题思路。这些例题展示了贪心算法如何通过每步选择来逐步逼近目标,以及在某些情况下,贪心算法可能难以证明其全局最优性。

&spm=1001.2101.3001.5002&articleId=78425976&d=1&t=3&u=3cde1229d73546a98c936af5b2ef3731)
1462

被折叠的 条评论
为什么被折叠?



