DAY1 贪心算法学习报告

集训DAY1:贪心算法 学习报告

这天的题还有一道未解决,暂时不会代码实现,由于时间有限(精力是相对无限的),所以留待明天补档。

/(课堂笔记)
贪心算法的核心:局部最优得整体最优
证明:数学归纳 微扰 决策包容性 反证 范围缩放
贪心的思路有时是从最差情况寻求优化方案。/

下面,主要讲几个印象比较深刻的方面:

一.线段覆盖相关问题
例题1:给定n个线段[l,r],最多能选择多少条互不相交的线段?
解法:先按右端点排序,之后从最小右端点开始寻找下一个左端点,再从它的右端点向下寻找,以此类推。

例题2:给定n个线段[l,r],至少要标记多少个横坐标,才能使每一个线段上都至少被标记一次?
解法:先按右端点排序,设定第一个点的位置为第一个右端点,如果下一个线段左端点大于这条线段的右端点,就增加一个点到下一条线段的右端点;否则,把这个点移动到右端点和它自身的位置中较小的一个(这道题也可以排右端点,如果左端点大于右端点就增加一个点)。

这两道题是很基础的线段类贪心问题,题1是要使对后面影响尽可能小,所以排右端点(右端点直接决定了能有多大),找最小的左端点;题2是要使对后面影响尽可能大,所以排左端点(左端点直接决定了能有多小),然后尽可能往已有的最大右端点去放,不能兼顾就增加一个。
简单的做个总结,由于右端点决定上限,左端点决定下限,因此我们总是只研究一个的变化情况,不过两个通常都需要排(有时也可以只排一个,但是千万不要忘记在不满足的时候return 0!!!

补充一道今天做到的一道题雷达分布,这道题的贪心方法完全如例题2,但是这个题还有一个条件,如果有的范围雷达无法扫描到要输出-1,这个输出必须在这一组输入都结束之后才能进行,然而样例的那一组就一个数,导致我贪心对了之后这一个点debug了一个半小时才发现不对劲(要不是邱神提供了一份用函数写的AC的代码,今晚的,啊不,明早的博客又要两点发布了)。下面附这一晚debug次数破纪录的感人画面:
在这里插入图片描述
再下面是代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
struct yjx{
	double l,r;
}a[1002];
bool cmp(yjx x,yjx y){
	if(x.l != y.l) return x.l < y.l;
	return 0;
}
int main(){
	int i,j,d,t = 0,n,sum,x[1002],y[1002];
	double pos;
	while(1){
		++t;
		sum = 1;
		scanf("%d %d",&n,&d);
		if(n == 0 && d == 0) break;
		memset(a,0,sizeof(a));
		for(i = 1;i <= n;i++){
			scanf("%d %d",&x[i],&y[i]);
			if(y[i] > d){
				sum = -1;
				break;
			}
			a[i].l = x[i] - sqrt(d * d - y[i] * y[i]);
			a[i].r = x[i] + sqrt(d * d - y[i] * y[i]);
		}
		if(sum == -1){
			printf("Case %d: %d\n",t,sum);
			continue;
		}
		sort(a + 1,a + n + 1,cmp);
		pos = a[1].r;
		for(i = 2;i <= n;i++){
			if(a[i].l > pos){
				sum++;
				pos = a[i].r;
			}
			else pos = min(pos,a[i].r);
		}
		printf("Case %d: %d\n",t,sum);
	}
	return 0;
}

这种写法其实不够简洁,更好的方法是把这个重复的操作过程写进一个solve函数,这样返回-1就不会受到后续的影响,可以很简洁地完成,且绝对不会犯我的错误,建议读者有兴趣自行改编一下。

二.优先队列的应用
在一些贪心里面有时候需要每一次都操作一下整个数组的最值,且这个数组的值总是不断变化,如果一直sort可能会超时,而优先队列单次操作比sort快得多,不容易超时,很实用。下面简单记录一下优先队列的几个基本功能:

#include<queue>
#include<vector>
#include<functional>
int main(){
	priority_queue<int,int<vector>,greater<int> > Q;//注1
	                                        (aa_aa)
	int temp = Q.top();//取队首(队首并不会因此出队)
	Q.pop()//去掉队首,注意括号是必要的
	int a[101];
	Q.push(a[1])//把a[i]放到队中
}

(注释1:这里“aa_aa"想表达的意思是,下划线的位置有一个空格,不要忽略掉,只是一个批注,无实义;这里的优先队列是递增的,如果想要递减的,需要把greater换为less)
优先队列会自动的在每次加入或删除后维持队列的单调性。

三.贪心的思路如何得来(一大波玄学警告
一般来说,不管是否有具体情境这些忽悠人的玩意儿,如果一道贪心的题涉及到很多计算有关的东西(比如各类数字游戏),那么这个题的贪心方法多半是要用数学推导求解(最好不要尝试瞎猜,因为很多比较难的贪心并不是瞎猜能猜出来的),这类题的优点一般是代码本身比较简单但是推导过程相当麻烦。通常证明除了暴力的直接推导,也可能需要采取微扰(一般适用于局部不影响整体的情况)。在技巧上,如开头所讲,有时候会用到从最复杂的情况开始寻找优化,有时候也会用到多个不同情况的合并和单调排列操作。
如果一道贪心的题充满了各种区间(通常是没有严格限制先后顺序,直接输入的区间),那么这个题要从上述区间的思路考虑,这类题主要要考虑清楚怎么排序,是排左还是排右,研究的是哪一个端点。这类题通常采取替换和顺序查找的方法比较好,不适合应用一些DFS、删除区间的复杂操作,否则会导致代码相当复杂且debug困难(代码实现困难倒是其次 )。区间常常考察“最少”“最多”问题,有的时候要应用反向和局部总体思维,由“最少”思考“最多”,由“最多”思考“最少”(例如我们研究怎么能使覆盖的不重合线段尽可能多,其实就可以研究怎么能使一条线段的影响尽可能小,研究怎么能使应用的点尽可能少,有的时候也可以研究怎么能使一个点标记的线段尽可能多)。
如果实在没能找到一种合理的思路进行贪心,或者无从下手,可以先考虑模拟,然后再从模拟中研究贪心的策略。

四.(2018NOIP)铺路问题的学习探究
(注:此部分没有把所有方法代码实现,完整的代码实现我会在学完所需要的工具之后作补充)
1.半模拟半贪心
很明显,为了使铺的次数尽可能少,要使一次能铺完尽可能多的路。由于为0的不能再铺,这就会把路分为几段,之后再DFS寻找下一个最长的连续路段铺,直到最长的路段长度也只有1,这时候直接加上剩下的这些深度(因为落单的这几个也只能一个一个的铺了)就可以解决。但是由于时间限制,在没有ST表的情况下不能拿满100分。
2.合并贪心
对于一段路,最坏的情况就是所有的深度都要一个一个填,从这种基础上,考虑是否有可以简化的地方。对于每个相邻的位置,深度较小的一个都可以在填平深度较大的一个的过程中被填平,因此填这一个的操作次数就被节约了,相当于把这两段路合成为一段来考虑。res = sum - min(d[i],d[i+1])。
3.题解的贪心策略 (个人理解仅供阅读,请以官方为准)
假设一段路的深度都是一样的,那么只需要把整段路填相同次数就可以了;整个填路的过程,可以看做是邻近的几段路互相填齐,再与其他的路段填齐,直到都填成0(让d0和d(n+1)也参与进来,认为他们的深度都是0)。对于相邻两段路深度的差值,一次填充只会影响首尾两项的差(因为中间的都一起填了相同深度),所以这道题在研究怎么填齐差距的时候可以不受到位置的干扰,而且由于差值不大于原深度的限制,每一个+1都会在-1的左侧(也就是说只要配凑就完事了,不用在乎差值是不是也是相邻的,甚至不用在乎配凑的方法),因此可以用大于0的差值和间接计算总填充次数。

DAY1的练习时间看似充裕实际上还是非常紧张(都怪那个坑人的输出bug),所以题解写的也晚,一些东西没能及时研究出来,等到研究明白了后续会补齐(会在评论区发传送门)。
Thank you for reading!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值