【算法讲解】贪心算法介绍和题目推荐
1.贪心算法介绍
贪心算法(greedy algorithm,又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的
局部
最优解。贪心算法不是对所有问题都能得到整体最优解,关键在于贪心策略
的选择。
这是百度对贪心的解释。想要使用贪心算法,必须具备无后效性的特征。这点可以在之后的做题中体现出来。
局部最优解
是什么呢?顾名思义,就是某种情况下的最优解。贪心策略
,即对问题求解的策略(方法),这两个概念现在看有点晦涩难懂,需要在后续的做题中加深理解。
样例讲解详见【题解】【洛谷P2240】【贪心】——【深基12.例1】部分背包问题。
2.用贪心算法解决问题的步骤
- 建立数学模型来描述问题;
- 把求解的问题分成若干个
子问题
; - 对每一子问题求解,得到子问题的
局部最优解
; - 把子问题的局部最优解合成原来问题的一个解。
3.贪心算法的证明
想要看某个问题能否使用贪心算法求解,只需要证明即可。常见的贪心策略证明方法的正确性有微扰法
(邻项交换法)、反证法
、数学归纳法
等。想要证明贪心算法的不正确性,只需要举出反例即可。具体讲解在下面有,可自行领会。
4.各种贪心算法题型
4.1.最优选择问题
问题
:给出
n
n
n个物品,第
i
i
i个物品重量为
w
i
w_i
wi,选择尽量多的物品,使得总重量不超过
C
C
C。
贪心策略
:将每件物品从小到大排序,依次选择当前最轻的。直到放不下为止。
证明
(反证法):假设每次不放最轻的可以达成最优解
。那么可以通过将此物品替换成更轻的来节省出空间,放下更多的物品。
参考模版:
#include<bits/stdc++.h>
using namespace std;
int main(){
int n, c, w[20010], ans = 0;
cin >> n >> c;
for (int i = 1; i <= n; i++)cin >> w[i];
sort(w + 1, w + n + 1, greater<int>());
for (int i = 1; c > 0; c -= w[i++], ans++);
cout << ans << endl;
return 0;
}
·洛谷
P2676
[USACO07DEC] Bookshelf B——戳这查看题目
·洛谷P1223
排队接水——戳这查看题目
·洛谷P1478
陶陶摘苹果(升级版)——戳这查看题目
·信奥一本通T1322
拦截导弹问题(Noip1999)——戳这查看题目
还有一类变形的最优选择问题。要求某个区间(通常情况下是相邻两个)的数的和不超过
C
C
C。要将一个序列
S
S
S调整成这样至少需要多少步?这里直接给出问题,贪心策略类似于4.5.区间选点问题
。
·洛谷
P3817
小A的糖果(区间最优选择问题)——戳这查看题目
·洛谷P1181
数列分段 Section I——戳这查看题目
·信奥一本通T1320
均分纸牌(Noip2002)——戳这查看题目
4.2.部分背包问题
问题
:给出
n
n
n个物品,第
i
i
i个物品重量为
w
i
w_i
wi,价值为
v
i
v_i
vi,求出在总重量不超过
C
C
C的情况下最大的价值。(每一样物品可以分割)
贪心策略
:将每一件物品按单价
进行排序。依次按单价从大到小选择,如果不能放下整块的了就进行分割。
证明
(反证法):假设没在背包中放入单价高的物品,而放入了单价低的金币,那么可用等重量的高价值金币替换
掉背包里的低价值金币,总价值更高
了。
参考模版
:
#include<bits/stdc++.h>
using namespace std;
struct object{//储存物品的总重量和总价格
int m,v;
}a[110];
bool cmp(object x,object y){//排序规则
return x.v * y.m > y.v * x.m;//单价高者优先,这里运用了一个小技巧,可以避免浮点数精度误差
}
int main(){
int n, t, zm = 0;//用zm存储当前背包装下的总重量
double ans = 0;//用ans存储答案
scanf("%d%d", &n, &t);
for (int i = 1; i <= n; i++)scanf("%d%d", &a[i].m, &a[i].v);
sort(a + 1, a + n + 1, cmp);
//贪心策略:优先将性价比高的物品装进背包
for (int i = 1; i <= n; i++){//判断每一个物品
if (zm +a [i].m > t){//如果装不下整个
ans += (t - zm) * (1.0 * a[i].v / a[i].m);//将能装下的装进去
break;
}
zm += a[i].m;//总重量增加
ans += a[i].v;//总价格增加
}
printf("%.2lf", ans);//保留两位小数,可自行调整
return 0;
}
·洛谷
P2240
【深基12.例1】部分背包问题——戳这查看题目
·洛谷P1208
[USACO1.3] 混合牛奶 Mixing Milk——戳这查看题目
·信奥一本通T1225
金银岛——戳这查看题目
·信奥一本通T1227
Ride to Office——戳这查看题目
4.3.乘船问题
问题
:有
n
n
n个人,第
i
i
i个人重量为
w
i
w_i
wi,每艘船载重为
C
C
C,最多可乘
2
2
2个人,现在想用最少的船将所有人运走,问船的数量。
贪心策略
:将所有人按重量从小到大排序,依次考虑当前最重的人,如果当前最轻的人都不能与他一起乘船,那么就只能自己乘一条船。否则选择当前最轻
的人与他一起乘船。
证明
(邻项交换法):如果当前最重
的人选择当前第二轻
的人与他一起乘船,他们可能无法乘坐一条船。当前第二重的人和当前最轻的人同样有可能无法搭配。无法达成最优解。
参考模版
:
#include<bits/stdc++.h>
using namespace std;
int main(){
//用l记录当前最轻的人的下标,r记录当前最重的人的下标,sum记录答案
int n, w, p[100010], l = 1, r, sum = 0;
cin >> w >> n;
for (int i = 1; i <= n; i++)cin >> p[i];
sort(p + 1, p + n + 1);//排序
r = n;
while (l <= r){
if (p[l] + p[r] <= w)//是否超过上限
l++, r-- , sum++;
else
r--, sum++;//继续尝试
}
cout<< sum <<endl;
return 0;
}
·洛谷
P1094
[NOIP2007 普及组] 纪念品分组——戳这查看题目
4.4.选择不相交区间问题
问题
:给定
n
n
n个开区间
(
a
,
b
)
(a,b)
(a,b),选择尽量多个区间,使得这些区间两两没有公共点。
贪心策略
:将每一个区间按右端点从小到大排序。遍历所有的区间。如果当前区间左端点
与最长区间右端点相冲突,直接放弃
,否则选取。
证明
(数学归纳法):如果冲突了,按两种情况讨论。(区间
1
1
1为已选区间
,区间
2
2
2为待选区间)
1. 包含关系
区间1 ||||||
区间2 |||||||||||
这两个比赛冲突了,要选择区间
1
1
1,因为区间
1
1
1右端点靠左,这样后续比赛被占用的时间会少一点。
2. 相交关系
比赛1 |||||||||||
比赛2 |||||||||||
还是选择区间 1 1 1,理由一样。
按照数学归纳法,每一次这么选择都是当前为止的最优解,到最后就成了全局最优解。
参考模版
:
#include<bits/stdc++.h>
using namespace std;
struct line { // 存储区间信息
int start, end;
}a[1000010];
bool cmp(line x, line y) {
return x.end < y.end; // 右区间越大的越靠前
}
int main() {
int n, finish = 0, ans = 0; // 用finish存储当前覆盖的区间的右端点
cin >> n;
for (int i = 1; i <= n; i++)cin >> a[i].start >> a[i].end;
sort(a + 1, a + n + 1, cmp);
for (int i = 1; i <= n; i++) // 循环n个区间
if (a[i].start >= finish) // 如果端点不重合不冲突
ans++, finish = a[i].end; // 可以取这一个端点
cout << ans;
return 0;
}
·洛谷
P1803
凌乱的yyy / 线段覆盖——戳这查看题目
·信奥一本通T1429
线段——戳这查看题目
·信奥一本通T1323
活动选择——戳这查看题目
·信奥一本通T1422
【例题1】活动安排——戳这查看题目
4.5.区间选点问题
问题
:给定
n
n
n个闭区间
[
a
,
b
]
[a,b]
[a,b],在数轴上选尽量少的点,使得每个区间内都至少有一个或多个点。
贪心策略
:将每一个区间按右端点从小到大排序。如果当前点无法覆盖此区间,就在右边再加一个点。直到能覆盖为止。
证明
(反证法+数学归纳法):假设在左边
加点能够达成最优解。那么有可能会出现这种情况:
如果要在
[
0
,
5
]
[0,5]
[0,5]这个区间保证至少有两个点
,
[
4
,
10
]
[4,10]
[4,10]这个区间也保证至少有两个点
。如果将这两个点放在
[
0
,
1
]
[0,1]
[0,1],那么
[
4
,
10
]
[4,10]
[4,10]这个区间还要再放两个点。一共需要放四个点
。
但是如果在最右边 [ 4 , 5 ] [4,5] [4,5]放两个点,那么 [ 4 , 10 ] [4,10] [4,10]这个区间也有两个点了。一共只需要放四个点,并且没有比这更优的解法了。
如果对于每两个区间都这样干,那么按照数学归纳法
,每一次这么选择都是当前为止的最优解,到最后就成了全局最优解。
参考模版
(“种树”
A
C
AC
AC代码):
#include<bits/stdc++.h>
using namespace std;
struct tree{//定义一个结构体,储存每一个区间的数据
int b, e, t;
}a[5010];
bool cmp(tree x, tree y){//对于每个区间右端点越小越先操作
return x.e < y.e;
}
int main(){
int n, m, tot = 0;//用tot记录答案
bool v[30010] = {};//用v[i]记录第i个区间是否有点
cin >> n >> m;
for (int i = 1; i <= m; i++)cin >> a[i].b >> a[i].e >> a[i].t;
sort(a + 1, a + m + 1, cmp);
for (int i = 1; i <= m; i++){//模拟每一次操作
int cnt = 0;//记录目前此区间内点的数量
for (int j = a[i].b; j <= a[i].e; j++)//遍历区间
if (v[j])cnt++;
if (cnt >= a[i].t)continue;//满足要求就跳出
cnt = a[i].t - cnt;//记录还需要添加多少个点
for (int j = a[i].e; j >= a[i].b; j--)//从右向左添加点
if (!v[j]){//如果可以添加点
v[j] = 1;//标记
cnt--;//还需要添加点的个数
if (cnt == 0)break;//添加完了就跳出
}
}
for (int i = 1; i <= n; i++)//统计种了多少树
if(v[i])tot++;
cout << tot << endl;
return 0;
}
·信奥一本通
T1423
【例题2】种树——戳这查看题目
4.6.区间覆盖问题
问题
:给定
n
n
n个闭区间
[
a
,
b
]
[a,b]
[a,b],选择尽量少的区间覆盖一条指定的线段区间
[
s
,
t
]
[s,t]
[s,t]。
贪心策略
:先假设当前已经覆盖了线段区间
[
s
,
l
]
[s,l]
[s,l],当前覆盖的最后一条线段
区间为
[
b
n
,
e
n
]
[b_n,e_n]
[bn,en]。那么就在起点在
[
b
n
,
e
n
]
[b_n,e_n]
[bn,en]的区间中选择终点最长的区间覆盖。以此往复直到覆盖整条线段
[
s
,
t
]
[s,t]
[s,t]。
证明
(反证法):假设每一次不选择终点最长的区间能达到最优解。那么有可能导致使用的区间变多甚至不可能覆盖
[
s
,
t
]
[s,t]
[s,t]。可以使用终点更长的区间替换。
参考模版
:
#include<bits/stdc++.h>
using namespace std;
int n, L, W, s, maxt, ans, i;//s表示当前覆盖的右端点,t表示当前找到的最远右端点,用ans记录答案
struct line{//记录一条线段的左右端点
double l, r;
}a[15010];
bool cmp(line a, line b){//排序规则:左端点靠前的靠前
return a.l < b.l;
}
int main(){
cin >> n >> L;//覆盖一个线段区间[0,L]
for (i = 1; i <= n; i++)cin >> a[i].l >> a[i].r;
sort(a + 1, a + n + 1, cmp);//排序
//没有超过草坪的长就继续寻找
for (int i = 1; maxt < L; ans++/*找到符合条件的线段将答案加1*/){
//将当前右端点更新为上一次找到的符合条件的最大右端点
for (s = maxt; a[i].l <= s && i <= n; i++)//在当前右端点内寻找,否则会有线段覆盖不到
if (a[i].r > maxt)maxt = a[i].r;
if (maxt == s){//没有符合条件的线段
cout << -1 << endl;//不能覆盖线段
break;
}
}
if(maxt >= L)cout << ans << endl;//能覆盖线段就输出答案
return 0;
}
·信奥一本通
T1424
【例题3】喷水装置——戳这查看题目
4.7.带限期与罚款的单位时间任务调度
问题
:
n
n
n个任务,每个任务需要一个单位时间执行,任务
i
i
i在时间
d
i
d_i
di前必须完成,否则要罚款
w
i
w_i
wi元。确定所有任务的执行顺序,使得罚款最少。
贪心策略
:首先将每个任务按罚款从大到小排序(罚款越高的要越先完成)。安排任务时尽量往后放
(因为前面可能有时限更短
的任务)。如果不能在规定时间完成的,也尽量往后放(不能不做)(原因同上)。
证明
(反证法):假设任务往前放能达成最优解。那么有可能时限更短
的任务被占用了时间。可以将此任务往后调,得到的答案更优。
参考模版
:
#include<bits/stdc++.h>
using namespace std;
struct game{//定义结构体储存每个任务的信息
int val, time;
}a[510];
bool cmp(game a, game b){//扣钱多的任务先完成
return a.val > b.val;
}
int main(){
int m, n, vis[510] = {};//用vis数组储存每一秒有没有在做的任务
cin >> m >> n;
for (int i = 1; i <= n; i++)cin >> a[i].time;
for (int i = 1; i <= n; i++)cin >> a[i].val;
sort(a + 1, a + n + 1, cmp);
for (int i = 1; i <= n; i++){//安排每一个任务
bool flag = 1;//是否要罚款
for (int j = a[i].time; j >= 1; j--){//尽量往后放
if (vis[j] == 0){//这个时间还没有安排任务
flag = 0;//不需要罚款
vis[j] = 1;//这个时间被安排任务了
break;
}
}
if(flag){//需要罚款
for (int j = n; j >= 1; j--){//尽量往后放
if (vis[j] == 0){//这个时间没有被安排任务
vis[j] = 1;
break;
}
}
m -= a[i].val;//减去罚款
}
}
cout << m << endl;
return 0;
}
4.8.其它
·洛谷
P1223
排队接水(排序)——戳这查看题目
·洛谷P1090
[NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G(最优选择问题+哈夫曼树)——戳这查看题目
·洛谷P1106
删数问题——戳这查看题目
·洛谷P4995
跳跳!——戳这查看题目
·洛谷P5019
[NOIP2018 提高组] 铺设道路——戳这查看题目
·洛谷P4447
[AHOI2018初中组] 分组——戳这查看题目
·洛谷P1080
[NOIP2012 提高组] 国王游戏——戳这查看题目
部分摘自【基础算法 —— 贪心算法 】,根据实际情况进行了一定修改。
5.后记
5.1.关于贪心的无后效性
有很多人不知道贪心的无后效性是什么
,这里我给大家举个例子。
这是一个城市的地图:
其中,连接点
a
a
a和点
b
b
b的边上的数字表示
a
−
>
b
a->b
a−>b的距离。求点
1
1
1到点
5
5
5的最短距离。
如果使用贪心
解决这个问题,因为我们没有那么厉害。所以肯定只能想到这种贪心策略:
每一次选择当前最短的边行走,直到到达点 5 5 5为止。
使用这种贪心策略,我们得到的答案将会是12
,路程为1->3->4->5
。但是最优解为9
,路程为1->2->4->5
。
为什么会出现这种情况呢?因为每一次选择的不同的点,都会导致你能走的路都不一样。
比如当你选择点2
时,你只能去到点4、3
或者回到点1
。但是当你选择点3
时,可以去到点2、4、5
或者回到点1
。
而且即使能去到的点相同,路程也有可能不同。比如上述的点2
和点3
。他们都可以去到点4
。但是3->4
的距离为7
,而2->4
的距离仅为1
。
总体来讲,这就是使用贪心来解决问题的局限性
,俗称鼠目寸光。那么我们也知道了,无后效性是指如果选择当前的选项不会对以后的选项造成影响。可以拿这个问题跟上面的贪心题做一个比较。
这是一个比较易懂的问题。对于比较抽象的问题,可以去看看【题解】【洛谷P2240】【贪心】——【深基12.例1】部分背包问题的最后一条——3.思维扩展。
当然,如果你是像迪杰斯特拉
那样的大佬,能够想出dijkstra
算法,那么就当我什么也没说。
5.2.我对贪心的看法
1. 并不是所有的贪心算法都需要排序,只是排序恰好符合贪心选择局部最优解
的特性。
2. 贪心也算是一个入门比较难的算法了。刚开始学可能有点难以适应。只能通过多刷题来找找手感。
喜欢就订阅此专辑吧!
【蓝胖子编程教育简介】
蓝胖子编程教育,是一家面向青少年的编程教育平台。平台为全国青少年提供最专业的编程教育服务,包括提供最新最详细的编程相关资讯、最专业的竞赛指导、最合理的课程规划等。本平台利用趣味性和互动性强的教学方式,旨在激发孩子们对编程的兴趣,培养他们的逻辑思维能力和创造力,让孩子们在轻松愉快的氛围中掌握编程知识,为未来科技人才的培养奠定坚实基础。
欢迎扫码关注蓝胖子编程教育