You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;
}

五、理解与反思

问题:老师的做法是把当前组放满,刚好放满了就开新组,如果这个组怎么也放不满,那就说明前面放错了。 这个方法和小猫爬山不一样,小猫爬山是对于当前猫,遍历所有的组,看看哪个组能放进去,然后也要尝试开新组,这里并不能贪心说如果有一个组能放就不开新组了。 我一直有个困惑,不知道应该是遍历物品(老师的木棍做法)还是遍历组(老师的小猫爬山做法)。

小猫爬山,没有说一定要把每台车都装满,如果有空座,但小猫坐不下,太沉了,可以再租一辆车。所以需要枚举小猫,为小猫找车。 本题是要求木棍一定要填充满木棒,不填满不算完美解决,所以我们保证必须A木棒满了,才能创建B木棒