外卖(food) & 洛谷4040宅男计划 三分套二分&贪心
【题目描述】
叫外卖是一个技术活,宅男宅女们一直面对着一个很大的矛盾,如何以有限的金钱在宿舍宅得尽量久。
外卖店一共有 N 种食物,每种食物有固定的价钱 Pi 与保质期 Si ,保质期的意义是食物会在被买的第 Si 天后过期。譬如你在今天叫了一个 Si =1 的食物,则你必须在今天或者明天吃掉它。
现在你有 M 元钱,每次叫外卖需要 F 元的运费,送外卖的小哥身强体壮,叫一次可以帮你带来任意份任意种食物,请问在保证每天都吃到至少一份未过期食物的前提下,你最多能宅多久?
【输入文件】
第一行 T 表示数据组数
对于每组数据,第一行三个整数 M F N
接下来 N 行每行两个整数 P i S i
【输出文件】
对于每组数据,在一行中输出一个数表示最多宅的天数
【样例输入】
food.in
332 5 2
5 0
10 2
10 10 1
10 10
10 1 1
1 5
【样例输出】
food.out
3
0
8
【数据约定】
30%:0 <= Si <= 10,1 <= M <= 20,1 <= N <= 10
100%:0 ≤ S i ≤ 2000000,1 ≤ M ≤ 2000000
100%:T ≤ 10,1 ≤ F ≤ M,1 ≤ N ≤ 200,1 ≤ P i ≤ M
思路:
觉得这题好难呀 也不知道怎么想到正解 就直接讲讲正解吧
solution说 :“这是道构造贪心的好题”
发现这题无从下手,但是如果知道点外卖点了多少次就好做了(我并没有觉得很好做???)
所以我们枚举点外卖的次数 这样我们就知道总的运费了 也就知道了买食物的总钱数
然后我们需要考虑的是两次点外卖之间的间隔 这里是一个贪心
首先我们初始化的时候就要去掉那些又贵保质期又短的食物 保证食物的保质期与价格正相关
先说结论吧 当点外卖的两两之间的间隔时间相等时 可以维持天数是最多的
假如我们要维持6天 点2次外卖 那么在第一天和第四天点外卖花的钱是最少的
因为这样子的话点的外卖中保质期最长的只需要2天就可以了 而价钱与保质期正相关
保质期短 价钱就少
我们二分每轮点外卖维持的天数
可以预处理出点一次外卖维持一定天数的最小价钱
所以这里就比较好判断了
但是要注意的是 可能点一定次数的外卖并且维持一定的天数后 还有钱剩余
这时就可以在最后一次点外卖的时候多点一些 维持更多的天数
CODE:
View Code1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #define go(i,a,b) for(register int i=a;i<=b;i++) 5 #define yes(i,a,b) for(register int i=a;i>=b;i--) 6 #define ll long long 7 #define M 200+10 8 #define N 1000000+10 9 #define inf 21000000 10 using namespace std; 11 ll read() 12 { 13 int x=0,y=1;char c=getchar(); 14 while(c<'0'||c>'9') {if(c=='-') y=-1;c=getchar();} 15 while(c>='0'&&c<='9') {x=(x<<1)+(x<<3)+c-'0';c=getchar();} 16 return x*y; 17 } 18 ll T,n,m,f,maxn,ans,p[M],s[M],minn[N],sm[N]; 19 void work(ll st,ll my) //step money 20 { 21 ll l=0,r=maxn; 22 while(l<r) 23 { 24 ll mid=(l+r+1)>>1; 25 if(sm[mid]*st<=my) l=mid; 26 else r=mid-1; 27 } 28 ans=max(ans,l*st+(my-sm[l]*st)/minn[l+1]); 29 } 30 int main() 31 { 32 T=read(); 33 while(T--) 34 { 35 m=read();f=read();n=read(); 36 memset(minn,77,sizeof(minn)); 37 maxn=0;ans=0; 38 go(i,1,n) 39 { 40 p[i]=read();s[i]=read()+1; 41 minn[s[i]]=min(minn[s[i]],p[i]); 42 maxn=max(maxn,s[i]); 43 } 44 yes(i,maxn-1,1) minn[i]=min(minn[i],minn[i+1]); 45 go(i,1,maxn) sm[i]=sm[i-1]+minn[i]; 46 if(!f) {printf("%lld\n",m/minn[1]);continue ;} 47 go(i,1,m/f) work(i,m-i*f); 48 printf("%lld\n",ans); 49 } 50 }
和上面那题唯一不同的地方就是 数据范围
这里的数据范围是:0<=Si<=10^18,1<=F,Pi,M<=10^18,1<=N<=200
于是用上面的代码就会超时啦
然后发现点外卖的次数是可以三分的(当然不是我发现的)
以下摘自 gql's blog :
可以证明,外卖的轮次与存活天数是个二次函数关系
来下面我再来证明下Qw
首先感性理解下,假如我现在有很多钱,我只让快递小哥来一次,我最多只能活保质期max天嘛
然后如果小哥来两次,我依然付得起很多很多东西,我就能活2*保质期max对趴
以此类推我们可以发现最初我的钱很多的时候我能活的天数是增加的
但是!再举个栗子
假如我一天点一次外卖,我就会要花很多很多外卖费,活的天数可能更少
通过这个极端的栗子可以发现,如果我点外卖点的太频繁,我花的外卖费就很多,能用来买东西的钱就很少辣
所以它是个单峰函数!
有点感性,,,?
那那那讲下另一个方法,lzq还港了一个方法,感觉似懂非懂的我大概说下QAQ
就,我把花费分成两部分,一部分是快递费一部分是买东西
快递费就分摊到了每一天嘛
然后我们现在相当于是说要每一天的平均花费min
那如果我这一轮买的东西多一个
我的快递费平摊的对象就+1,同时的我买东西的花费也会增加
就是从f/i+P/i变成了f/(i+1)+(P+p[i])/(i+1)
利用反比例函数的性质可以发现开始的时候f/i这个变化值是会很大的,而相对而言的p增加的少一些,所以减少比增加多,它就会是单调增的
但是到了后期f/i的变化量就很小了嘛,还有一点就是到了后期的pi会很大(因为我们已经是让p单调增的了
所以增加的就比减少的多了,所以就成了单调减的
综上,它是个单调函数
但是并不仅仅只要三分那么简单 由于这题的 si 实在是太大了
开不了那么大的minn[]和sm[] 所以我们就不能向之前那样子预处理了
每次都要自己计算qwq
优秀题解 CODE:
View Code1 #include<cstdio> 2 #include<algorithm> 3 using namespace std; 4 typedef long long ll; 5 int n;int cnt;ll m;ll f; 6 struct food 7 { 8 ll cst;ll kep; 9 friend bool operator <(food a,food b){return a.kep<b.kep;} 10 }tp[210],fo[210]; 11 inline ll cost(ll t)//计算存活到t的花费 12 { 13 ll res=0; 14 for(int i=1;i<=cnt;i++) 15 { 16 if(res<0){return -1;} 17 if(t>fo[i].kep){res+=fo[i].cst*fo[i].kep;t-=fo[i].kep;} 18 else {res+=fo[i].cst*t;return res;} 19 }return -1;//这里是避免爆INF 20 } 21 inline ll calcd(ll pm)//计算花多少钱可以存活多少天 22 { 23 ll l=0;ll r=pm; 24 while(l<r)//单调可以二分 25 { 26 ll mid=(l+r+1)/2;ll y=cost(mid); 27 if(y==-1||y>pm){r=mid-1;}else {l=mid;} 28 } 29 return r; 30 } 31 inline ll calct(ll r)//计算T 32 { 33 ll rest=m-r*f;if(rest<0||rest>m){return 0;}//还是避免爆longlong 34 ll pm=rest/r;ll k=calcd(pm);if(k==0){return 0;} 35 ll c2=cost(k+1);ll c1=cost(k);if(c2==-1){return r*k;} 36 else {rest-=c1*r;return rest/(c2-c1)+r*k;} 37 } 38 int main() 39 { 40 scanf("%lld%lld%d",&m,&f,&n); 41 for(int i=1;i<=n;i++){scanf("%lld%lld",&tp[i].cst,&tp[i].kep);} 42 sort(tp+1,tp+n+1);tp[0].kep=-1;fo[0].kep=-1; 43 for(int i=1;i<=n;i++)//单调栈去掉无用的东西 44 {while(cnt!=0&&tp[i].cst<fo[cnt].cst){cnt--;}fo[++cnt]=tp[i];} 45 for(int i=cnt;i>=1;i--){fo[i].kep-=fo[i-1].kep;}//后向差分方便计算 46 ll l=1;ll r=m/f+1;//三分法求函数最值 47 while(r-l>5)//缩小区间 48 { 49 ll x1=l+(r-l)/3;ll x2=l+((r-l)*2)/3; 50 ll y1=calct(x1);ll y2=calct(x2); 51 if(y1<y2){l=x1;}else {r=x2;} 52 } 53 ll res=calct(l);//然后暴力枚举最大值 54 for(ll i=l+1;i<=r;i++){res=max(res,calct(i));} 55 printf("%lld",res);return 0;//拜拜程序~ 56 }