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’次,那么叫外卖的支出升高,也会导致买到的数量不够。[上面好像是一堆废话...]
也就是说这题需要满足的是:从单峰开始往左走,每次少叫一次外卖,你的支出一定会升高;往右边走,没多叫一次外卖,你的支出一定会增高[但是真的有这个性质吗?我不这么觉得]
下面再挂一张图:是笔者思考证明的过程。
#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; }