木棒 题解
直接上题。。。
题目描述
乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过50个长度单位。然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。
请你设计一个程序,帮助乔治计算木棒的可能最小长度。每一节木棍的长度都用大于零的整数表示。
输入格式
输入包含多组数据,每组数据包括两行。第一行是一个不超过64的整数,表示砍断之后共有多少节木棍。第二行是截断以后,所得到的各节木棍的长度。在最后一组数据之后,是一个零。
输出格式
为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。
样例输入
9 5 2 1 5 2 1 5 2 1 4 1 2 3 4 0### 样例输出
6 5
分析
我们可以直接从小到大枚举原始木棒的长度 \(\mathrm{len}\) ,当然也就是我们的答案。不难看出,\(\mathrm{len}\) 一定为所有木棒长度总和 \(\mathrm{sum}\) 的因数,而原始木棒的根数 \(\mathrm{cnt}\) 就等于 \(\mathrm{sum/len}\)。
对于每一个 \(\mathrm{len}\),我们可以依次搜索没更原始木棒都由哪些木棒拼成。具体来讲,搜索需要维护的信息包括:已经拼好的原始木棒根数,正在拼的木棒的当前长度,每个木棒的使用情况。 在每一个状态中,我们从尚未使用的木棒中选择一个,尝试拼在当前原始木棒里,然后递归到下一个状态。递归边界有两个,成功拼好 \(\mathrm{cnt}\) 根木棒,或者无法继续拼接而宣布失败。
这个算法目前看来效率比较低,于是。。。
具体实现
先来介绍一下剪枝这个神器。剪枝,很形象,就是帮我们的搜索去掉一些不需要的重复的东西,从而提高效率。其基本型有 5种。
1.优化搜索顺序,在一些搜索问题中,搜索树的各个层次,各个分支之间的顺序是不固定的。不同的搜索顺序会产生不同的搜索树形态,其规模也相差甚远。
2.排除等效冗余,在搜索过程中,我们如果判定通过几个分支达到目的是等效的(eg.耗时耗空间相同),则我们只需要对其中一条分支进行搜索。
3.可行性剪枝(上下界剪枝),在搜索过程中,我们对当前状态进行检查,如果发现当前剪枝无法实现目的,就直接执行回溯。就好比,走迷宫的时候看到前面是死胡同就回头,而不是走到路的尽头在返回。
4.最优性剪枝,在最优化问题中,如果一个当前的代价已经超过了当前已知的最优解则沿着这条分支走下去永远都不会更新答案,此时可以直接执行回溯。
5.记忆化,可以记录每个状态的搜索结果,在重复遍历一个状态时可以直接检索并返回。
回到此题,我们来依次考虑几个剪枝。
- STEP1:优化搜索顺序,把木棒长度从大到小排序,优先选择较长的木棒
- STEP2:排除等效冗余
- 1)限制先加入的木棒的长度是递减的。比如先放入长度为 \(\mathrm{x}\) 的木棒,在放入 \(\mathrm{y}\) 的木棒与先放入 \(\mathrm{y}\),再放入 \(\mathrm{x}\) 显然是等价的,所有只需搜索其中一种。
- 2)对于当前原始木棒,我们记录一个最近一次尝试拼接的长度。如果分支搜索失败则无需再向该木棒尝试拼入其他相同长度的木棒。
- 3)如果第一个尝试拼入的木棒返回失败,则可以直接判定当前分支失败,立即回溯。EMMMM……怎么说呢,我一开始也是懵的,仔细一想它其实因为拼入这根木棒前,面对的原始木棒都是空的(还没有进行拼接),所以这些木棒都是等效的。木棒拼在当前木棒失败,拼在其他木棒也一定会失败。
- 4)如果在当前u原始木棒中拼入一根木棒后,木棒恰好完整,并且接下来拼接剩余原始木棒的分支返回失败,那么直接判定当前分支失败。再用贪心解释一遍,再用一根木棒恰好拼完必然比再用若干根木棒拼完更好
上述剪枝分别利用“同一木棒上顺序的等效性”,“等长木棒的等效性”,“空木棒的等效性”已经“贪心”,大大提高了搜索效率
ps.此题在输入时必须判断输入的当前木棒长度是否大于50
------------算法思路参考李煜东《算法竞赛》
完整代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 105;
int a[MAXN], vis[MAXN], n, len, cnt, j;
// a 为木棒长度数组,vis 记录此木棒是否被用过,n 为木棒数
// len为枚举的原始木棒长度,cnt为枚举的原始木棒数,j为输入的符合条件的木棒总数
bool cmp(int x, int y) {return x > y;}
bool dfs(int stick, int cur, int last) {
// 正在拼接第stick根木棒(已拼接stick-1根),当前长度为cur,拼接到第stick根木棒的上一根木棒为last
if(stick > cnt)
// 所有都拼好,搜索成功
return true;
if(cur == len)
// 第stick根已拼好,开始拼下一根
return dfs(stick + 1, 0, 1);
int k = 0; // 剪枝 2)
for(int i = last; i <= j; i++) { // 剪枝 1) 从last开始枚举
if(!vis[i] && cur + a[i] <= len && k != a[i]) {
vis[i] = true;
if(dfs(stick, cur + a[i], i + 1) == true)
return true;
k = a[i];
vis[i] = false;
if(cur == 0 || cur + a[i] == len) return false;
// 剪枝 3) 4)
}
}
return false;
// 所有分支均尝试过,搜索失败
}
int main() {
while(scanf ("%d", &n) && n) {
int sum = 0, v = 0;
j = 0;
for(int i = 1; i <= n; i++) {
int x;
scanf ("%d", &x);
if(x > 50) continue;
// 判断是否满足题目条件
a[++j] = x;
sum += a[j];
v = max(v, a[j]);
}
sort(a + 1, a + j + 1, cmp);
for(len = v; len <= sum; len++) {
if(sum % len != 0) continue;
cnt = sum / len;
memset(vis, 0, sizeof vis);
if(dfs(1, 0, 1)) break;
}
printf("%d\n", len);
}
return 0;
}