题目大意
乔治有一些同样长的小木棍,他把这些木棍随意砍成几段,直到每段的长都不超过50。现在,他想把小木棍拼接成原来的样子,但是却忘记了自己开始时有多少根木棍和它们的长度。给出每段小木棍的长度,编程帮他找出原始木棍的最小可能长度。
总体思路
令totLen为所有小木棍总长度,枚举每一个原始木棍的可能长度eachLen(sumLen%eachLen==0),判断那些原始木棍能否全部由小木棍凑成。
判断过程的朴素算法为:枚举原始木棍的每一种排列方式,看看这样能否凑成totLen/eachLen个原始木棍。
一些定义
- 到当前木棍的排列:Dfs到当前木棍时,将Dfs访问过的木棍按访问的顺序排成一行,这一行木棍的排列 叫做到当前木棍的排列。
- 某一木棍的长度前缀和:当前木棍排列中的第一个木棍到“某一”木棍之间(包括这两个木棍)木棍的总长度。
- 前n个木棍能凑成:在到当前木棍的排列中,对于每个k∈[1, N]∩Z,都能在到当前木棍排列中找到一个木棍,使得该木棍的长度前缀和=k*eachLen。
- 组合成的木棍组:在上述每个k∈[1,N]∩Z,k对应的木棍和k-1对应的木棍之间的(不包括k-1对应木棍,包括k对应木棍)木棍们。
- 排序变换:将每个组合成的木棍组内的木棍从大到小排列,然后将木棍组按照木棍组中的第一个木棍的长度从大到小排列。
隐式图中,每个节点的值为:
- 这是前几个木棍
- 到当前木棍的排列。
每条边的值为新增的木棍的长度。
但是此朴素算法效率很低。
整体算法的改变
当Dfs到当前木棍时,如果前(当前木棍的长度前缀和/eachLen)个原始木棍不能凑成,则Dfs下去的结果肯定不成立。所以Dfs到当前节点时,应当保证前(当前木棍的长度前缀和/eachLen)个原始木棍能凑成(条件*)。
怎么表示当前木棍的排列
因为保证了前(当前木棍的长度前缀和/eachLen)个原始木棍能凑成,所以我们不用记录组合成的木棍组的具体排列,只要记录哪些木棍访问过了即可。而对于第(当前木棍的长度前缀和/eachLen+1)个原始木棍(以后简称当前原始木棍),我们要达到的,是使得以后在处理后一个原始木棍时,仍然满足条件*。换句话说,就是把当前处理的原始木棍拼满。所以我们要记录一个rest,表示要凑成当前原始木棍,还需要的小木棍的长度为多少。这样,每个节点的值就变成了:
- 当前是前几个木棍(cnt)。
- rest.
- 每个木棍是否访问过的集合。
剪枝
排序
假设:对于每个能凑成totLen/eachLen个原始木棍的排列,对其进行排序变换。对于每种排列,变换过后的结果与原来是等价的,但是多个排列经过变换后变成了同一种排列。这样,我们规定每一种可行排列都满足每个木棍组内的木棍长度递减,木棍组的第一个木棍的长度递减,这样就缩小了解空间。具体操作为:运算前先将每个木棍从大到小排序。Dfs当前木棍时记录当前从数组中哪个下标处开始,备选下一个木棍就从当前位置枚举。
为什么排序从大到小
因为如果一个木棍的排列不能凑成凑成totLen/eachLen个原始木棍,长度长的木棍在先可以较早地判断出该排列不满足要求。
排除显然不成立的情况
位于当前木棍扩展时,当前备选木棍与上一次的备选木棍长度相等
遇到本情况显然跳过,因为Dfs到上一次所选节点时,其备用木棍组合包括了Dfs到当前备选木棍时的木棍组合。前者都不成立,后者当然不成立。
(当前木棍的长度前缀和%eachLen)==0时,第一个备选木棍便无法(通过放入第(当前木棍的长度前缀和/eachLen+1)个原始木棍)来凑成所有原始木棍。
本情况也要跳过。假设不跳过,则这第一个备选木棍就要在后后个原始木棍中再次出现。对以后的组合成的木棍组进行排序变换,则那个木棍又出现在了第(当前木棍的长度前缀和/eachLen+1)原始木棍中,这与当前情况矛盾。
当前木棍的长度=eachLen,还不能凑成所有原始木棍
跳过,证明与上同理。
#include <cstdio> #include <algorithm> #include <cstring> #include <cassert> using namespace std; const int MAX_STICK = 70; bool Vis[MAX_STICK]; int Len[MAX_STICK]; int Tot; #define LOOP(i, n) for(int i=1; i<=n; i++) #define LoopFrom(i, m, n) for(int i=m; i<=n; i++) bool cmp(int a, int b) { return a > b; } bool Dfs(int cnt, int rest, int eachLen, int begin) { if (cnt == Tot) return rest == 0; if (rest == 0) { rest = eachLen; begin = 1; } int prevLen = -1; LoopFrom(next, begin, Tot) { if (Len[next] == prevLen || Vis[next] || rest-Len[next]<0) continue; prevLen = Len[next]; Vis[next] = true; if (Dfs(cnt + 1, rest - Len[next], eachLen, next+1)) return true; else if (rest == eachLen || rest-Len[next]==0) { Vis[next] = false; return false; } else { Vis[next] = false; continue; } } return false; } int main() { #ifdef _DEBUG freopen("c:\noi\source\input.txt", "r", stdin); #endif Tot = 0; int tempTot; scanf("%d", &tempTot); int maxLen = 0, sumLen = 0, stickLen; while(tempTot--) { scanf("%d", &stickLen); if (stickLen <= 50) { Len[++Tot] = stickLen; maxLen = max(maxLen, stickLen); sumLen += stickLen; } } sort(Len + 1, Len + Tot + 1, cmp); LoopFrom(eachLen, maxLen, sumLen) { if (sumLen%eachLen) continue; memset(Vis, false, sizeof(Vis)); if (Dfs(0, 0, eachLen, 1)) { printf("%d ", eachLen); break; } } return 0; }