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=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;
}
View Code

[我还是莫名其妙的A了...]

还有一个用黄金比例分割+一些特判技巧(玄学)A的?

#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;
}
View Code

 

感觉学的有点不踏实,不过至少还是学会了三分法怎么打。感谢一直帮忙的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有趣吖...

posted @ 2016-02-24 20:46  诚叙  阅读(435)  评论(0编辑  收藏  举报