• Luogu1120 小木棍题解dfs剪枝(转)


    原文地址  https://www.luogu.org/blog/complexity/solution-p1120

    原文:

    这是我第一次写luogu博客,哪里写的不好请大家见谅哦 (^_^)

    此题明显是一道搜索剪枝题。此题难度应该不到提高+,但在考场上写这道题会吃力一些,因为不好调。我详细说一下这题的全思路,不过略长。

    前排提示:第四条的优化7讲的是那个不少人不明白的优化,如果你只是不明白那个优化可以空降。

    一,管理员已经在题目中告诉你输入时去掉长度大于50的木棍。

    二,想好搜索什么。很明显我们要枚举把哪些棍子拼接成原来的长棍,而原始长度(原来的长棍的长度)都相等,因此我们可以在dfs外围枚举拼接后的每根长棍的长度。那枚举什么范围呢?

      其长度至少是最长的一根木棍,此时最长的这根木棍恰好单独组成原来的长棍。如果 原始长度 小于 最长的这根木棍,那么这根最长的木棍就无法自己或与其它木棍组成原来的长棍。

      其长度至多是所有木棍的长度之和,此时所有的木棍拼在一起恰好成为一根原来的长棍。如果 原始长度 大于所有木棍的长度之和,那么即使所有木棍拼在一起也组不够原来的长棍了。

      这么大的循环套dfs会超时么?当然会了。所以我们可以考虑到当 原始长度 不能被 所有木棍的长度之和 整除的话,这些木棍是拼不出整数根的(如果都拼成枚举的原来长棍的长度)。因此在循环时把它们刷掉。

      这里借鉴了dalao的(小)优化,即原始长度枚举到 所有木棍的长度之和/2 即可,因为此时所有木棍有可能拼成2根木棍,原始长度再大的话就只能是所有木棍拼成1根了。

    三,脑补一下怎么搜。设dfs(int k,int last,int rest),k表示正在拼第几根原来的长棍,last表示使用的上一根木棍(输入的短棍)的编号,rest表示当前在拼的长棍还有多少长度未拼。于是循环枚举下一根将要使用的木棍。

    四,上面的做法不超时说明你太强大了。你开始思考对程序做一些优化。(下面的优化请按顺序想)

    1.一根长木棍肯定比几根短木棍拼成同样长度的用处小,即短的木棍可以更灵活组合,所以对输入的所有木棍按长度从大到小排序,从长到短地将木棍拼入,这样短木棍可以更加灵活地接在。

     如果你还不太清楚“灵活”的含义,请形象脑补一下——如果先用短木棍,那么需要很多根连续的短木棍接上一根长木棍才能拼成一根原来的长棍,那么短木棍都用了后,剩下了大量长木棍,拼起来就不如短木棍灵活,更难接出原始长度。而先用长木棍,最后再用短木棍补刀,这样就剩下了相对较短的木棍,能更加灵活地拼接出原始长度。

    2.根据优化1,将输入的木棍从大到小排好序后,当用木棍i拼合原始长棍时,从第i+1根木棍开始往后搜。

    3.当dfs返回拼接失败,需要更换当前使用的木棍时,不要再用与当前木棍的长度相同的木棍,因为当前木棍用了不行,改成与它相同长度的木棍一样不行。这里我预处理出了排序后每根木棍后面的最后一根与这根木棍长度相等的木棍(程序中的next数组),它的下一根木棍就是第一根长度不相等的木棍了。

     这个预处理可以优化时间,不必在循环中慢慢往下找长度不相等的木棍。

    4.只找木棍长度不大于未拼长度rest的所有木棍。我看其他大部分人的做法(包括书上的啊)都是直接在循环中判断,但我认为可以根据木棍长度的单调性来二分找出第一个木棍长度不大于未拼长度rest。它后面的木棍一定都满足这个条件。

    5.用vis数组标记每根木棍是否用过。另外在dfs回溯的时候别忘了去掉这些标记,这样就不用每次dfs之前memset了(memset用多的话速度可TM慢了)!

     优化5的习惯可以沿用到各种竞赛

    6.由于是从小到大枚举 原始长度,因此第一次发现的答案就是最小长度。dfs中只要发现所有的木棍都凑成了若干根原长度的长棍(容易发现 凑出长棍的根数=所有木棍的长度之和/原始长度),立刻一层层退出dfs,不用滞留,退到dfs外后直接输出原始长度并结束程序。

    7.还有一个难想却特别特别重要的优化:如果当前长棍剩余的未拼长度等于当前木棍的长度或原始长度,继续拼下去时却失败了,就直接回溯并改之前拼的木棍。有些人不太明白这个优化,这里简单说一下:

     当前长棍剩余的未拼长度等于当前木棍的长度时,当前木棍明显只能自组一根长棍,但继续拼下去却失败,说明这根木棍不能自组?!这根木棍不自组就没法用上了,所以不用搜更短的木棍了,直接回溯,改之前的木棍;

     当前长棍剩余的未拼长度等于原始长度时,说明这根原来的长棍还一点没拼,现在正在放入一根木棍。很明显,这根木棍还没有跟其它棍子拼接,如果现在拼下去能成功话,它肯定是能用上的,即自组或与其它还没用的木棍拼接。但继续拼下去却失败,说明现在这根木棍不能用上,无法完成拼接,所以直接回溯,改之前的木棍。

    做了这么多优化可以确保飞跑了……搜索题啊,每招优化都要学,学一招说不定竞赛的时候就能跑的快一点。

    #include<bits/stdc++.h>
    using namespace std;
    inline int read(){
        int x=0; bool f=1; char c=getchar();
        for(;!isdigit(c);c=getchar()) if(c=='-') f=0;
        for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+c-'0';
        if(f) return x;
        return 0-x;
    }
    int n,m,a[66],next[66],cnt,sum,len;
    bool used[66],ok; //used数组即优化5的vis数组,记录每根木棍是否用过;ok记录是否已找到答案。 
    bool cmp(int a,int b){return a>b;}
    void dfs(int k,int last,int rest){ //k为正在拼的木棍的编号,last为正在拼的木棍的前一节编号,rest为该木棍还未拼的长度
        int i;
        if(!rest){ //未拼的长度为0,说明这根原始长棍拼完了,准备拼下一个 
            if(k==m){ok=1; return;} //优化6,全部拼完并符合要求,找到答案,直接返回 
    
            for(i=1;i<=cnt;i++) //找一个还没用的最长的木棍打头即可。反正要想全都拼接成功,每根木棍都得用上 
                if(!used[i]) break;
            used[i]=1; 
            dfs(k+1,i,len-a[i]);
            used[i]=0;
            if(ok) return; //优化6,找到答案就退出 
        }
        //优化4,二分找第一个 木棍长度不大于未拼长度rest 的位置 
        int l=last+1, r=cnt, mid;
        while(l<r){
            mid=(l+r)>>1;
            if(a[mid]<=rest) r=mid;
            else l=mid+1;
        }
        for(i=l;i<=cnt;i++){
            if(!used[i]){ //优化5,判断木棍是否用过 
                used[i]=1;
                dfs(k,i,rest-a[i]);
                used[i]=0;
                if(ok) return; //优化6,找到答案就退出 
    
                if(rest==a[i] || rest==len) return; //优化7 
                i=next[i]; //优化3 
                if(i==cnt) return;
            }
        }
        //到了这里,说明这时候拼不成当前这根原始木棍了,传回失败信息并修改之前拼的木棍 
    }
    int main(){
        n=read();
        int d;
        for(int i=1;i<=n;i++){
            d=read();
            if(d>50) continue;
            a[++cnt]=d;
            sum+=d;
        }
        sort(a+1,a+cnt+1,cmp); //优化1,木棍按长度从大到小排序 
        //优化3,预处理next数组 
        next[cnt]=cnt;
        for(int i=cnt-1;i>0;i--){
            if(a[i]==a[i+1]) next[i]=next[i+1];
            else next[i]=i;
        }
        for(len=a[1];len<=sum/2;len++){ //枚举原始长度 
            if(sum%len!=0) continue; //如果不能拼出整数根 就跳过 
            m=sum/len; //优化6中的那个计算 
            ok=0;
            used[1]=1;
            dfs(1,1,len-a[1]);
            used[1]=0;
            if(ok){printf("%d
    ",len); return 0;} //优化6,输出答案,退 
        }
        printf("%d
    ",sum); return 0;
    }

    有问题可以评论

     by Zap dalao

    //注:拼成后的每根长棍中,小棍的生成顺序一定是从大到小单调递减的(根据

     int l=last+1, r=cnt, mid;

    )。所以优化7的第一点正确。

  • 相关阅读:
    如何创建线程详解(二)
    JAVA 多线程和进程概念的引入
    JMeter压力测试
    建模揭秘----构建用户模型
    浅谈“领域驱动设计”
    Restlet 学习笔记
    实则以数据库为中心---其实数据库不存在
    基于可重用构件的软件开发过程模型
    四层架构设计模型驱动
    架构
  • 原文地址:https://www.cnblogs.com/Y15BeTa/p/luogu1120.html
Copyright © 2020-2023  润新知