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!累死我了!

 

posted @ 2019-06-14 11:47  暗い之殇  阅读(206)  评论(0编辑  收藏  举报