文章目录
(一)贪心算法基础
1、贪心算法是什么
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,所得的是某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态。一般来说,如果在想到某个似乎可行的策略,并且自己无法举出反例,那么就勇敢地实现它。
2、基本要素
- 贪心选择
贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。
它是与动态规划算法的主要区别。贪心选择采用自顶向下、以迭代的方法做出相继选择,每做一次贪心选择就将所求问题简化为一个规模更小的问题。 - 最优子结构
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。运用贪心策略在每一次转化时都取得了最优解。贪心算法的每一次操作都对结果产生直接影响,而动态规划则不是。贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。动态规划主要运用于二维或三维问题,而贪心一般是一维问题。
3、算法思想
贪心算法的基本思路是从问题的某一个初始解出发一步一步地进行,根据某个优化测度,每一步都要确保能获得局部最优解。每一步只考虑一个数据,它的选取应该满足局部优化的条件。若下一个数据和部分最优解连在一起不再是可行解时,就不把该数据添加到部分解中,直到把所有数据枚举完,或者不能再添加算法停止
。
4、贪心算法过程
- 建立数学模型来描述问题
- 把求解的问题分成若干个子问题
- 对每一子问题求解,得到子问题的局部最优解
- 把子问题的解局部最优解合成原来解问题的一个解。
5、贪心算法适用的问题一般具有的特性
- 随着算法的进行,将积累起其它两个集合:一个包含已经被考虑过并被选出的候选对象,另一个包含已经被考虑过但被丢弃的候选对象。
- 有一个函数来检查一个候选对象的集合是否提供了问题的解答。该函数不考虑此时的解决方法是否最优。
- 还有一个函数检查是否一个候选对象的集合是可行的,也即是否可能往该集合上添加更多的候选对象以获得一个解。和上一个函数一样,此时不考虑解决方法的最优性。
- 选择函数可以指出哪一个剩余的候选对象最有希望构成问题的解。
- 最后,目标函数给出解的值。
- 为了解决问题,需要寻找一个构成解的候选对象集合,它可以优化目标函数,贪婪算法一步一步的进行。起初,算法选出的候选对象的集合为空。接下来的每一步中,根据选择函数,算法从剩余候选对象中选出最有希望构成解的对象。如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;否则就加到集合里。每一次都扩充集合,并检查该集合是否构成解。如果贪婪算法正确工作,那么找到的第一个解通常是最优的。
4、算法存在的问题
- 不能保证求得的最后解是最佳的
- 不能用来求最大值或最小值的问题
- 只能求满足某些约束条件的可行解的范围
6、算法实现
- 从问题的某个初始解出发
- 采用循环语句,当可以向求解目标前进一步时,就根绝局部最优策略,得到一个部分解,缩小问题的范围或规模。
- 将所有部分解综合起来,得到问题的最终解。
(二)简单贪心实例
1、【PAT B1020】月饼
- 题目描述
月饼是中国人在中秋佳节时吃的一种传统食品,不同地区有许多不同风味的月饼。现给定所有种类月饼的库存量、总售价以及市场的最大需求量,试计算可以获得的最大收益是多少。
注意:销售时允许取出一部分库存。样例给出的情形是这样的:假如有三种月饼,存量分别为18/15/10万吨,总售价分别为75、72、45亿元。如果市场的最大需求量只有20万吨,那么最大收益策略应该是卖出全部15万吨第二种月饼以及5万吨第三种月饼,得72+45/2=94.5(亿元)。
- 输入格式
每个输入包含1个测试用例。每个测试用例先给出一个不超过1000的正整数N表示月饼的种类数以及不超过500(以万吨为单位)。数字间以空格分隔。
- 输出格式
对每组测试用例,在一行中输出最大收益,以亿元为单位并精确到小数点后两位。
- 输入样例
3 20
18 15 20
45 72 45
- 输出样例
94.5
- 题意
现有月饼需求量为D,已知n种月饼各自的库存量和总售价,问如何销售这些月饼,使得可以收获的利益最大。求最大收益。
- 思路
步骤一:采用“总是选择单价最高的月饼出售,可以获得最大的利润”的策略。因此,对每种月饼,都根据其库存量和总售价来计算出这种月饼的单价。之后,将所有月饼按单价从高到低排序。
步骤二:从单价高的月饼开始枚举。
①如果该种月饼的库存量不足以填补所有需求量,则将该种月饼全部卖出,此时需求量减少该种月饼的库存量大小,收益值增加该种月饼的总售价大小。
②如果该种月饼的库存量足够供应需求量,则只提供需求量大小的月饼,此时收益值增加当前需求量乘以该种月饼的单价,而需求量减为0.
这样,最后得到的收益值即为所求的最大收益值。 - 策略正确性的证明
假设有两种单价不同的月饼,其单价分别为a和b(a<b)。如果当前需求量为K,那么两种月饼的总收入分别为aK与bK,而aK<bK显然成立,因此需要出售单价更高的月饼。
-
注意事项
①月饼库存量和总售价可以是浮点数(题目中只说是正数,没说是正整数),需要用double型存储。对于,总需求量D虽然题目说是正整数,但是为了后面计算方便,也需要定义为浮点型。
②当月饼库存量高于需求量时,不能先令需求量为0,然后再计算收益,这会导致收益为0.
③当月饼库存量高于需求量时,要记得将循环中断,否则会出错。 -
代码
#include <iostream>
#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 argc, char** argv) {
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){
//第i种月饼全部卖出
D-=cake[i].store;
ans+=cake[i].sell;
}else{
//如果月饼库存量高于需求量
ans+=cake[i].price*D;//只卖出剩余需求量的月饼
break;
}
}
printf("%.2f\n",ans);
return 0;
}
2、【PAT B1023】组个最小数
- 题目描述
给定数字0~9各若干个。可以任意顺序排列这些数字,但必须全部使用。目标是使得最后得到的数尽可能小(注意:0不能做首位)。例如,给定两个0、两个1、三个5和一个8,得到的最小的数就是10015558.
现给定数字,请编写程序输出能够组成的最小的数。
- 输入格式
每个输入包含1个测试用例。每个测试用例在一行中给出十个非负整数,顺序表示所有数字0、数字1…数字9的个数。整数间加一个空格分隔。十个数字的总个数不超过50,且至少拥有一个非0的数字。
- 输出格式
在一行中输出能够组成的最小的数。
- 输入样例
2 2 0 0 0 3 0 0 1 0
- 输出样例
10015558
- 思路
策略是:先从1 ~ 9中选择个数不为0的最小的数输出,然后从0~9输出数字,每个字输出次数为其剩余个数。
以样例为例,最高位为个数不为0的最小的数1,此后1的剩余个数减1(由2变为1)。接着按剩余次数(0剩余两个,1剩余一个,5出现三个,8出现一个)依次输出所有数。 - 策略正确性的证明
首先,由于所有数字都必须参与组合,因此最后结果的位数是确定的。然后,由于最高位不能为0,因此需要从[1,9]中选择最小的数输出(如果存在两个长度相同的数的最高位不同,那么一定是最高位小的数更小)。最后,针对除最高位外的所有位,也是从高位到低位优先选择[0,9]中还存在的最小的数输出。
- 注意点
由于第一位不能是0,因此第一个数字必须从1~ 9中选择最小的存在的数字,且找到这样的数字之后要及时中断循环。
- 参考代码
#include <iostream>
#include<algorithm>
using namespace std;
int main(){
int count[10];//记录数字0~9的个数
for(int i=0;i<10;i++){
scanf("%d",&count[i]);
}
for(int i=1;i<10;i++){
//从1~9中选择count不为0的最小的数字
if(count[i]>0){
printf("%d", i);
count[i]--;
break;//找到一个之后就中断
}
}
for(int i=0;i<10;i++){
//从0~9中输出对应个数的数字
for(int j=0;j<count[i];j++){
printf("%d",i);
}
}
return 0;
}
3、背包问题
- 问题描述
有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品 | 重量 | 价值 |
---|---|---|
A | 35 | 10 |
B | 30 | 40 |
C | 60 | 30 |
D | 50 | 50 |
E | 40 | 35 |
F | 10 | 40 |
G | 25 | 30 |
- 问题分析
①目标函数: ∑pi最大,使得装入背包中的所有物品pi的价值加起来最大。
②约束条件:装入的物品总重量不超过背包容量:∑wi<=M( M=150)
③贪心策略:
(1)选择价值最大的物品
(2)选择重量最小的物品
(3)选择单位重量价值最大的物品
有三个物品A,B,C,其重量分别为{30,10,20},价值分别为{60,30,80},背包的容量为50,分别应用三种贪心策略装入背包的物品和获得的价值如下所示: - 贪心策略1(按价值最大)
物品 | 价值 | 重量 |
---|---|---|
B | 120 | 30 |
A | 60 | 20 |
- 贪心策略2(按重量最小)
物品 | 价值 | 重量 |
---|---|---|
C | 50 | 10 |
A | 60 | 20 |
- 贪心策略3(按单位重量价值最大)
物品 | 单位价值 | 重量 |
---|---|---|
C | 50/10=5 | 10 |
B | 12./30=4 | 30 |
-
算法设计
①计算出每个物品单位重量的价值
②按单位价值从大到小将物品排序
③根据背包当前所剩容量选择物品
④如果背包的容量大于当前物品的重量,那么就将当前物品装进去。否则,就将当前物品舍去,然后跳出循环结束。 -
参考代码
#include<iostream>
#include<algorithm>
using namespace std;
typedef struct{
int w;
int v;
double avg;
}P;
bool cmp(P a,P b){
return a.avg>b.avg;
}
int main(){
P *p;
int n,i,m;//n 物品个数 m背包容量
while(cin>>n>>m){
p=new P[n];
for(i=0;i<n;i++){
cin>>p[i].w>>p[i].v;
p[i].avg=p[i].v/p[i].w*1.0;
}
sort(p,p+n,cmp);
int maxvalue=0;
for(i=0;i<n;i++){
if(p[i].w<=m){
m-=p[i].w;
maxvalue+=p[i].v;
}else{
break;
}
}
cout<<maxvalue<<endl;
}
return 0;
}
(三)区间贪心实例
1、区间不相交问题
- 问题描述
给出N个开区间(x,y),从中选择尽可能多的开区间,使得这些开区间两两没有交集。例如对开区间(1,3)、(2,4)、(3,5)、(6,7)来说,可以选出最多三个区间(1,3)、(3,5)、(6,7),它们互相没有交集。
- 问题分析
首先考虑最简单的情况,如果开区间 I1被开区间 I2包含,如下图a)所示,那么显然选择I1是最好的选择,因为如果选择I1,那么就有更大的空间去容纳其他开区间。
接下来把所有开区间按左端点x从小到大排序,如果去除掉区间包含的情况,那么一定有y1>y2>…>yn成立,如图b)所示。现在考虑应当如何选取区间。通过观察会发现,I1的右边有一段是一定不会和其他区间重叠的,如果把它去掉,那么I1的左边剩余部分就会被I2包含,由图a)可知,应当选择I1.因此对这种情况,总是先选择左端点最大的区间。
- 参考代码
#include <iostream>
#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;
while(scanf("%d",&n),n!=0){
for(int i=0;i<n;i++){
scanf("%d%d",&I[i].x,&I[i].y);
}
sort(I,I+n,cmp);//把区间排序
//ans记录不相交区间个数,lastX记录上一个被选中区间的左端点
int ans=1,lastX=I[0].x;
for(int i=1;i<n;i++){
//如果该区间右端点在lastX左边
if(I[i].y<=lastX){
lastX=I[i].x;//以I[i]作为新选中的区间
ans++;//不相交区间个数加1
}
}
printf("%d\n",ans);
}
}
(四)参考文献
【1】五大常用算法之一:贪心算法
【2】五大基本算法之贪心算法 Greedy
【3】贪心算法—机器之心
【4】贪心算法总结
【5】算法笔记