好久没写博客啦 最近做了许多贪心的题目,感觉整个人的身心都通透了啊 做着做着,发现其实贪心也是有许多基本模型的,趁热打铁,把孤的心得体会记录下来,希望对大家,也对孤有所帮助。 (以下内容为孤自己整理,有不足之处还请多多指教) 一、区间覆盖问题 常常会遇到这样一种题目:在一条(时间、数等)轴上,有几段区间。通常会有这几种问法: (1)、在轴上放最少的点,使得每一段区间都至少有一(若干)个点。 例:POJ 1328 Radar Installation 题目大意是在X轴上方有几个小岛,现在要在X轴放雷达,已知雷达覆盖半径与小岛位置,求最少放多少雷达使得每个小岛都被覆盖。 题解:由半径和小岛的y值,据勾股定理可求出每个小岛都在X轴上有一段对应区间,在此区间内可以覆盖小岛,于是问题便转化为了:在轴上放最少的点,使得每一段区间都至少有一个点。 对于此类题目,通常把区间按照右端点从小到大排序,再在没有点的区间的最右边放上一个点。 为什么呢?如何保证这样是最优的? 想象一下,在你面前有几段区间,先看第一段区间,其它区间的右端点都在它的右端点的右边,所以,在它的右端点上放上一个点,能够在保证此区间被覆盖的前提下,尽可能多地覆盖后面的区间,对于当前这个区间,无论后面如何,这种选法都是最优的,并且不会给后面的选择造成坏的影响。 上代码:#include<cstdio> #include<cmath> #include<algorithm> using namespace std; int t[2],ans; struct node { double x,y; bool operator < (const node &p)const{return y<p.y;} }a[1005]; int main() { int n,d,tot=0; bool flag; double o; while(scanf("%d%d",&n,&d),n||d) { printf("Case %d: ",++tot); flag=0; for(int i=1;i<=n;i++) { scanf("%d%d",&t[0],&t[1]); if(t[1]>d||flag) {flag=1;continue;} a[i].x=t[0]-sqrt(d*d-t[1]*t[1]); a[i].y=t[0]+t[0]-a[i].x; } if(flag) {printf("-1\n");continue;} sort(a+1,a+1+n); for(int i=1;i<=n;) { ans++; o=a[i].y; while(a[i].x<=o&&i<=n) i++; } printf("%d\n",ans); ans=0; } }
ps:如果你想通了,那么肯定也能想通,按照左端点由大到小排,放在左端点也是可以的。
(2)、各个区间重叠排斥,每个单位时间只能完成不排斥的区间,最少需要多少时间?
例1:POJ 1083 Moving Tables
题目大意是要从i教室搬桌子到j教室,而走廊的宽度只够一张桌子移动,问需要多少分钟才能搬完所有桌子?
题解:首先注意由于走廊是两侧,所以当起点是偶数时,会将起点的前一个点也占用,当终点是奇数时,会将终点的后一个点也占用。
对于这类题,先画图分析:
看以上这5个区间,有的互相排斥,有的却可以同时完成,如红线所示,重叠最多的点是4,重叠了4个区间。
实际上,答案就是重叠最多的点所重叠的次数
证明:(数学归纳法)
假设当最多有n个桌子重叠时,搬n趟可以搬完。
那么当最多有n+1个桌子重叠时,先忽略第n+1层桌子,前n趟可以将有n次及以下重叠的桌子搬完,此时仅剩下第n+1层最多仅有一个桌子重叠,此时再搬一趟,就可以全部搬完。
所以,当n=k成立时,n=k+1也成立。
因为n=1时显然成立,所以对于所有正整数,假设成立,证毕。
上代码:
#include<cstring> int main() { int tmp,n,x,y,a[405]; scanf("%d",&tmp); for(int u=1;u<=tmp;u++) { memset(a,0,sizeof a); scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d%d",&x,&y); if(x>y) x^=y,y^=x,x^=y;//交换 if(x%2==0) a[x-1]++; if(y%2) a[y+1]++; for(int j=x;j<=y;j++) a[j]++;//标记覆盖 } x=0; for(int i=1;i<=400;i++) if(a[i]>x) x=a[i]; printf("%d\n",x*10); } }
例2:POJ 3190 Stall Reservations
与Moving Tables类似,只是需要输出方案。这里使用第二种解法。
题解:将区间按左端点从小到大排序,依次放入,能放则放,不能放则再加一个牛棚。
证明:暂且不管前面如何防置,我们来看已经放好一些区间时的情况:
此时倘若接下来起点为6的区间和一个起点为7的区间,你先放哪个?(注意已经按照左端点排过序,此时6和7是最前面的两个数)
果断选6啊,因为无论如何最后6和7都是要放入牛棚的,如果此时放7,则浪费了更多的空间
而且基于此原理,显然,6应该放入2号牛棚
不知你有没有一种感觉,其实此类贪心,就是在不产生损害的前提下,将产生有利情况的可能性最大化。
上代码:
#include<algorithm> #include<queue> using namespace std; struct node { int l,r,id; bool operator < (const node& p)const{return r>p.r;}//越早结束越优先 }a[50005]; priority_queue<node>q; bool cmp(node a,node b) { return a.l<b.l; } int main() { int n,b[50005],ans=1; scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d%d",&a[i].l,&a[i].r); a[i].id=i; } sort(a+1,a+1+n,cmp); b[a[1].id]=ans; q.push(a[1]); for(int i=2;i<=n;i++) { if(q.top().r<a[i].l)//尽可能往前放 { b[a[i].id]=b[q.top().id]; q.pop(); } else//如果最早结束的都无法放下的话,就得加一个牛棚了 b[a[i].id]=++ans; q.push(a[i]); } printf("%d\n",ans); for(int i=1;i<=n;i++) printf("%d\n",b[i]); }
二、合并果子问题
相信大家都对这种问题很熟悉吧~
例1:POJ 3253 Fence Repair
例2:POJ 1862 Stripies
这种类型的题,一般是给一种计算规则,然后计算最大或最小,通常我们会根据这种计算规则的增减性来决定优先级。通常这种题有两种出法,一种是合并,一种是分割,这两者并无异处,可以一视同仁。
如例一,就是合并果子的分割版本,它的计算规则是加法,会使值增大,而通过简单地举例或数学式子可以发现,越早合并的值经历计算规则的次数会越多。题目要求最小值,只需让最小的值去通过计算规则就好了。
又如例二,给出了另外的计算规则:2*sqrt(m1*m2),此时开根号会使值变小,题目要求最小值,只需让最大的值去通过计算规则就好了。
上代码:
例一:
#include<cstdio> #include<queue> using namespace std; priority_queue<long long,vector<long long>,greater<long long> >a; int main() { int n; long long x,y,s=0; scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%lld",&x),a.push(x); for(int i=1;i<n;i++) { x=a.top(),a.pop(); y=a.top(),a.pop(); s+=x+y;a.push(x+y); } printf("%lld",s); }
例二:
#include<cstdio> #include<queue> using namespace std; priority_queue<long long,vector<long long>,greater<long long> >a;//greater是小根堆,最小值优先,less相反 int main() { int n; long long x,y,s=0; scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%lld",&x),a.push(x); for(int i=1;i<n;i++) { x=a.top(),a.pop(); y=a.top(),a.pop(); s+=x+y;a.push(x+y); } printf("%lld",s); }
三、找决定优先级的因素
这类题,一般乍一看让人很摸不着头脑,无从下手。
例:POJ 3262 Protecting the Flowers
要是时间多的吃得多,时间少的吃得少,该怎么办呢???
先从简单情况分析:
假设有两头牛A,B。
若选A,则损失timeA * eatB
若选B,则损失timeB * eatA
如何比较?同时乘以1/(eatA*eatB)。那么损失的大小关系就是timeA/eatA和timeB/eatB的大小关系!
同理,用数学归纳法,可得优先级就是由时间与吃的比例来确定的。但是这里孤要介绍另一种方法:交换比较法
众所周知,贪心一般都有个先后顺序,不同的顺序会导致不同的结果,那么,我们任选一种选法,交换其中两项的顺序,再与原来比较,再找出现有顺序与原有顺序的区别,就可以得到决定结果的顺序因素!
以这道题举个例子:设牛的时间为Ti,吃草速度为Di,那么:
先牵走牛1,损失:(sumD-D1)*2*T1
再牵走牛2,损失:(sumD-D1-D2)*2*T2
再牵走牛3,损失:(sumD-D1-D2-D3)*2*T3
......以此类推
我们将牵走牛1与牛2的顺序交换,再看:
先牵走牛2,损失:(sumD-D2)*2*T2
再牵走牛1,损失:(sumD-D1-D2)*2*T1
再牵走牛3,损失:(sumD-D1-D2)*2*T3
......此后同上
可以看出,两种顺序的总损失是不一样的。从牛3往后相同,所以我们要比较前两头牛。
第一种选法,损失:sumD*(2*T1+2*T2)-2*D1*T1-2*D1*T2-2*D2*T2
第二种选法,损失:sumD*(2*T1+2*T2)-2*D1*T1-2*D2*T1-2*D2*T2
差别就在红色字处。倘若我们要第一种选法的损失小于第二种选法,则要D1*T2<D2*T1
即D1/T1<D2/T2 !!!
所以,决定优先级的因素被我们找出来了。
这样,交换其中两个元素的顺序进行对比的方法,可以探究隐藏的规律。同样的对比方法,在分治的三分法中也有体现。
呼~,好久没有如此舒畅地写过博客了,爽!