贪心
贪心
引言
贪婪是一种人类本能的东西,贪心算法也是最接此人类平常思维的一种解题策略。其实,在解决一些日常问题时,我们本能就怀着对目标最直观、 最简单、最高效的思路,其中往往就带有贪心思想的影子。虽然它不能保证求得的最后解一定是最佳的,但是它可以为某些问题确定一个可行性范围。在某些范围内,贪心算法是我们的最佳选择。
贪心法的基本思想
贪心法是从问题的某个初始解出发,采用逐步构造最优解的方法,向给定的目标前进。在每一个局部阶段,都做一个在当前“看上去”最优的决策,并期望通过每一次所做的局部最优选择产生出一个全局最优解。做出贪心决策的依据称为“贪心策略”。要注意的是,贪心策略一旦做出,就不可再更改。
与搜索、动态规划不同的是,贪心只是一种策略或方法,而不是算法。推进的每一步不是依据某一个固定的递推式,而是做一个当时“看似最佳”的贪心选择(操作),不断将问题归纳为更小的相似子问题。所以,归纳、分析、选择正确合适的贪心策略,是解决贪心问题的关键。
贪心算法的步骤
从问题的某一初始解出发
由所有解元素组合成问题的一个可行解
贪心法的正确性证明
对于一个问题,如果想用贪心法求解,首先要想到基于某种“序”或者“规则”的贪心策略。
其次还要能证明其正确性。 要严格证明一个贪心算法的正确性是很困难的,目前最有效的一种方法叫“矩阵胚理论”,但是很复杂。信息学竞赛中常用的贪心证明方法,一般有反证法、构造法、调整法。其实,即使一个贪心算法是不完全正确的,也可以努力寻找一些调整方法,或 制定多种贪心策略,通过调整优化、比较择优来争取得到最优解 ,甚至也可以先得到一个“较优”解,然后,在此基础上进行搜索剪枝或分支定界。
构造法
根据描述的算法,用贪心的策略依次构造出一个解,可证明一定是合法的解。即用贪心法找可行解。
反证法
用贪心的策略,依次构造出一个解
调整法
用贪心的策略,依次构造出一个解
题目
A:修理牛棚
这题可以使用调整法。
考虑如何使总长度最短。因为有些位置没有牛,所以有些牛棚是不需要覆盖木板的。如果能每个牛棚分单独一个木板,一定总长度是最小的。但是由于木板数量有限制,所以我们有时只能用一个比较长的木板的时候,顺便把没有牛的牛棚也覆盖了。就造成了浪费。所以一定是使用最大数目木板的。
第一头牛的位置在
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1100;
int m,c,a[N],b[N],sum;
bool cmp(int a1,int a2)
{
return a1>a2;
}
int main()
{
scanf("%d%d",&m,&c);
if(m>=c)//特判
{
printf("%d",c);//如果可以使用的木板数够用,可以一个牛棚用一块木板
return 0;
}
for(int i=1;i<=c;i++) scanf("%d",&a[i]);
sort(a+1,a+c+1);
for(int i=1;i<c;i++) b[i]=a[i+1]-a[i]-1;//算出每个空隙的长度
sort(b+1,b+c,cmp);//把空隙长度从小到大排序
for(int i=1;i<m;i++) sum+=b[i];//选择最长的M-1个空隙
printf("%d",a[c]-a[1]+1-sum);
return 0;
}
B:删数问题
这题可以使用构造法
如果采取“找最大的数字删除”这种贪心思想,答案显然是错误的。比如
为了使数字变大,某个数字删去后一定要比原来大,于是我们得出贪心策略:每一步总是选择一个使剩下的数最小的数字删去,即按高位到低位的顺序搜索,若各位数字递增,则删除最后一个数字;否则,删除第一个递减区间的首字符,这样删一位便形成了一个新数字串。然后回到数字串首,按上述规则再删下一个数字。重复以上过程
这样, 问题就转化为“求一个数字串的递减区间首字符” 。
还有 两个细节问题 :
-
删除后会有前导
0 的情况,要特判一下。 -
如果整个序列是升序的但是还没删完,就从后面删起。
#include<iostream>
#include<cstdio>
using namespace std;
const int N=310;
string s;
int n,a[N],m,lastp;
bool r;
//因为某些数会被删除,所以导致i-1不一定是i的上一个数,可以用-1标记删除的数,使用循环从i-1开始往前遍历,找出第一个不等于-1的数
int last(int p)//返回上一个(未被删除的)数
{
for(int i=p-1;i>=1;i--)
{
if(a[i]!=-1) return i;
}
return 0;
}
int main()
{
cin>>s>>m;
for(int i=0;i<s.size();i++) a[i+1]=s[i]-'0';//将字符串转化为数字
n=s.size();
for(int i=2;i<=n;i++)
{
lastp=last(i);//得到上一个数
if(lastp&&a[lastp]>a[i])
{
a[lastp]=-1;//删除,标记为-1
m--;
i--;
}
if(!m) break;
}
for(int i=n;i>=1;i--)//如果整个序列是升序的但是还没删完,就从后面删起。
{
if(!m) break;
if(a[i]!=-1)
{
a[i]=-1;
m--;
}
}
for(int i=1;i<=n;i++)
{
if(a[i]>0) r=true;//特判前导0
if(r&&a[i]!=-1) printf("%d",a[i]);
}
return 0;
}
C:蜈蚣
这题要运用 最不利原则 (就是要尽量取多)
贪心过程:
-
要穿的
C \times F 只袜子都要取。(注意同一个颜色的袜子可以给多只蜈蚣穿,所以取第i 种颜色的袜子应该是a_i \div F ) -
根据最不利原则,让蜈蚣凑不齐
F 只颜色相同的袜子,所以小于F 的袜子也要取。 -
判断
-1 (袜子不够穿) -
如果取了
F-1 只后还能满足,则取(cnt-1)(F-1) 个(cnt 是\ge F 的颜色种类个数) -
如果取了
F-1 只后不能满足,更新数组a (a_i=a_i-(a_i-(F-1)) \div F \times F ),接着有cnt-c 只蜈蚣还没满足,所以还能取(cnt-c) \times (F-1) 只,最后再取a_i-F
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=110;
int c,f,n,a[N],cnt,sum,num;
int main()
{
scanf("%d%d%d",&c,&f,&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
cnt+=a[i]/f;
if(a[i]<f) sum+=a[i];
}
if(cnt<c)
{
printf("-1");
return 0;
}
sum+=c*f;//把要穿的取了
cnt=0;
for(int i=1;i<=n;i++)
{
if(a[i]>=f) cnt++;
num+=(a[i]-(f-1))/f;
}
if(num>=c) sum+=(cnt-1)*(f-1);//取了f-1只后还能满足
else//取了f-1只后不能满足
{
c-=num;
for(int i=1;i<=n;i++) a[i]-=(a[i]-(f-1))/f*f;//更新数组a
sort(a+1,a+n+1);
sum+=(cnt-c)*(f-1);//有cnt-c只还没满足
for(int i=n,j=1;j<c;i--,j++) sum+=a[i]-f;
}
printf("%d",sum);
return 0;
}
D:安装饮水机
这题是一道区间贪心。
我们既然要满足安装的饮水机最少,又要满足每个人都要喝到水。我们可以先求出
于是得出贪心策略:求出区间左右端点
证明:
每一个区间都一定包含一个点,所以当前选择方案是一种合法方案。假设最终答案是
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1100,M=150100;
int n,s,w,cnt;
struct node
{
int l,r;
}a[N];
bool f,r[M];
bool cmp(node a1,node a2)
{
return a1.r<a2.r;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&s,&w);
//算出每个人能走到的最左和最右位置
a[i].l=s-w;
a[i].r=s+w;
if(a[i].l<0) a[i].l=0;
}
sort(a+1,a+n+1,cmp);//按右端点大小排序
for(int i=1;i<=n;i++)
{
f=false;
for(int j=a[i].l;j<=a[i].r;j++)//找区间内是否已经有饮水机
{
if(r[j])
{
f=true;
break;
}
}
if(!f)//如果没有,则在r上建立一个
{
r[a[i].r]=true;
cnt++;
}
}
printf("%d",cnt);
return 0;
}