一、基本概念
贪心算法是指在对问题进行求解时,总是做出在当前看来是最好的选择。也就是说,不是从整体最优上加以考虑,他所做出的仅仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后不会影响以前的状态,只与当前的状态有关。
所以对采用的贪心策略一定要仔细分析其是否满足无后效性。
二、基本思路
1、建立数学模型来描述问题 //找到解题思路(往往最难)。
2、把求解的问题分解为若干个子问题。
3、对每一个子问题求解,找到子问题的局部最优解。
4、把子问题的解局部最优解合成原来解问题的一个解。
三、使用问题
贪心策略适用的前提是:局部最优策略能导致产生全局最优解。
四、实现框架
从问题的某一初始解出发;
while(能朝给定总目标前进一步)
{
利用可行的策略,求出可行解的一个解元素;
}
由所有解元素合成问题的一个可行解;
五、贪心策略的选择
因为贪心算法只能通过解局部最优解的策略来达到全局最优解,因此,一定要注意判断问题是否适合用贪心策略,找到的解是否是问题的最优解。
六、例题解析
1、删数问题
输入一个高精度正整数n,删掉其中任意s个数字后剩下的数字按原左右次序组成一个新的正整数。求组成的新的正整数最小。
分析:
贪心策略为每删除一个数都是剩下的数最小。即从高位到低位检索,如果数字递增,则删除最后一位,否则删除第一个递减区间的第一位。
例如:n=175438
s=4
删数过程如下:
n=175438 //删掉7;
=15438 //删掉5;
=1438 //删掉4;
=138 //删掉8;
=13 //解为13;
for(int i=1;i<=s;i++)//一共要删除s个字符
{
for(int j=0;j<len-1;j++)//从串首开始找,len是n的长度
if(n[j]>n[j+1])//找到第一个符合条件的
{
for(int k=j;k<len-1;k++)//删除字符n的第j个字符,后面的字符往前整
n[k]=n[k+1];
break;
}
len--;//长度减1
}
int j=0,m=len;
while(n[j]=='0'&&m>1)//删除串首无用的0,输出答案
{j++;m--;}
for(int i=j;i<len;i++)
cout<<n[i];
2、活动安排(典型贪心算法)
给出多组(begin,end),在一条时间线上有几组能同时进行。
#include <iostream>
#include <algorithm>
using namespace std;
struct pro
{
int begin,end;
};
bool cmp(pro a,pro b)
{
return a.end<b.end;
}
int main()
{
int n;
pro x[101];
int sum[101]={0};
int count=0;
while(cin>>n)
{
if(n==0)break;
for(int a=0;a<n;a++)
cin>>x[a].begin>>x[a].end;
sort(x,x+n,cmp);
for(int a=0,t=-1;a<n;a++)
{
if(t<=x[a].begin)
{
sum[count]++;
t=x[a].end;
}
}
count++;
}
for(int a=0;a<count;a++)
cout<<sum[a]<<endl;
return 0;
}
活动安排问题只求一组最多场次
升级版:求多次最多场次即没组最多场次算一次。
#include <iostream>
#include <algorithm>
using namespace std;
struct pro
{
int l,w;
bool s;//这里需要比上面多一个bool类型,用来判断非每组数据是否被检索过
};
bool cmp(pro a,pro b)
{
return a.l<b.l;
}
int main()
{
int t;
pro x[10000];
int n;
int sum;
cin>>t;
while(t--)
{
cin>>n;
sum=0;
for(int a=0;a<n;a++)
{cin>>x[a].l>>x[a].w;x[a].s=false;}
sort(x,x+n,cmp);
int nn=n;
while(nn)//这里的nn用来控制找了几次最多场次
{
int ans=-1;
int de=0;
for(int b=0;b<n;b++)
if(x[b].s==false)
if(ans<=x[b].w)
{
de++;//每多一个场次,总场次减1
x[b].s=true;
ans=x[b].w;
}
nn-=de;
sum++;//计算最多场次数
}cout<<sum<<endl;
}
return 0;
}
3、贪心问题要灵活运用“指针”。
例如贪心作业第四题 用木板铺泥地。
首先将给出的泥地的每组(begin,end)按照end或begin从小到大排序。
初始:point指向数据外。
(1)第一次的point一定是小于begin的,接下来如果point小于begin,那么就说明上一次铺的木板没有影响到本次,则需要重新定义point。
(2)如果point大于end,则要更新end的位置。
(3)如果上一次铺的木板影响到本次(即point>=begin),len变为end-point从此处给point加值。
for(int a=0;a<n;a++)
{
if(point>x[a].d)continue;//更新end位置
if(point>x[a].p)//上一次铺的木板影响到了本次
{
int len=x[a].d-point;//len变为end-point
if(len%l==0){sum+=len/l;point+=len;}
else {sum+=(len/l+1);point+=(len/l+1)*l;}
}
else {
int len=x[a].d-x[a].p;
if(len%l==0)
{
point=x[a].p+len;
sum+=len/l;
}
else
{
point=x[a].p+(len/l+1)*l;
sum+=(len/l+1);
}
}//上一次的模板没有影响到本次,需要重新定义point
}
4、性价比类问题
此类问题需要按照性价比排序,根据要求先做性价比高/低的事。
典型背包问题
#include <iostream>
#include <algorithm>
using namespace std;
struct bag
{
int w,v;
double c;
}x[1000];
bool cmp(bag a,bag b)
{
return a.c>b.c;
}//按性价比排序
int main()
{
int n,m;
cin>>n>>m;
for(int a=0;a<n;a++)
{
cin>>x[a].w>>x[a].v;
x[a].c=(x[a].v*1.0)/x[a].w;
}
sort(x,x+n,cmp);
double ans=0;//用来存储价值
for(int a=0;a<n;a++)
{
if(m<=0)break;//背包空间小于0
if(x[a].w<=m)
{ans+=x[a].v*1.0;
m-=x[a].w;}
else
{
ans+=m*x[a].c;
m=0;
}
}
cout<<ans<<endl;
}
心得体会
本周最主要学习了贪心算法,这仅仅是ACM算法家族中的一个算法,但通过一周的做题,我深深感受到了ACM的威力。不仅算法思路难找,而且实现的代码也不好想。即便作业上标明了贪心算法,可在做题的过程中总是不知从哪下手。这让我认识到ACM最注重的不是编码能力,而是在考验我们的思维能力。想要做出一道题,不能直接到开电脑就开始一步步的往电脑上敲代码,而是应该在读懂题目后首先思构出解题方法与解题步骤,需要先在本子上自己模拟一遍程序。
虽然本周题目很难,但我也学到了很多东西,学到了一些解题的思路,也让我的基本遍程能力得到了提升。在这周做题的过程中,我感触最深的一个词就是“区间”。基本上每道题都会涉及到区间,通过一周的练习,我感觉以后即使碰到的不是关于贪心算法的区间问题,我也能应对。
这一周的学习是我的一个开始,让我慢慢提升自身能力,同时也让我认识到自己的渺小。当真正比赛的时候,我恐怕很难ac出题目。这让我认清了现实,自己的能力还远远不足。所以在将来的学习中我必须更加刻苦,更加努力。去接受这个充满挑战的过程!