【算法讲解】贪心算法介绍和题目推荐

1.贪心算法介绍

    贪心算法(greedy algorithm,又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键在于贪心策略的选择。

    这是百度对贪心的解释。想要使用贪心算法,必须具备无后效性的特征。这点可以在之后的做题中体现出来。

    局部最优解是什么呢?顾名思义,就是某种情况下的最优解。贪心策略,即对问题求解的策略(方法),这两个概念现在看有点晦涩难懂,需要在后续的做题中加深理解。

    样例讲解详见【题解】【洛谷P2240】【贪心】——【深基12.例1】部分背包问题

2.用贪心算法解决问题的步骤

  1. 建立数学模型来描述问题;
  2. 把求解的问题分成若干个子问题
  3. 对每一子问题求解,得到子问题的局部最优解
  4. 把子问题的局部最优解合成原来问题的一个解。

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金银岛——戳这查看题目
·信奥一本通T1227Ride 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;
}

·洛谷P1230智力大冲浪——戳这查看题目
·信奥一本通T1430家庭作业——戳这查看题目

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. 贪心也算是一个入门比较难的算法了。刚开始学可能有点难以适应。只能通过多刷题来找找手感。

喜欢就订阅此专辑吧!

【蓝胖子编程教育简介】
蓝胖子编程教育,是一家面向青少年的编程教育平台。平台为全国青少年提供最专业的编程教育服务,包括提供最新最详细的编程相关资讯、最专业的竞赛指导、最合理的课程规划等。本平台利用趣味性和互动性强的教学方式,旨在激发孩子们对编程的兴趣,培养他们的逻辑思维能力和创造力,让孩子们在轻松愉快的氛围中掌握编程知识,为未来科技人才的培养奠定坚实基础。

欢迎扫码关注蓝胖子编程教育
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝胖子教编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值