|
|
|
|
## [$AcWing$ $167$. 木棒](https://www.acwing.com/problem/content/169/)
|
|
|
|
|
|
|
|
|
|
### 一、题目描述
|
|
|
|
|
乔治拿来 **一组等长** 的木棒,将它们随机地砍断,使得每一节木棍的长度都 **不超过** $50$ 个长度单位。
|
|
|
|
|
|
|
|
|
|
然后他又想把这些木棍 **恢复到为裁截前的状态** ,但忘记了 **初始时有多少木棒** 以及 **木棒的初始长度**。
|
|
|
|
|
|
|
|
|
|
请你设计一个程序,帮助乔治计算木棒的 **可能最小长度**。
|
|
|
|
|
|
|
|
|
|
每一节木棍的长度都用大于零的整数表示。
|
|
|
|
|
|
|
|
|
|
**输入格式**
|
|
|
|
|
输入包含多组数据,每组数据包括两行:
|
|
|
|
|
|
|
|
|
|
第一行是一个不超过 $64$ 的整数,表示砍断之后共有多少节木棍。
|
|
|
|
|
|
|
|
|
|
第二行是截断以后,所得到的各节木棍的长度。
|
|
|
|
|
|
|
|
|
|
在最后一组数据之后,是一个零。
|
|
|
|
|
|
|
|
|
|
**输出格式**
|
|
|
|
|
为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**备注**
|
|
|
|
|
> $1≤N≤60$
|
|
|
|
|
|
|
|
|
|
**输入样例**:
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
9
|
|
|
|
|
5 2 1 5 2 1 5 2 1
|
|
|
|
|
4
|
|
|
|
|
1 2 3 4
|
|
|
|
|
0
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**输出样例**:
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
6
|
|
|
|
|
5
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 二、朴素$dfs$
|
|
|
|
|
<font color='red' size=4><b>这道题可以说是搜索剪枝例题中的经典,如果能够完全吃透这道题,对剪枝的领悟一定能更深!</b></font>
|
|
|
|
|
|
|
|
|
|
<!-- 让表格居中显示的风格 -->
|
|
|
|
|
<style>
|
|
|
|
|
.center
|
|
|
|
|
{
|
|
|
|
|
width: auto;
|
|
|
|
|
display: table;
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
margin-right: auto;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
<div class="center">
|
|
|
|
|
|
|
|
|
|
| 东西 | 尺寸 |
|
|
|
|
|
| ---- | ---- |
|
|
|
|
|
| 木棒 | 大 |
|
|
|
|
|
| 木棍 | 小 |
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
**朴素版本** 的搜索代码:
|
|
|
|
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#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$开始找木棍进行填充,并且记录这个上一个是从哪个下标开始的,新选择的木棍号码,一定要比上一个大就行,不可以选择比上一个小的,防止了非单调递增序的产生,下面的我们首先需要理解的组合与排列的对比代码:
|
|
|
|
|
|
|
|
|
|
**排列**
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#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;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**组合**
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#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$
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#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$木棒
|