POJ 1011 拯救少林神棍(Sticks)
Description
乔治拿来一组等长的木棍,将它们随机地砍断,使得每一节木棒的长度都不超过50个长度单位。然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棍以及木棍的初始长度。请你设计一个程序,帮助乔治计算木棍的可能最小长度。每一节木棒的长度都用大于零的整数表示。
Input
输入包含多组数据,每组数据包括两行。
第一行是一个不超过64的整数,表示砍断之后共有多少节木棒。
第二行是截断以后,所得到的各节木棒的长度。
在最后一组数据之后,是一个零。
Output
为每组数据,分别输出原始木棍的可能最小长度,每组数据占一行。
Sample Input
9
5 2 1 5 2 1 5 2 1
4
1 2 3 4
0
Sample Output
6
5
AC代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <memory.h>
using namespace std;
int N, L;//定义木棒数目和假设棍子的长度
vector<int> anLength;//用于存放所有木棒长度
int anUsed[65];//是否用过的标记且全局变量自动初始化为0即木棒未使用过
int nLastStickNo;//用于储存最近那根拼上去的木棒下标
bool DFS(int nUnusedSticks, int nLeft);
int main()
{
while(1)
{
cin >> N;//输入所有木棒根数
if( N == 0 ) break;
int nTotalLen = 0;//所有木棒总长度
anLength.clear();
for(int i = 0;i < N; i++)
{
int n;//单根木棒长度
cin >> n;
anLength.push_back(n);
nTotalLen += anLength[i];
}
sort(anLength.begin(),anLength.end(),greater<int>()); //要使木棒从长到短进行尝试,好比天平上称物品,加砝码也是从大到小试
for(L = anLength[0];L <= nTotalLen / 2; L++) //从最长的木棒开始从长到短依次枚举木棍长度L
{
if(nTotalLen % L) continue;
//能整除代表你可以拼成若干组等长木棍,不能整除代表你可能会拼得几根等长的棍子,但最后总会有木棒剩下
memset(anUsed, 0, sizeof(anUsed));//初始化anUsed数组元素为0
if(DFS(N, L)) //s根可用的木棒,L为假设的木棍长度
{
cout << L << endl;
break;
}
}
if(L > nTotalLen / 2) cout << nTotalLen << endl;
//如果枚举出的木棍长度大于所有木棒总长度的一半那么就没法拼成两根或更多木棍,故此木棒总长度便是最短的木棍长度
}
return 0;
}
bool DFS(int nUnusedSticks, int nLeft)
{
// nLeft表示当前正在拼的棍子和 L 比还缺的长度
if(nUnusedSticks == 0 && nLeft == 0)
return true;//没有木棒剩下且没有哪根棍子缺长度,任务完成!
if(nLeft == 0) //木棒剩下且没有哪根棍子缺长度,证明一根木棍刚刚拼完,再新拼一根
nLeft = L; //开始拼新的一根木棍
//剪枝4:
int nStartNo = 0;
if(nLeft != L) //拼这根棍子的木棒不是第一根木棒
nStartNo = nLastStickNo + 1;//从上一次选取的后面进行选取,保证了拼木棍时木棒先长后短
for(int i = nStartNo;i < N; i++)
{
if(!anUsed[i] && anLength[i] <= nLeft)
{
//剪枝1:
if(i > 0)
{
if(anUsed[i-1] == false && anLength[i] == anLength[i-1])
continue;//若木棒之前一根木棒用过或和前一根等长那么就不用这根木棒
}
anUsed[i] = 1;
nLastStickNo = i;
if (DFS(nUnusedSticks - 1, nLeft - anLength[i]))
return true;
else{
anUsed[i] = 0;//说明本次不能用这第i根木棒但这第i根以后还有用
//剪枝2和3:
if(nLeft == L||anLength[i] == nLeft) return false;
//2:这第i根木棒是这根棍子的第一根木棒,但anUse[i]=0表示这第i根木棒不能用,故拼接失败
//3:这第i根木棒是这根棍子最后一根木棒,但anUse[i]=0表示这第i根木棒不能用,故拼接失败
}
}
}
return false;//所有的木棒都拿来试过了,但棍子长度不合理故return false
}
DEV C++测试结果:
算法解析
DFS的基本递推关系:
bool DFS(int R, int M)
{
if( R == 0 && M == 0)
return true; //拼接任务完成
//若能找到一根长度不超过M的木棒, 假设长为S,拼在当前棍子上,然后:
DFS(R – 1,M - S);
//如果找不到:
return false;
}
剪枝 1
不要在同一个位置多次尝试相同长度的木棒。
即:如果某次拼接选择长度为S 的木棒,导致最终失败,则在同一位置尝试下一根木棒时,要跳过所有长度为S 的木棒。
剪枝 2
如果由于以后的拼接失败,需要重新调整第i根棍子的拼法,则不会考虑替换第i根棍子中的第一根木棒(换了也没用)。如果在不替换第一根木棒的情况下怎么都无法成功,那么就要推翻第 i − 1 i-1 i−1根棍子的拼法。如果不存在第 i − 1 i-1 i−1根棍子,那么就推翻本次假设的棍子长度,尝试下一个长度。
若棍子 i i i 如下拼法导致最后不能成功:
可以考虑把木棒2,3换掉重拼棍子i,但是把2,3都去掉后,换1是没有意义的。
为什么替换第i根棍子的第一根木棒是没用的?
因为假设替换后能全部拼成功,那么这被换下来的第一根木棒,必然会出现在以后拼好的某根棍子k中。那么我们原先拼第i根棍子时, 就可以用和棍子k同样的构成法来拼,照这种构成法拼好第i根棍子,继续下去最终也应该能够全部拼成功。
剪枝 3
不要希望通过仅仅替换已拼好棍子的最后一根木棒就能够改变失败的局面。
假设由于后续拼接无法成功,导致准备拆除的某根棍子 i i i 如下:
将 3 拆掉,留下的空用其他短木棒来填是徒劳的!
假设替换3后最终能够成功,那么3必然出现在后面的某个棍子 k k k 里。将棍子 k k k 中的3和棍子 i i i 中用来替换3的几根木棒对调,结果当然一样是成功的。这就和i原来的拼法会导致不成功矛盾
剪枝 4
拼每一根棍子的时候,应该确保已经拼好的部分,长度是从长到短排列的,即拼的过程中要排除类似下面这种情况:
未完成的棍子 i i i
木棒3 比木棒2长,这种情况的出现是一种浪费。因为要是这样往下能成功,那么2, 3 对调的拼法肯定也能成功。由于取木棒是从长到短的,所以能走到这一步,就意味着当初将3放在2的位置时,是不成功的
排除办法:每次找一根木棒的时候,只要这不是一根棍子的第一条木棒,就不应该从下标为0的木棒开始找,而应该从刚刚(最近)接上去的那条木棒的下一条开始找。这样,就不会往2后面接更长的3了
为此,要设置一个全局变量 nLastStickNo ,记住最近拼上去的那条木棒的下标。