• AcWing 167. 木棒


    \(AcWing\) \(167\). 木棒

    一、题目描述

    乔治拿来 一组等长 的木棒,将它们随机地砍断,使得每一节木棍的长度都 不超过 \(50\) 个长度单位。

    然后他又想把这些木棍 恢复到为裁截前的状态 ,但忘记了 初始时有多少木棒 以及 木棒的初始长度

    请你设计一个程序,帮助乔治计算木棒的 可能最小长度

    每一节木棍的长度都用大于零的整数表示。

    输入格式
    输入包含多组数据,每组数据包括两行:

    第一行是一个不超过 \(64\) 的整数,表示砍断之后共有多少节木棍。

    第二行是截断以后,所得到的各节木棍的长度。

    在最后一组数据之后,是一个零。

    输出格式
    为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。

    备注

    \(1≤N≤60\)

    二、题目分析

    这道题可以说是搜索剪枝例题中的经典,涉及到的剪枝操作令人汗颜,如果能够完完全全吃透这道题,对剪枝的体悟一定能更深!

    东西 尺寸
    木棒
    木棍

    先来一下 朴素版本 的搜索代码:

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 65;
    int a[N], n, len, sum;
    bool st[N];
    
    // len:每个木棒的长度
    // u:当前正在装的木棒
    // last:当前木棒已经装入的木棍长度
    bool dfs(int u, int last) {
        // 假设有100个同学,10个房间,每个房间10个人,如果前9个房间完全装满,则最后一个房间10个人
        // sum = 100,len = 10
        // u = 10,表示前9个已装满,已经装入了90人,剩余10人,则最后一个房间一定是装满的
        if (u * len == sum) return true; //如果u成功来到最后一个房间,则表明可以完整的填充最后一个房间
    
        //如果本木棒已填满,就开启下一个木棒
        if (last == len) return dfs(u + 1, 0);
    
        for (int i = 0; i < n; i++) {
            if (st[i] || last + a[i] > len) continue; //如果使过了,或者,当前木棒剩余空间不足以装入i木棍,放过
    
            st[i] = true;                         //装入
            if (dfs(u, last + a[i])) return true; //装入后,此路径的后续汇报了成功标识,则本路径就是成功的,不需要再继续搜索了
            st[i] = false;                        //回溯
        }
        return false;
    }
    int main() {
        while (cin >> n && n) {
            sum = 0;
            memset(st, false, sizeof st);
            for (int i = 0; i < n; i++) cin >> a[i], sum += a[i];
            //从1~sum,由小到大逐个枚举木棒长度,这样,可以保证第一个符合条件的木棒长度是最小的
            //在 木棒长度=len的前提下,开始向第1个木棒里装东西,目前第1个木棒内已装入的长度为0
            //如果当前木棒长度=len的情况下,可以找到完美的装填办法,就是找到的答案
            for (len = 1; len <= sum; len++) {
                if (dfs(1, 0)) {
                    printf("%d\n", len);
                    break;
                }
            }
        }
        return 0;
    }
    

    毫无疑问,\(TLE\)了,需要优化,本题的优化挺\(BT\)的,很极限,需要仔细阅读理解:

    1、木棒长度是所有木棍总长度的约数

    因为原来每根木棒长度是相同的,所以

    \[\large sum(总长度)=len(单个木棒长度)*num(木棒数量) \]

    也就是一定要有$$\large sum% len=0$$

    2、要组合不要排列

    这个问题中的木棒长度跟搭配 顺序 没有关系 ,比如编号为\([1,2,3]\)结合的木棍,与编号为\([3,2,1]\)结合的木棍,视为同一个组合,我们只要控制好枚举的顺序,就可以得到唯一的组合搭配顺序,而不需要枚举出所有的排列形式。这一点也很好实现:每个木棒开始时,从下标\(0\)开始找木棍进行填充,并且记录这个上一个是从哪个下标开始的,新选择的木棍号码,一定要比上一个大就行,不可以选择比上一个小的,防止了非单调递增序的产生,下面的我们首先需要理解的组合与排列的对比代码:

    排列

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 20;
    int n, m;
    bool st[N];
    vector<int> path;
    
    void dfs(int u) {
        if (u == m) {
            for (int i = 0; i < path.size(); i++)
                cout << path[i] << " ";
            cout << endl;
            return;
        }
        for (int i = 1; i <= n; i++) {
            if (!st[i]) {
                path.push_back(i);
                st[i] = true;
                dfs(u + 1);
                st[i] = false;
                path.pop_back();
            }
        }
    }
    // 测试用例:
    //  5 3
    int main() {
        cin >> n >> m;
        dfs(0);
        return 0;
    }
    

    组合

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 20;
    int n, m;
    bool st[N];
    vector<int> path;
    
    void dfs(int u, int start) {
        if (u == m) {
            for (int i = 0; i < path.size(); i++) cout << path[i] << " ";
            cout << endl;
            return;
        }
        for (int i = start; i <= n; i++) {
            if (!st[i]) {
                path.push_back(i);
                st[i] = true;
                dfs(u + 1, i + 1);
                st[i] = false;
                path.pop_back();
            }
        }
    }
    // 测试用例:
    //  5 3
    int main() {
        cin >> n >> m;
        dfs(0, 1);
        return 0;
    }
    

    3、优化搜索顺序

    有了运输小猫那道题的经验,我们知道:将木棍按 由大到小 排序,然后去深搜,可以让搜索更快。

    原理:先枚举大的,后面的搜索空间就少了,可以减少分支。

    4、当填充某个木棍失败时(回溯后)

    • \(①\) 在一个木棒结尾正好装下,但后续动作无法完成完美填充
    • \(②\) 在一个空木棒头部放木棍,但后续动作无法完成完美填充

    推论
    假如一根小木棍放在当前大木棒中,正好在尾部填充满,无法完成后续的完美填充,那么可以判断它放在任何一根大木棒中都无法实现完美填充。

    证明
    反证法:假如有一根小木棍放在当前大木棒\(x\)的最后一根不行,但是放在其他大木\(y\)的某个位置行,则大木\(y\)中该小木棍可以通过平移使得该小木棍置于 最后一个位置,这样就存在方案使得该小木放在大木棒最后一个位置可行,与假设相悖,得证。

    同理,\(②\)也是正确的结论。

    三、代码实现

    \(23ms\)

    #include <cstdio>
    #include <cstring>
    #include <cmath>
    #include <algorithm>
    using namespace std;
    const int N = 70;
    int a[N];   //用来装每个木棍的长度
    int n;      //木棍个数
    int len;    //组长度
    int sum;    //总长度
    bool st[N]; //标识某个木棍是不是使用过了
    
    /*
    start:从哪个索引号开始继续查找
    u:木棒号,下标从0开始
    res:最后一个木棒目前的长度
    
    只要搜索到解就停止的dfs问题,一般用bool类型作为返回值,因为这样,搜到最优解就返回true,用dfs的返回值作为条件,
    就可以瞬间退出递归了。比上一题中专门设置标志变量的方法要更好。
    */
    bool dfs(int u, int last, int start) {
        //因为填充的逻辑是填充满了一个,才能走到下一个面前,所以如果成功到达了第u个前面的话,说明前u-1个都是填充满的
        //如果在第u个面前,检查到木棒长度 乘以 木棒数量 等于总长度,说明完成了所有填充工作,递归终止
        if (len * u == sum) return true;
    
        //因为每一个木棒原来都是客观存在的,所以,每组木棍必须可以填充满一个木棒
        //不能填充满,就不能继续填充下一个木棒
        if (last == len) return dfs(u + 1, 0, 0); // 注意当一组完成时,下一组从0开始搜
    
        //在当前木棒没有填充满的情况下,需要继续找某个木棍进行填充
        // start表示搜索开始的位置
        //防止出现 abc ,cba这样的情况,我们要的是组合,不要排列,每次查找选择元素后面的就可以了
        for (int i = start; i < n; i++) {
            //使用过 or 超过枚举的木棒长度
            if (st[i] || last + a[i] > len) continue;
    
            //准备将i号木棍,放入u这个木棒中
            st[i] = true;                                   //标识i号木棍已使用过
            if (dfs(u, last + a[i], start + 1)) return true; //将i号木棍放入u号木棒中
            st[i] = false;                                  //恢复现场
    
            //可行性剪枝
            //优化4:如果在第u组放置失败,且此时第u组长度为0,这是最理解的状态,这种情况都放不下,那么占用了一些空间的情况下,就肯定更放不下!
            if (last == 0) return false;
    
            //优化5:如果加入一个元素后某个分组和等于len了,但是后续搜索失败了(后续肯定是开新组并且last从0开始),则没有可行解,和4是等价的。
            if (last + a[i] == len) return false;
    
            //优化6:冗余性剪枝
            //如果当前未放置成功,则后面和该木棒长度相等的也一样,直接略过即可
            while (i < n - 1 && a[i] == a[i + 1]) i++;
        }
        return false;
    }
    int main() {
        //多组测试数据,以0结束输入
        while (scanf("%d", &n) && n) {
            //多组数组初始化
            sum = 0;                      //木棒总长清零
            memset(st, false, sizeof st); //清空使用状态桶数组
    
            //录入n个木棍的长度
            for (int i = 0; i < n; i++) scanf("%d", &a[i]), sum += a[i]; //总长度
    
            //优化1:按小猫思路,由大到小排序,因为大卡车越往前越好找空放,越往后越不容易找空
            //根据搜索顺序优化,枚举长度小的分支比长度大的分支要多,所以先枚举长度大的
            sort(a, a + n, greater<int>());
    
            //枚举每一个可能的木棒长度,注意这是一个全局量,因为需要控制递归的出口
            for (len = a[0]; len <= sum; len++) { //优化2:从最短的木棍长度开始,可以减枝
                //优化3:如果总长度不能整除木棒长度,那么不能按这个长度设置木棒长度
                if (sum % len) continue;
    
                //在当前len长度的基础上,开始搜索
                if (dfs(1, 0, 0)) {
                    printf("%d\n", len); //找到最短的木棒长度
                    break;               //找到一个就退出
                }
            }
        }
        return 0;
    }
    

    四、桶优化版本

    \(2ms\)

    #include <cstdio>
    #include <cstring>
    #include <cmath>
    
    using namespace std;
    const int N = 70;
    const int INF = 0x3f3f3f3f;
    
    int n;        // n个木棍
    int Min, Max; //最小长度,最大长度
    int sum;      //木棍的总长度
    int len;      //枚举的每个木棒长度
    int b[N];     //桶
    
    /*
      u:正在填充第u个木棒
      last:最后一个木棒填充了的长度
      start:已经填充完的木棍长度,后续的需要小于等于这个长度
    */
    bool dfs(int u, int last, int start) {
        //如果前面u-1个都已经完整填充了,成功创建了第u个木棒
        //前面使用了(u-1)*len个长度,剩余的长度:sum-(u-1)*len,并且sum=len*u,所以此木棒需要装入的长度就是len,肯定能实现完美填充
        if (u == sum / len) return true;
    
        //每个木棒,必须由若干个木棍填充满,否则没有资格创建新的木棒
        //因为早晚都得填充满,就这么多可用的木棍,每个都研究了一遍,还填充不满当前的,说明当前枚举的木棒长度不对,无法填充,返回
        //如果当前木棒已经填满,则创建一个新的木棒,新木棒已填充长度为0,可以从Max最大的数开始填充
        if (last == len) return dfs(u + 1, 0, Max);
    
        //从大到小枚举每个可能的木棍长度,尝试把长度为i的木棍填充进u木棒
        //要装入u木棒中,因为组合的问题,只能装的长度是小于等于start,也就是前一个长度的木棍
        //同时,秉承先放大再放小的贪心原则,倒序枚举每个可能木棍的长度
        for (int i = start; i >= Min; i--)
            if (b[i] && last + i <= len) {            //如果存在此长度的木棍,并且,当前木棒剩余空间可以装的下此木棍的长度
                b[i]--;                               //装一下试试,试一下的话,就用了一个i长度的木棍
                if (dfs(u, last + i, i)) return true; //剩余木棒数量没有减少,还是num。此木棒的长度变为last+i,起始值变成i,表示:下一个用来填充的木棍长度最小是i
                b[i]++;                               //如果没有成功完成最终的填充任务,回溯
                if (last == 0 || last + i == len) return false; //上面回溯了,没有成功,而且,i如果放在开头,或者i放在尾巴上,后继却无法完成完美填充,说明木棒长度不符合,详见题解证明
            }
        return false;
    }
    
    int main() {
        while (scanf("%d", &n) && n) {
            Min = INF, Max = 0; //木棍最小长度,木棍最大长度
            sum = 0;            //木棍总长度
            memset(b, 0, sizeof b); //桶
    
            for (int i = 1; i <= n; i++) {
                int x;
                scanf("%d", &x);                      //木棍长度
                b[x]++;                               //用桶记录木棍长度,主要是因为数据保证每一节木棍的长度均不大于 50,有以下两个特点:1、数据的重复性高,2、范围小,适合用桶来计数
                Min = min(x, Min), Max = max(x, Max); //木棍最小值,木棍最大值。桶计数时,必须标配最小值和最大值,方便确定数据范围,不做无意义的遍历
                sum += x;                             //木棍总长度
            }
            //要求:输出原始木棒的可能最小长度
            //策略:由小到大枚举木棒可能的长度,一旦某个长度可以满足要求,完美还原木棒,则可以停止循环,找到了答案
            //细节:
            //(1)必须大过最长的那个木棍,否则没法装下最长的木棍
            //(2)不可能比木棍总长还长,但是可以相等,相等时表示只有一个木棒,把所有木棍全部装在一个木棒里,此时木棒长度最长,值=sum
            for (len = Max; len <= sum; len++) {
                if (sum % len) continue; //木棒长度必须是总长度的约数,才能保证能平均分开.不能有余数
                if (dfs(1, 0, Max)) {    //如果在当前木棒长度=len的情况下,可以完成完美填充任务,就是找到了最小长度len, 直接输出并退出即可
                    printf("%d\n", len);
                    break;
                }
            }
        }
        return 0;
    }
    

    五、问题与解答

    \(Q\):为什么采用桶优化后,性能提升了\(10\)倍以上,它优化了什么内容?
    \(A\):其实就是优化了如果当前木棍长度\(x\)放不下去的话,其它也是\(x\)长度的木棍就不用再试了,在第一种优化解法中,采用

    while (i < n - 1 && a[i] == a[i + 1]) i++;
    

    进行冗余性剪枝,其实这是没有必要的,是浪费。考虑到区间范围较小,数据的重复度很高,可以考虑采用桶的形式,将数据直接记录到桶中,以

    • 数值
    • 个数
    • 最大
    • 最小
      这四个维度去维护数据,一旦发现某个木棍长度无法完成填充任务后,马上就会跳过所有长度=\(x\)的数字,大大加快了执行速度。

    六、经验总结

    \(dfs\)\(bool\)返回与\(void\)返回的对比:
    1、\(bool\)返回,方便剪枝,\(void\)就不方便,代码冗长
    2、要学习和适应 \(bool\) \(dfs\)返回值方式

  • 相关阅读:
    Verilog手绘FVH信号
    Verilog编码规范与时序收敛
    关于DDS的基础知识
    阅读ug949-vivado-design-methodology笔记
    在windows系统上使用pip命令安装python的第三方库
    pandas第一课
    视频外同步信号研究---fvh
    FPGA调试技巧
    关于FIFO异步复位的问题
    搭建一个microblaze的最小系统
  • 原文地址:https://www.cnblogs.com/littlehb/p/15983444.html
Copyright © 2020-2023  润新知