集训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!