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

#include<cstdio>
#include<cstring>
#include<algorithm> using namespace std; const int maxn=;
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=,num,res=; for(int i=;i<=cnt;i++){
num=min(a[i].s-days+,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+,a+n+,cmp);
b[++cnt1]=a[];
for(int i=;i<=n;i++){
while(a[i].s==a[i-].s && i<n) i++;
b[++cnt1]=a[i];
}
sort(b+,b+cnt1+,cmp1);
ll tmp=b[].s;
for(int i=;i<=cnt1;i++){
if(b[i].s<=tmp) no_use[i]=true;
else tmp=b[i].s;
}
for(int i=;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=;i<=n;i++)
scanf("%lld%lld",&a[i].p,&a[i].s);
init(); ll l=,r=M/(F+a[].p),ml,mr;
ll ansl,ansr; ans=max(get_ans(l),get_ans(r));
while(l<=r){
ll Len=r-l+;
ml=l+Len/,mr=l+Len*/;
ansl=get_ans(ml),ansr=get_ans(mr);
if(ansl>ansr)
ans=max(ans,ansr),r=mr-;
else
ans=max(ans,ansl),l=ml+;
} printf("%lld",ans);
return ;
}
[我还是莫名其妙的A了...]
还有一个用黄金比例分割+一些特判技巧(玄学)A的?
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm> using namespace std; const int maxn=;
const double gold=(3.0-sqrt())/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==) return ;
ll sum=M-t*F,days=,num,res=; for(int i=;i<=cnt;i++){
num=min(a[i].s-days+,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+,a+n+,cmp);
b[++cnt1]=a[];
for(int i=;i<=n;i++){
while(a[i].s==a[i-].s && i<n) i++;
b[++cnt1]=a[i];
}
sort(b+,b+cnt1+,cmp1);
ll tmp=b[].s;
for(int i=;i<=cnt1;i++){
if(b[i].s<=tmp) no_use[i]=true;
else tmp=b[i].s;
}
for(int i=;i<=cnt1;i++)
if(!no_use[i])
a[++cnt]=b[i];
} int main(){
scanf("%lld%lld%d",&M,&F,&n);
for(int i=;i<=n;i++)
scanf("%lld%lld",&a[i].p,&a[i].s);
init(); ll l=,r=M/(F+a[].p),ml=+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-;
ll Len=r-l+;
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+;
ll Len=r-l+;
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 ;
}
感觉学的有点不踏实,不过至少还是学会了三分法怎么打。感谢一直帮忙的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有趣吖...
BZOJ3874 codevs3361 宅男计划的更多相关文章
- [BZOJ3874][AHOI2014] 宅男计划
Description 外卖店一共有N种食物,分别有1到N编号.第i种食物有固定的价钱Pi和保质期Si.第i种食物会在Si天后过期.JYY是不会吃过期食物的.比如JYY如果今天点了一份保质期为1天的食 ...
- 【JZOJ3673】【luoguP4040】【BZOJ3874】宅男计划
description 外卖店一共有N种食物,分别有1到N编号.第i种食物有固定的价钱Pi和保质期Si.第i种食物会在Si天后过期.JYY是不会吃过期食物的. 比如JYY如果今天点了一份保质期为1天的 ...
- 【BZOJ3874】[AHOI&JSOI2014]宅男计划(贪心,三分)
[BZOJ3874][AHOI&JSOI2014]宅男计划(贪心,三分) 题面 BZOJ 洛谷 题解 大力猜想一最长的天数和购买外卖的总次数是单峰的.感性理解一下就是买\(0\)次是\(0\) ...
- bzoj3874&2832 [Ahoi2014]宅男计划 模拟退火,三分
[Ahoi2014&Jsoi2014]宅男计划 Time Limit: 1 Sec Memory Limit: 256 MBSubmit: 962 Solved: 371[Submit][ ...
- 外卖(food) & 洛谷4040宅男计划 三分套二分&贪心
food评测传送门 [题目描述] 叫外卖是一个技术活,宅男宅女们一直面对着一个很大的矛盾,如何以有限的金钱在宿舍宅得尽量久. 外卖店一共有 N 种食物,每种食物有固定的价钱 Pi 与保质期 Si ...
- [luogu] P4040 [AHOI2014/JSOI2014]宅男计划(贪心)
P4040 [AHOI2014/JSOI2014]宅男计划 题目背景 自从迷上了拼图,JYY就变成了个彻底的宅男.为了解决温饱问题,JYY不得不依靠叫外卖来维持生计. 题目描述 外卖店一共有N种食物, ...
- Bzoj 3874: [Ahoi2014&Jsoi2014]宅男计划 三分+贪心
3874: [Ahoi2014&Jsoi2014]宅男计划 Time Limit: 1 Sec Memory Limit: 256 MBSubmit: 861 Solved: 336[Su ...
- 「AHOI2014/JSOI2014」宅男计划
「AHOI2014/JSOI2014」宅男计划 传送门 我们首先要发现一个性质:存货天数随买食物的次数的变化类似于单峰函数. 具体证明不会啊,好像是二分加三分来证明?但是没有找到明确的严格证明. 感性 ...
- BZOJ3874:[AHOI2014&JSOI2014]宅男计划(爬山法)
Description [故事背景] 自从迷上了拼图,JYY就变成了个彻底的宅男.为了解决温饱问题,JYY 不得不依靠叫外卖来维持生计. [问题描述] 外卖店一共有N种食物,分别有1到N编号.第i种 ...
随机推荐
- 记一次动态调用WebService
这次的使用参考博客园中的ID是 生命不息,折腾不止 http://www.cnblogs.com/leolion/p/4757320.html ,感谢分享 博客园让自己慢慢的成长,少不了这些无私奉献 ...
- [leetcode]_Climbing Stairs
我敢保证这道题是在今早蹲厕所的时候突然冒出的解法.第一次接触DP题,我好伟大啊啊啊~ 题目:一个N阶的梯子,一次能够走1步或者2步,问有多少种走法. 解法:原始DP问题. 思路: 1.if N == ...
- vue中的重要特性
一.vue中的自定义组件 html的代码: <!DOCTYPE html> <html lang="en"> <head> <meta c ...
- kettle的hello world
本篇介绍使用kettle的一个最简单的例子,可以初步了解下转换. 需求是这样的: 存在一个本地csv文件,文件的内容如下 现在需要将csv中的数据保存到本地的文本文件中 1.创建一个转换,并且重命名 ...
- mac里边配置android开发环境,intellij开发工具:
1 在android的官网下载 android sdk的mac版 http://developer.android.com/sdk/index.html 选择mac的版本 下载后打开sdk-mana ...
- [CentOS 7] 安装nginx第一步先搭建nginx服务器环境
简要地介绍一下,如何在CentOS 7中安装nginx服务器 方法/步骤 下载对应当前系统版本的nginx包(package) # wget http://nginx.org/packages/ ...
- POJ--1416
#include <stdio.h> #include <stdlib.h> ]={};//代表有没有切割的数组 ;//输入的要被切割的数字 ]={};//切完输出的数组成的数 ...
- 转:javascript 中select的取值
javascript获取select的值全解 获取显示的汉字 document.getElementById("bigclass").options[window.document ...
- python time 模块详解
Python中time模块详解 发表于2011年5月5日 12:58 a.m. 位于分类我爱Python 在平常的代码中,我们常常需要与时间打交道.在Python中,与时间处理有关的模块就包括: ...
- DrawTool多重笔之前奏 => 通过InkAnalyzer实现图形识别
这里要介绍的是通过InkAnalyzer来实现简单图形的识别,例如圆,椭圆,正方形,三角形等,当然你也可以通过扩展来实现自定义图形的识别,在使用InkAnalyzer前,你需要引用IAWinFX.dl ...