「POJ1011」小木棍 题解

前言:

关于小木棍的另一种读法:小林昆???

这是一道的dfs剪枝题目

部分剪枝源自洛谷大佬Kaori

洛谷:P1120

A 题面

B 解题

约定:

下文的木棍指的是被砍断之后的原始木棍,即输入数据

原始木棍不会说成木棍

B1 明确方法

阅读题目,

翻译一下题目要求:你有一些数,把这些数组合到一起,让每组数据总和(数据总和就是原始木棍长度)相等,求总和的最小值

好的,现在我们已经理解了题目要求,那该如何做呢?

总体思路是:枚举原始木棍长度,计算出原始木棍根数(分组组数),然后 dfs 判断是否可以组合出这么多根这么长的原始木棍

所以dfs函数的功能就是拼接木棍(给数据分组)

既然我们要枚举原始木棍的长度,那么考虑枚举范围:

左边界(l)=最长的一根木棍的长度

此时最长的这根木棍的长度就是原始木棍长度(数据总和)

如果原始木棍长度小于这根木棍的长度,

那么这个最长的木棍定然无法与其他木棍组合得到原始长度

右边界(r)=所有木棍长度总和

此时原始木棍的个数为1

也就是所有的木棍共同组成一根原始木棍

其实我们求到 所有木棍长度总和/2 就可以了

因为此时所有木棍有可能拼成2根木棍,原始长度再大的话就只能是所有木棍拼成1根了

(悄悄地瞅一眼题目数据范围,发现必须剪枝)

B2 main函数

根据上述分析,我们可以写出如下代码

同时注意一个小坑:

int n,cnt,r;
int duan,chang;
int a[61];

int main(){
    int temp;
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&temp);//注意这个小坑:数据不能大于50 
        if(temp>50)continue;//所以我们需要特殊处理 
        a[++cnt]=temp;//同时介绍一个小技巧:
                      //a++:在本条语句执行后让a+1
                      //++a:先执行a+1再执行其他内容 
        r+=temp;
    }

    for(int i=a[1];i<=r/2;i++){
        duan=r/i;//原始木棍个数
        chang=i;//原始木棍长度

        dfs();
    }
    return 0;
}

这里的duan指的是原始木棍的个数(数据分组组数)

而chang则是原始木棍的长度(数据总和)

剪枝1

dfs函数的作用是拼接这些木棍(对数据分组)

一根长木棍肯定比几根短木棍拼成同样长度的用处小,即短的木棍可以更灵活组合,

所以对输入的所有木棍按长度从大到小排序,先拼长棍,这样短木棍可以更加灵活地拼接

bool cmp(int a,int b){ return a>b; } 
. . . 
sort(a+1,a+cnt+1,cmp);

什么叫“灵活”?

如果先用短木棍,那么需要很多根连续的短木棍接上一根长木棍才能拼成一根原来的长棍,

那么短木棍都用了后,剩下了大量长木棍,拼起来就不如短木棍灵活,更难接出原始长度。

而先用长木棍,最后再用短木棍补刀,这样就剩下了相对较短的木棍,能更加灵活地拼接出原始长度。

——kaori

总结一下就是:通过定大找小,尽可能快的逼近正确答案。减少判断时间开销。

——wyb

剪枝2

感觉枚举范围是不是有点大?

那么我们其实可以考虑一下如何把一些情况continue掉

显然,如果原始长度 不能被 所有木棍的长度之和 整除 的话

那么,这些木棍是拼不出整数根原始木棍的(原始木棍长度相等)

所以,枚举循环可以这样写:

for(int i=a[1];i<=r/2;i++){
    if(r%i!=0)continue;//剪枝2 
    duan=r/i;
    chang=i;

    dfs();
}

B3 dfs(int tot,int last,int rest)

现在来解决dfs的问题

dfs函数的功能是判断是否可以拼出 duan 根 chang 长的原始木棍

tot表示正在拼第tot根原始木棍,

rest表示当前在拼的原始木棍还有多少长度未拼

last表示使用的上一根木棍

它大概是这样写的:

void dfs(int tot,int last,int rest){
    if(rest==0){     //当前原始木棍拼完
        if(tot==duan){//有足够的原始木棍
            //输出,结束程序
        }
        找到下一没有使用的最长的木棍
        dfs(tot+1,当前位置,chang-a[下一个])
    }
    找第一个木棍长度小于等于rest 的位置
    for(int i=第一个位置;i<=cnt;i++){
        dfs(tot,i,rest-a[i]);
    }
}

nice!

其实你照这个伪代码做就可以过样例了(/xyx)

显然我们还需要剪枝

剪枝3

因为如果满足条件我们就直接退出程序了(exit(0)了解一下?)

所以如果dfs返回拼接失败,需要更换当前使用木棒时

可以跳过与当前木棍的长度相同的木棍

因为当前木棍用了不行,改成与它相同长度的木棍一样不行

所以我用了一个near[]数组预处理排序后每根木棍后面的最后一根与这根木棍长度相等的木棍

它的下一根木棍就是第一根长度不相等的木棍了

也就是:

void dfs(...){
      //other code...
      for(int i=第一个位置;i<=cnt;i++){
        dfs(tot,i,rest-a[i]);

        i=near[i];//剪枝3
    }
}

int main(){
   //other code...
   sort(a+1,a+cnt+1,cmp);//剪枝1 
    near[cnt]=cnt;
    for(int i=cnt-1;i>0;i--){//剪枝 3
        if(a[i]==a[i+1]) near[i]=near[i+1];
        else near[i]=i;
    }
    //other code...
}

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

