BZOJ3874 codevs3361 宅男计划
AC通道1:http://www.lydsy.com/JudgeOnline/problem.php?id=3874
AC通道2:http://codevs.cn/problem/3361/
[题目分析]
[为什么会做到这道题]
首先会点进这道题,貌似是打CF一题遇到了贪心题[可是有神犇表示用三分法可以A],然后我就想起三分法似乎还从没有打过,于是找到了很久之前%的一篇J.K的博客。
[介绍一下三分法]
首先还是介绍一下三分法的作用是什么?...
如果要找一个凸函数的最优值,比如二次函数,也就是最优值在中间,而且两边依次递减的函数时,我们可以使用三分法来逼近答案。[二分法只能找单调函数]
传统意义三分法操作过程:每次在线段上取两个三分点,比较两点函数值的大小,然后舍弃小的一边,接着操作。
正确性怎么证明呢?
首先你脑洞一个单峰,然后在三分之一处描点,然后不管你怎么画,单峰一定不在较小值到最近端点的那一段。
如果两点在单峰的异侧,那么删掉任意1/3都保证单峰仍在区间内。
如果在同侧,那么靠近单峰的点一定>远离单峰的点,删掉小的一边,单峰仍在区间内。
为什么一定要画两个点呢?
因为两个点才好比较啊...一个点显然不够,因为不知道单峰在哪边,三个点好像又多了,于是优美的两个点。
为什么一定要取1/3处呢?
因为这样每次将线段长度*2/3,复杂度是稳定的。复杂度是log的。不过你设计一个别的也是可以的。
例如说在冬令营时宋老师提到一个神奇的黄金比例分割式三分。这个的原理是什么呢?
每次我选择两个点ml,mr,满足这样的性质,将[l,ml]的删去后,mr是新区间的ml;将[mr,r]删去后,ml是新区间的mr。
然后列一个等比关系,解出来ml=l+(3-根号5)/2*(r-l+1),mr=r-(3-根号5)/2*(r-l+1)。这样的话每次选新区间的时候,就只需要再多算一个点了。
好机智啊!它的复杂度也差不多,每次是原长*0.618...但实际运用中,浮点误差不可忽略,而且坐标是整点表示,于是这个算法bug较多,屡次80-90分WA,所以考场上采用上面的方法比较靠谱。
还有同学觉得不靠谱,因为区间大小<3时,三分好像就除不下去了,于是在最后的区间里暴力求一遍也是极好的。
[开始了第一阶段的思考----2.24]
好吧,我们再回来看这个题。
如果我告诉你要叫快递小哥t次,你能不能求出最多宅几天呢?...
仔细思考一下...应该是可以求出来的。首先我们可以排除掉一些不可能点的外卖,比如保质期短还很贵的。如果有保质期比它长或和它一样并且价格便宜的,我就可以舍弃它了。
现在我就得到了一个真正会购买的序列,满足a[i].s<a[i+1].s,a[i].p<a[i+1].p就是前一个比这一个保质期短,并且价格比这个便宜。
可以想象,我每次购买应该尽量平均,为什么呢?比如我一次买10天的,结果第二次只买1天的,不如1次买6天,1次买5天[因为保质期越久越贵嘛]。
那么贪心下来就是,考虑第一个保质期最短但是最便宜的商品,我每次当然都会购买,但是我能买多少呢?当然是要么买到没钱,要么买到能吃到保质期结束那天的个数。
那么往下接着考虑,保质期第二短的,我买完第一短的当然接着买咯,还是买到没钱,或者就是在第一个保质期过之后到第二个保质期之间都吃它。然后一直这样考虑下去...
[注意]上面买的时候都是给t组都买一份,但是如果发现买不到这个保质期过那么多个了...那我选择剩下的钱都买,然后放到t个中的某些次数中去。
贪心的部分也搞定了,但是我现在的问题是不知道要叫小哥多少次啊。
最少0次,最多m/(f+a[1],p)次,复杂度是不能考虑枚举的。
但是我们发现上面说了一大段的三分法,诶,我们好像可以用三分法咯?
三分法使用条件:单峰函数...
宅几天和外卖次数之间为什么会呈现单峰函数呢?是人性的扭曲?还是道德的沦丧?
笔者也觉得有点奇怪= =,J.K.是这么说的:“我不能确保这种方法的正确性,因为迄今为止我还没有看到其他能够复杂度能够承受的办法,最起码这样做的话,数据是可以过的,当然不排除数据不够全面。因为送物品非常自由,没有任何限制,所以我们要找一个合适的自变量进行枚举。可以发现,如果我们外卖的次数过少,那么就会出现一些食品性价比不高的情况;如果次数过多,那么就会浪费外卖运费。故可以从这里入手,因为可以看出这是一个类似于二次函数的函数。我们可以通过三分来查找峰值。”
说的好啊,笔者后来仔细思考了一下,假设叫T'次时最优,那么考虑买不够T'时,一定被迫购买了保质期较长的食品,导致购买数量不如T';如果买了大于T’次,那么叫外卖的支出升高,也会导致买到的数量不够。[上面好像是一堆废话...]
也就是说这题需要满足的是:从单峰开始往左走,每次少叫一次外卖,你的支出一定会升高;往右边走,没多叫一次外卖,你的支出一定会增高[但是真的有这个性质吗?我不这么觉得]
下面再挂一张图:是笔者思考证明的过程。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=210; typedef long long ll; struct Node{ ll s,p; }a[maxn],b[maxn]; int n,cnt,cnt1; ll M,F,ans; bool no_use[maxn]; bool cmp(const Node &A,const Node &B){ if(A.s!=B.s) return A.s<B.s; return A.p<B.p; } bool cmp1(const Node &A,const Node &B){ return A.p<B.p; } ll get_ans(ll t){ ll sum=M-t*F,days=0,num,res=0; for(int i=1;i<=cnt;i++){ num=min(a[i].s-days+1,sum/t/a[i].p);//在本商品处最多能购买的天数,如果超过保质期或者没钱,那么不要 sum-=num*t*a[i].p,days+=num,res+=num*t; if(days<=a[i].s){//如果目前还没有超过保质期,说明没钱,那么剩下的钱全部买这个加入答案 num=sum/a[i].p;res+=num;return res; } } return res; } void init(){ sort(a+1,a+n+1,cmp); b[++cnt1]=a[1]; for(int i=2;i<=n;i++){ while(a[i].s==a[i-1].s && i<n) i++; b[++cnt1]=a[i]; } sort(b+1,b+cnt1+1,cmp1); ll tmp=b[1].s; for(int i=2;i<=cnt1;i++){ if(b[i].s<=tmp) no_use[i]=true; else tmp=b[i].s; } for(int i=1;i<=cnt1;i++) if(!no_use[i]) a[++cnt]=b[i]; } int main(){ freopen("3874.in","r",stdin); freopen("3874.out","w",stdout); scanf("%lld%lld%d",&M,&F,&n); for(int i=1;i<=n;i++) scanf("%lld%lld",&a[i].p,&a[i].s); init(); ll l=1,r=M/(F+a[1].p),ml,mr; ll ansl,ansr; ans=max(get_ans(l),get_ans(r)); while(l<=r){ ll Len=r-l+1; ml=l+Len/3,mr=l+Len*2/3; ansl=get_ans(ml),ansr=get_ans(mr); if(ansl>ansr) ans=max(ans,ansr),r=mr-1; else ans=max(ans,ansl),l=ml+1; } printf("%lld",ans); return 0; }
[我还是莫名其妙的A了...]
还有一个用黄金比例分割+一些特判技巧(玄学)A的?
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<cstdio> #include<cmath> #include<cstring> #include<algorithm> using namespace std; const int maxn=210; const double gold=(3.0-sqrt(5))/2.0; typedef long long ll; struct Node{ ll s,p; }a[maxn],b[maxn]; int n,cnt,cnt1; ll M,F,ans; bool no_use[maxn]; bool cmp(const Node &A,const Node &B){ if(A.s!=B.s) return A.s<B.s; return A.p<B.p; } bool cmp1(const Node &A,const Node &B){ return A.p<B.p; } ll get_ans(ll t){ if(t==0) return 0; ll sum=M-t*F,days=0,num,res=0; for(int i=1;i<=cnt;i++){ num=min(a[i].s-days+1,sum/t/a[i].p);//在本商品处最多能购买的天数,如果超过保质期或者没钱,那么不要 sum-=num*t*a[i].p,days+=num,res+=num*t; if(days<=a[i].s){//如果目前还没有超过保质期,说明没钱,那么剩下的钱全部买这个加入答案 num=sum/a[i].p;res+=num;return res; } } return res; } void init(){ sort(a+1,a+n+1,cmp); b[++cnt1]=a[1]; for(int i=2;i<=n;i++){ while(a[i].s==a[i-1].s && i<n) i++; b[++cnt1]=a[i]; } sort(b+1,b+cnt1+1,cmp1); ll tmp=b[1].s; for(int i=2;i<=cnt1;i++){ if(b[i].s<=tmp) no_use[i]=true; else tmp=b[i].s; } for(int i=1;i<=cnt1;i++) if(!no_use[i]) a[++cnt]=b[i]; } int main(){ scanf("%lld%lld%d",&M,&F,&n); for(int i=1;i<=n;i++) scanf("%lld%lld",&a[i].p,&a[i].s); init(); ll l=1,r=M/(F+a[1].p),ml=1+r*gold,mr=r-r*gold; ll ansl=get_ans(ml),ansr=get_ans(mr); ans=max(get_ans(l),get_ans(r)); //printf("(%lld,%lld,%lld,%lld)\n",l,ml,mr,r); while(l<=r){ if(ansl>ansr){ ans=max(ans,ansr),r=mr-1; ll Len=r-l+1; mr=ml;ansr=ansl; ml=l+Len*gold; if(ml==mr && ml>l) ml--; ansl=get_ans(ml); } else{ ans=max(ans,ansl),l=ml+1; ll Len=r-l+1; ml=mr;ansl=ansr; mr=r-Len*gold; if(mr==ml && mr<r) mr++; ansr=get_ans(mr); } //printf("(%lld,%lld,%lld,%lld)\n",l,ml,mr,r); } ans=max(ans,ansl);ans=max(ans,ansr); printf("%lld",ans); return 0; }
感觉学的有点不踏实,不过至少还是学会了三分法怎么打。感谢一直帮忙的ZZD同学。Orz ZZD神犇。
最后重申一遍:“质疑大法好!多多质疑算法的正确性!多多总结算法的适用性!”
[第二阶段的证明思考-----2.25]
开场首先 "Orz-TB-srO" 跪见智商帝。
TB无意间听说我在讨论三分法,唔,就进行了愉快的思考,大喊:“这不是很容易吗?”,我当时十分愤懑= =,我思考了很久都没有想出来...居然被TB秒了。
好吧,有的时候就是要服大神,人家毕竟思考得不一样,你看,她就看错题目了。
她以为是宅M天需要的最少花费,难怪我和她争论了一会也不知道她到底在干什么...
不过我想了想,感觉和原题差不多啊,如果证明出了这个是单峰函数,原题应该也是单峰函数啊。
[然后TB埋头苦干,终于将她的思维翻译成了人类智慧层面的语言,下面是我的转述]
首先设一个函数ans(),ans(x)表示叫x次外卖,使得其能宅M天的最小花费。
目标:证明ans(x)是一个单峰函数。[峰值为最小值]
现在我们再设一个函数F(),F(i)表示叫i次外卖,除掉外卖费的其它花费的最小值。
即:F(i)=ans(i)-i*cost
这时我们相减一下F(i)和F(i-1)
F(i)-F(i-1)=ans(i)-ans(i-1)+cost
如果我们希望ans()是一个下凸函数,因为下凸函数求导应该得到的斜率先是负数再到正数,而且不希望有多个凹进去的地方?
那么ans()的导数应该是递增的才对。ans(i)-ans(i-1)正好就相当于这个导数。那么证明了F(i)-F(i-1)递增我们就证完了!
我们考虑从F(i-1)到F(i)再到F(i+1)的过程:
好了,我们现在已经得到了需要的结论,F(i)-F(i-1)是递增的。
那么我们可以再倒着推回去一遍。
∵F(i)-F(i-1)递增,ans(i)-ans(i-1)=F(i)-F(i-1)+cost,cost为常数
∴ans(i)-ans(i-1)递增。即ans()的导函数是递增的。
又∵ans()初始导函数为负数[一开始的时候,增加外卖次数当然是能够使最小花费变小],且结束时导函数为正数[增加外卖次数能使最小花费数增大]
∴ans()的导函数是先负后正的一个过程,而且ans()的导函数递增。
∴ans()是一个单峰函数,并存在极小值。
然后证完啦!
让我们再次 "Orz-TB-srO" 跪见智商帝。
鸣谢Cyan提出了这么神奇的方法,鸣谢Cyan有心看蒟蒻刷的题。
感谢笔者没有放弃思考与证明。OI有趣吖...