贪心算法简介
简单贪心
贪心法是求解一类最优化问题的方法,它总是考虑在当前状态下局部最优(较优)的策略,来使全局的结果达到最优(较优),显然如果采用较优而非最优的策略,得到的全局结果也不一定是最优的。而要获得最优结果,必须保证中间的每步策略都是最优的,因此严谨使用贪心策略来求解最优化问题需要对策略进行证明。一般来说,证明比较繁琐情况下,如果在想到某个似乎可行的策略,并且自己也无法举出反例,可以直接大胆使用。
实例一 月饼
给定所有种类月饼的库存量,总售价以及市场的最大需求量,试计算可以获得的最大收益。
输入格式:正整数N表示月饼种类数(N<=1000),正整数D表示最大需求量(D<=500万吨);随后一行给出N个正整数表示每种月饼的库存,最后一行给出N个正整数表示每种月饼的总售价。
输入:在一行中输出最大收益,以亿元为单位并精确到小数点后2位。
输入:
3 20
18 15 10
75 72 45
输出:
94.5
容易看出,答案是72+45/2=94.5。原则就是总是采用单价最高的月饼出售。
因此,思路如下:
(1)将所有月饼按照单价从高到低排序。
(2)从单价高的月饼开始枚举。如果该月饼的库存不足以填补需求,将该种月饼全部售出。需求量减少该种月饼的库存大小,收益增加该月饼的总售价。如果月饼库存足够填补需求,则只提供需求量大小的月饼。需求量减为0,收益增加。
#include <cstdio>
#include <algorithm>
using namespace std;
struct mooncake{
double store; //库存
double sell; //总售价
double price; //单价
}cake[1010];
//按照单价由高向低排序
bool cmp(mooncake a,mooncake b)
{
return a.price>b.price;
}
int main()
{
int n;
double D;
scanf("%d%lf",&n,&D);
for(int i=0;i<n;i++)
{
scanf("%lf",&cake[i].store);
}
for(int i=0;i<n;i++)
{
scanf("%lf",&cake[i].sell);
cake[i].price=cake[i].sell/cake[i].store;
}
sort(cake,cake+n,cmp);
double ans=0; //收益
for(int i=0;i<n;i++)
{
if(cake[i].store<=D)
{
D-=cake[i].store;
ans+=cake[i].sell;
}
else
{
ans+=cake[i].price*D;
break;
}
}
printf("%.2f\n",ans);
return 0;
}
实例二:组个最小数
给定数字0-9若干个,可以任意顺序排列这些数字,但必须全部使用。目标是使得最后得到的数尽可能小(0不可以为首位)。例如给定2个0,2个1,3个5,1个8,得到的最小数就是10015558。
输入格式:在一行中给出10个非负整数,顺序表示拥有数字0,1,......8,9的个数。十个数字的总个数不超过50,而且至少拥有1个非0数字。
输入
2 2 0 0 0 3 0 0 1 0
输出
10015558
策略:先从1-9中选择个数不为0的最小数输出,然后从0-9输出数字,每个数字输出次数就是其剩余次数。
#include <cstdio>
int main()
{
int count_num[10];
for(int i=0;i<10;i++)
{
scanf("%d",&count_num[i]);
}
for(int i=1;i<10;i++)
{
if(count_num[i]>0)
{
printf("%d",i);
count_num[i]--;
break;
}
}
for(int i=0;i<10;i++)
{
while(count_num[i]>0)
{
printf("%d",i);
count_num[i]--;
}
}
return 0;
}
区间贪心
区间不相交问题:给出N个开区间(x,y),从中选择尽可能多的开区间,使得这些开区间22没有交集。例如对于开区间
(1,3)(2,4) (3,5) (6,7)
最多可以选择3个开区间
(1,3) (3,5) (6,7)
互相没有交集。
首先考虑最简单的情况,如果开区间A1被开区间A2包含,那么显然选择开区间A1是最好的选择。
接着把所有开区间按照左端点x从大到小排序,如果去除掉区间包含的情况,必有y1>y2>y3>.....>yn成立。那么如何选择区间。如图所示:A1的右边有一段是一定不会和其他区间重叠的,如果把他去掉,那么A1就会被包含,这时应该选择A1.因此对于这种情况应该总是先选择
左端点最大的区间。
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn=110;
struct Inteval
{
int x,y;
}I[maxn];
bool cmp(Inteval a,Inteval b)
{
if(a.x!=b.x)
return a.x>b.x; //先按照左端点从大到小排序
else
return a.y<b.y; //左端点相同的按照右端点从小到大排序
}
int main()
{
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%d%d",&I[i].x,&I[i].y);
}
sort(I,I+n,cmp);
int ans=1;
int lastX=I[0].x;
for(int i=1;i<n;i++)
{
if(I[i].y<=lastX)
{
lastX=I[i].x;
ans++;
}
}
printf("%d\n",ans);
return 0;
}
类似问题:给出N个闭区间【x,y】,求最少需要确定多少点,才可以使得每个区间都至少有一个点。
类似,把区间按照左端点由大到小排序,如果A1被A2包含,则在A1区间内的点一定也在A2区间。我们只需要总是选择最左边端点即可,尽可能覆盖多的区间。
只需修改代码如下即可
I[i].y<=lastX;
I[i].y<lastX;
显然贪心算法用来解决一类最优化问题,并希望由局部最优策略来推得全局最优策略。贪心算法适用的问题一定满足最优子结构的问题。
变种
求区间的交集:合并有交集的区间[1,4,[2,6,[6,7 合并结束是[1,7
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int maxn=105;
struct Interval
{
int x,y;
}I[maxn];
bool cmp(Interval I1,Interval I2)
{
if(I1.x!=I2.x)
return I1.x<I2.x;
else
return I1.y>I2.y;
}
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
{
cin>>I[i].x;
cin>>I[i].y;
}
sort(I,I+n,cmp);
vector<Interval> res;
Interval temp=I[0];
for(int i=1;i<n;i++)
{
if(I[i].y<=temp.y)
continue;
else if(I[i].x<=temp.y)
temp.y=I[i].y;
else if(I[i].x>temp.y)
{
res.push_back(temp);
temp=I[i];
}
}
res.push_back(temp);
for(int i=0;i<res.size();i++)
{
cout<<res[i].x<<" "<<res[i].y<<endl;
}
}
实例三
这是一道典型的贪心算法,为了避免多吃的危险,我们在最初先不要消耗一份一盒包装的雪糕,而应该总是选择消耗一份一盒包装的蛋糕最小的方法。即按照下面的方法进行
消耗2盒3包装的蛋糕/3盒2包装的蛋糕 这时消耗1盒包装的最少,为0
消耗1盒3包装的蛋糕,再消耗1盒2包装的蛋糕 此时消耗1盒包装数为1
消耗2盒2包装的蛋糕 此时消耗1盒蛋糕数目为2
消耗1盒3包装的蛋糕 此时消耗1盒蛋糕数目为3
消耗1盒2包装的蛋糕 此时消耗1盒蛋糕数目为4
此时消耗1盒蛋糕数目为6
#include <cstdio>
#include <iostream>
using namespace std;
int t;
bool pan_duan(int n,int a,int b,int c)
{
int day=0;
if(c>=2)
{
day+=c/2;
c%=2;
}
if(b>=3)
{
day+=b/3;
b%=3;
}
while(c>=1&&b>=1&&a>=1)
{
day+=1;
c-=1;
b-=1;
a-=1;
}
while(b>=2&&a>=2)
{
day+=1;
b-=2;
a-=2;
}
while(c>=1&&a>=3)
{
day+=1;
c-=1;
a-=3;
}
while(b>=1&&a>=4)
{
day+=1;
b-=1;
a-=4;
}
day+=a/6;
if(day>=n)
return true;
else
return false;
}
int main()
{
scanf("%d",&t);
int n,a,b,c;
for(int i=0;i<t;i++)
{
scanf("%d%d%d%d",&n,&a,&b,&c);
if(pan_duan(n,a,b,c))
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
小船过河问题
只有一艘船,能乘2人,船的运行速度为2人中较慢一人的速度,过去后还需一个人把船划回来,问把n个人运到对岸,最少需要多久。
(1)当只有2个人时,过河时间是t2(t1<t2)
(2)当只有3个人时,过河时间是t1+t2+t3,即最快的把另一个送过河,再返回来送另一个。
(3)当人数多时,>=4时,可以考虑每次都把最慢和次慢的送走。这个时候又有2种方式。
1)最快的把次快的送过去,自己回来,然后最慢的和次慢的过河,已经在对岸的次快把船划回来。时间为a[0]+2a[1]+a[n-1]
2)最快的把最慢送过去,自己回来,然后把次慢(这个时候已经是最慢了)送过河,自己再回来。时间为2a[0]+a[n-2]+a[n-1]
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int n;
cin>>n;
int a[n];
for(int i=0;i<n;i++)
{
cin>>a[i];
}
sort(a,a+n);
int sum=0;
while(n>3)
{
sum+=min(a[0]+2*a[1]+a[n-1],2*a[0]+a[n-2]+a[n-1]);
n-=2;
}
if(n==3)
sum+=a[0]+a[1]+a[2];
else if(n==2)
sum+=a[1];
else
sum+=a[0];
cout<<sum<<endl;
return 0;
}
动态规划解法:
程序员算法基础——贪心算法 - 简书