AcWing 167. 木棒

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

一、题目描述

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

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

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

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

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

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

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

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

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

备注

\(1≤N≤60\)

输入样例

9
5 2 1 5 2 1 5 2 1
4
1 2 3 4
0

输出样例

6
5

二、朴素\(dfs\)

这道题可以说是搜索剪枝例题中的经典,如果能够完全吃透这道题,对剪枝的领悟一定能更深!

东西 尺寸
木棒
木棍

朴素版本 的搜索代码:

#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) {
    // 如果上一个木棒填充完整,开启了新的木棒,那么,新木棒的当前木棍总长是0,同时,如果u个木棒全部装满就可以包含所有,就是找到了合适的木棒长度和木棒个数
    if (u * len == sum) return true;

    // 如果当前木棒已填满,开启下一个木棒
    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; // 装入后,此路径的后续汇报了成功标识,则本路径就是成功的,不需要再继续搜索了
        // 注意:这里不能直接 return dfs(u,last+a[i]),原因是因为即使按现在这样的装法不能成功,但不代表着不可以尝试其它的木棍,不能直接 return
        // 这个bool和void的使用方法有一些区别,冉要仔细体会 ,也可以理解为在使用 bool dfs时,return要小心再小心,就能不犯错误
        st[i] = false; // 回溯
    }
    return false; // 如果上面的代码中未能return true,就表示进入了一条死的路径,当前len不合法 或者是 当前木棒选择的木棍不正确
}
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\)了,需要优化。

三、优化

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

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

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

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

也就是说我们在枚举\(len\)时,可以通过上面的性质,避免一些无效的、肯定不对的木棒长度枚举,从而起到优化的目的。

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 <bits/stdc++.h>
using namespace std;
const int N = 70;
int a[N];   // 用来装每个木棍的长度
int n;      // 木棍个数
int len;    // 组长度
int sum;    // 总长度
bool st[N]; // 标识某个木棍是不是使用过了

/*
u   :木棒号,下标从0开始
last:最后一个木棒目前的长度
start:从哪个索引号开始继续查找

只要搜索到解就停止的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;
}
posted @ 2022-03-09 08:38  糖豆爸爸  阅读(173)  评论(0编辑  收藏  举报
Live2D