• 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)
    ",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)
    ",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有趣吖...

  • 相关阅读:
    Atitit orm 之道 艾龙著 1. 一、ORM的由来 1 2. ORM的组成:      2 3.  常见的ORM框架:      3 4. 、ORM与数据持久化的关系 3 5. Atitit
    Atitit 移动互联网 产业维度 1. 移动互联网带来的模式变革  1 2. 从视窗到“苹果与机器人”,软件发展模式的颠覆  2 3. 第3章 从X86到ARM,蚂蚁绊倒了大象  2 4. 第5
    Atitit 装备工具分类 attilax总结 艾龙著 工具链体系 武器与软件行业工具也是很近似的的。 1. 分类思维 1 1.1. 总分类:冷、热 1 1.2. 轻、重、大规模杀伤性 1
    Atitit 区块链之道 attilax著 艾龙著 1. 金融=制度+技术+信息 1 2. 第一章可信的协议 1 3. 第二章引导未来:区块链经济七大设计原则 1 4. 第五章 新商业
    Atitit 几大研发体系对比 StageGate体系 PACE与IPD体系 敏捷开发体系 CMMI体系 艾龙 著 1. 3. 1.5:业界领先的研发管理体系简介 2 1 2. 《产品及生命周期
    Atitit 传感器之道 1. 视觉传感器 摄像头 1 1.1. 一、光线传感器: 1 1.2. 二、距离传感器: 1 1.3.    第一种是震动传感器。 4 1.4.   第二种是声响传感
    Atitit 架构之道 之 可读性可维护性架构之道 提升效率架构之道 attilax著 艾龙 著 1.1. Hybrid架构 1 1.2. 分层架构是使用最多的架构模式 Layers模式 也称Tie
    Atitit cko之道首席知识官之道 attilax著 艾龙著 1. 2 2. 第 1 章 知识管理到底是什么,有什么用/1 2 3. 1.1 知识管理全景/1 1.2 波士顿矩阵/3 1.2.
    Atitit 提升效率 降低技术难度与提升技术矛盾的解决方案 1. 问题 2 1.1. 高手喜欢技术挑战怎么办,但会提升技术难度 导致新手不会用怎么办 2 2. 解决方案 2 2.1. 通过开会统
    Atitit 依赖管理之道 1. 概念 依赖管理,是指在什么地方以什么形式引入外部代码。 1 1.1.1. 理解模块化和依赖管理: 1 1.2. 依赖管理,有三个层面。 单一职责原则,协议对象引用,
  • 原文地址:https://www.cnblogs.com/Robert-Yuan/p/5215128.html
Copyright © 2020-2023  润新知