• P1120 小木棍 [数据加强版]


    原题链接  https://www.luogu.org/problemnew/show/P1120

    不得不说,这题真的是一道深度搜索毒瘤题qwq,整整坑了我一个上午。

    先说一下大体思路:

    1.读入数据的同时(注意过滤掉长度大于50的木棍),算出所有小木棍的总和sum,因为至少所有的小木棍会拼成一根长度为sum的超大木棍;

    2.找出所有小木棍中长度最大的那根max,原始长度len一定大于等于这个max,所以我们从max开始搜索,若找到一个符合条件的原始长度len就立刻返回,此时这个len一定是最小值;

    考虑一下怎么搜:我们设search(int k,int last,int rest) 表示当前正在拼接第k根木棍,上一根用到的小木棍是last,当前还有rest没拼接完; 

    当然若这个题不加任何剪枝技巧进行深度优先搜索的话,时间效率是指数级别的,效率非常低,程序将严重超时。对于此题我们可以从可行性和最优性上加以剪枝:

    最优性剪枝:

    1.所以木棍的总长度为sum,那么原始长度len一定能够被sum整除,即len | sum ,因为你要拼出来整数根木棍,不可能拼出来小数根木棍;

    2.木棍的原始长度一定大于等于所有小木棍中最长的那根;

    可行性剪枝:

    3.一根长木棍肯定比几根短木棍拼成同样长度的用处小,即短的木棍可以更灵活组合,所以对输入的所有木棍按长度从大到小排序,从长到短地将木棍拼入,这样短木棍可以更加灵活,拼成原始长度len的成功率更高;

    4.在截断后的排好序的木棍中,当用第i根木棍拼接时,可以从i+1后的木棍开始搜。因为根据优化3,你总是先用长度更大的木棍,所以前i个木棍已经用过了;

    5.当dfs返回拼接失败,需要更换当前使用的木棍时,不要再用与当前木棍的长度相同的木棍,因为当前木棍用了不行,改成与它相同长度的木棍一样不行。这里我开了个数组nxt[i]表示与第 i 根长度相同的所有木棍中最后一根,那么nxt[i]的下一根就是和i长度不相同的木棍;这个预处理可以优化时间,不必在循环中慢慢往下找长度不相等的木棍;

    6.我们拼接木棍的时候只找长度小于等于剩余长度rest的木棍(长了接不上啊),所以我们可以二分查找到第一根长度小于等于rest的木棍,那么它后面的木棍都是小于rest的木棍;

    7.当我们搜到了最后一根木棍的时候,我们直接返回,因为剩下的一定能拼成原式长度len;

    证明:  所有木棍的长度总和为sum,当前枚举的原式长度为len,那么能拼成m=sum/len 根木棍;若当前正在拼第m根木棍的话,那么说明前面的(m-1)根木棍已经拼好了,用了len*(m-1)的长度,那么剩下的长度就是:sum-len*(m-1)=sum-(len*m-len)=sum-sum+len=len,说明剩下的所有木棍的总和就是len,正好就是枚举的原式长度len,那么我们就不用搜了,直接返回就好了;

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

    9.我们其实只需枚举到sum/2就行了,如果还拼不成功的话,那么答案只能是sum了;

    10.还有一个挺重要的剪枝,但是容易忽略:

    当我们剩余长度rest等于当前拼接的木棍的长度时,若拼接失败了,那么直接返回改用之前的木棍;

    这一点很难想也很难理解,当时我也特别的懵啊,现在好歹明白了,现在就给你们解释一波吧(解释得不是很到位,可能你们也挺不懂qwq):

    当前长度rest等于正在拼的这个小木棍的长度,所以是不是我们只要把这个小木棍接上去就又拼完了一根木棍?但是我们去拼其他的木棍的时候却拼接失败了,如果我们不返回的话,继续换木棍往下拼,一定是要用几根和为rest的小木棍一块拼才能把那个rest的空补上,那么你原来那个剩下的长度为rest的木棍就搁在那儿了,你想想,前面说过短木棍比长木棍更灵活更好拼,那么你刚刚的操作就相当于,用了好几根短木棍去换下来了一根长木棍,但是那些短木棍都拼不成功,你这一根长木棍不就更拼不成功了吗? 

    有了这么多剪枝,代码跑起来就快多了,下面就是AC代码啦:

    #include<iostream>
    #include<cstdio>
    #include<cmath>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    int read()                            //读入优化 
    {
        char ch=getchar();
        int a=0,x=1;
        while(ch<'0'||ch>'9')
        {
            if(ch=='-') x=-x;
            ch=getchar();
        }
        while(ch>='0'&&ch<='9')
        {
            a=(a<<3)+(a<<1)+(ch-'0');
            ch=getchar();
        }
        return a*x;
    }
    int cmp(int x,int y)                   //将小木棍长度从大到小排序 
    {
        return x>y;
    }
    int n,sum,minn,m,len,flag,a[70],vis[70],nxt[70],x,cnt;  
    //a数组是合法的每根小木棍的长度
    //vis数组是看看每根小木棍是否已经用过
    //nxt[i]数组是看看和第i根小木棍出长度相同的所有木棍中的最后一根 
    void search(int k,int last,int rest)   //正在拼第k根大木棍,上一根用的小木棍是last,还剩rest
    {
        int i,j;
        if(k==m) {flag=1;return ;}         //剪枝7,如果当前正在拼最后一根,那么肯定能拼成,就直接返回吧
        if(rest==0)                        //拼完了第k根,换下一根 
        {
            for(i=1;i<=cnt;i++)            //剪枝3,尽量让更长的小木棍打头  
               if(!vis[i])                 //如果小木棍没用过就试一下  
               {
                   vis[i]=1;                  //用过这根木棍了 
                search(k+1,i,len-a[i]);    //搜索第k+1根,上一根用的小木棍是i,还剩len-a[i]   
                vis[i]=0;                  //剪枝8,回溯操作 
                return ;
               }
        } 
        int l=last+1,r=cnt;
        while(l<r)                         //剪枝6,二分找第一根小于等于rest的木棍 
        {
            int mid=(l+r)>>1;
            if(a[mid]>rest) l=mid+1;
            else r=mid;
        }
        for(i=l;i<=cnt;i++)                //剪枝4,后面的小木棍都比rest小 
        {
            if(!vis[i]&&rest>=a[i])
            {
                vis[i]=1;
                search(k,i,rest-a[i]);
                if(flag) return ;          //如果能拼成就返回  
                vis[i]=0;                  //剪枝8,回溯操作 
                if(rest==a[i]) return ;    //剪枝10,如果rest和当前的小木棍的长度相同,那就说明后面都拼不成了,直接返回 
                i=nxt[i];                  //剪枝5,跳到和当前木棍长度相同的所有木棍中的最后一根,这样就不用再重复的搜索长度相同的木棍了 
            }
        }
        return ;
    } 
    
    int main()
    {
        n=read();
        for(int i=1;i<=n;i++) 
        {
            x=read();    
            if(x>50) continue;               //注意过滤掉长度大于50的小木棍          
            a[++cnt]=x;                    //cnt是实际剩下来的小木棍的数量 
            sum+=x;                        //求出所有长度小于50的小木棍的总和 
        }
        sort(a+1,a+1+cnt,cmp);             //剪枝3,让小木棍从大到小排序 
        nxt[cnt]=cnt;                      //最后一根小木棍的nxt只能是自己,后面没有小木棍了 
        for(int i=cnt-1;i>0;i--)
        {
            if(a[i]==a[i+1]) nxt[i]=nxt[i+1];    //如果和后面的那根的长度相同,那么nxt值相同 
            else nxt[i]=i;                 //如果后面没有长度和它相同的木棍,那么nxt就是自己 
        }
        vis[1]=1;                         
        for(int i=a[1];i<=sum/2;i++)       //剪枝9,枚举可能的每根长度 
        {
            if(sum%i==0)                   //剪枝1 
            {
                m=sum/i;                   //有m根木棍 
                len=i;
                search(1,1,len-a[1]);
                if(flag) 
                {
                    printf("%d",len);return 0;
                }
            }
        }
        if(!flag) cout<<sum;
        return 0;
    } 

    好一道剪枝神搜题啊qwq!累死我了!

  • 相关阅读:
    分布式日志收集系统:Facebook Scribe
    Python学习笔记 02 Python基础
    Python正则表达式指南
    在ASP.NET 2.0中操作数据教程系列
    sql server2005 分页特性
    UML学习
    is,as,sizeof,typeof,GetType
    viso,PowerDesigner,Rational Rose比较
    领导艺术-八项管理定律
    一个博士,一个研究生,和一个MBA对一碗牛肉面的思考(转)
  • 原文地址:https://www.cnblogs.com/xcg123/p/11022558.html
Copyright © 2020-2023  润新知