剪枝4

只找木棍长度不大于未拼长度rest的所有木棍

也就是在这里:

因为木棍长度是从大到小排列的

所以我们可以二分查找

//剪枝4 二分找第一个 木棍长度不大于未拼长度rest的位置 
    int l=last+1,r=cnt,mid;
    while(l<r){
        mid=(l+r)/2;
        if(a[mid]<=rest)r=mid;
        else l=mid+1;
    }

 

剪枝5

emmm,突然才想起我忘了一个常用的vis[]去重操作

这个就不必多说了吧

if(vis[i])continue;//剪枝5 判断木棍是否用过 
vis[i]=1;
dfs();
vis[i]=0;

剪枝6

由于是从小到大枚举原始长度,因此第一次发现的答案就是最小长度

所以一旦发现满足条件直接输出然后直接结束程序

这里介绍一个小技巧:exit(0)

它的作用是直接结束整个程序

if(rest==0){      //拼完了当前原始木棍 
        if(tot==duan){//满足条件 直接输出 
            cout<<chang;//因为我们是从小枚举到大 题目是求最小值 
            exit(0);//剪枝6 exit(0)的作用是直接结束整个程序 
        }
        int k;
        for(k=1;k<=cnt;k++){//找一个还没用的最长的木棍
            if(!vis[k])break;
        }    
        vis[k]=1;
        dfs(tot+1,k,chang-a[k]);
        vis[k]=0;
    }

 

剪枝7

这个剪枝就有点小复杂,却又特别重要

如果当前长棍剩余的未拼长度等于当前木棍的长度原始长度,继续拼下去时却失败了,就直接回溯并改之前拼的木棍

——kaori

解释如下:

1.当前长棍剩余的未拼长度等于当前木棍的长度时,这根木棍在最优情况下显然是拼到这

如果在最优情况下继续拼下去失败了,那肯定是之前的木棍用错了,回溯改即可

2.当前长棍剩余的未拼长度等于原始长度时,说明这根原来的长棍还一点没拼,现在正在放入一根木棍

很明显,这根木棍还没有跟其它棍子拼接,如果现在拼下去能成功话,它肯定是能用上的,即自组或与其它还没用的木棍拼接

但继续拼下去却失败,说明现在这根木棍不能用上,无法完成拼接,所以回溯改之前的木棍。

for(int i=l;i<=cnt;i++){
        if(vis[i])continue;//剪枝5 判断木棍是否用过 
        vis[i]=1;
        dfs(tot,i,rest-a[i]);
        vis[i]=0;
        if(rest==a[i]||rest==chang)return;//剪枝7
        i=near[i];            //剪枝3 
    }

C 代码

#include<bits/stdc++.h>
using namespace std;

int n,cnt,r;
int duan,chang;
int a[61];
int vis[61];//剪枝5 
int near[61];//剪枝7 

bool cmp(int a,int b){
    return a>b;
} 

void dfs(int tot,int last,int rest){//tot为正在拼的木棍的编号
                                    //last为正在拼的木棍的前一节编号
                                    //rest为该木棍还未拼的长度
    if(rest==0){      //拼完了当前原始木棍 
        if(tot==duan){//满足条件 直接输出 
            cout<<chang;//因为我们是从小枚举到大,题目是求最小值 
            exit(0);//剪枝6 exit(0)的作用是直接结束整个程序 
        }
        int k;
        for(k=1;k<=cnt;k++){//找一个还没用的最长的木棍
            if(!vis[k])break;
        }    
        vis[k]=1;
        dfs(tot+1,k,chang-a[k]);
        vis[k]=0;
    }
    //剪枝4 二分找第一个 木棍长度不大于未拼长度rest的位置 
    int l=last+1,r=cnt,mid;
    while(l<r){
        mid=(l+r)/2;
        if(a[mid]<=rest)r=mid;
        else l=mid+1;
    }
    for(int i=l;i<=cnt;i++){
        if(vis[i])continue;//剪枝5 判断木棍是否用过 
        vis[i]=1;
        dfs(tot,i,rest-a[i]);
        vis[i]=0;
        if(rest==a[i]||rest==chang)return;//剪枝7
        i=near[i];            //剪枝3 
    }
}

int main(){
    int temp;
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&temp);//注意这个小坑:数据不能大于50 
        if(temp>50)continue;//所以我们需要特殊处理 
        a[++cnt]=temp;//同时介绍一个小技巧:
                      //a++:在本条语句执行后让a+1
                      //++a:先执行a+1再执行其他内容 
        r+=temp;
    }
    sort(a+1,a+cnt+1,cmp);//剪枝1 
    near[cnt]=cnt;
    for(int i=cnt-1;i>0;i--){//剪枝3
        if(a[i]==a[i+1]) near[i]=near[i+1];
        else near[i]=i;
    }
    for(int i=a[1];i<=r/2;i++){
        if(r%i!=0)continue;//剪枝2 
        duan=r/i;
        chang=i;
        vis[1]=1;
        dfs(1,1,chang-a[1]);
    }
    return 0;
}
View Code

nice 这篇题解终于写完了

D 总结

做这种步骤复杂的剪枝题目

你尤其需要草稿本

每一步都可能可以剪枝

同时每一步的剪枝也需要判断是否必要

不然就会出现玄学现象

比如本题解中的剪枝4 二分查找便是不必要剪枝

(因为不写二分也能过)

THE END.

posted @ 2020-11-22 21:48  _Famiglistimo  阅读(1115)  评论(0编辑  收藏  举报