【题解】小木棍

题目来源:loj

题目描述

乔治有一些同样长的小木棍,他把这些木棍随意砍成几段,直到每段的长都不超过50。现在,他想把小木棍拼接成原来的样子,但是却忘记了自己开始时有多少根木棍和它们的长度。给出每段小木棍的长度,编程帮他找出原始木棍的最小可能长度。

输入

第一行为一个单独的整数N表示砍过以后的小木棍的总数,其中N≤60,第二行为N个用空个隔开的正整数,表示N根小木棍的长度。

输出

仅一行,表示要求的原始木棍的最小可能长度。

输入样例

9
5 2 1 5 2 1 5 2 1

输出样例

6

思路

从题意来看,要得到原始最短木棍的可能长度,可以按照分段数的长度,
依次枚举所有的可能长度len
每次枚举len时,用深搜判断是否能用截断后的木棍拼合出整数个len,能用的话,找出最小的len即可
对于1S的时间限制,用不加任何剪枝的深搜时,时间效率为指数级,效率非常低,程序运行将严重超时。对于此题,可以从可行性和最优性上加以剪枝
从最优性方面分析,可以做以下两种剪枝:
  1. 设所有木棍的长度和是 maxx ,那么原长度(也就是需要输出的长度)一定能够被 maxx 整除,不然就没法拼了,即一定要拼出整数根
  2. 木棍原来的长度一定大于等于所有木棍中最长的那一根
综合上述两点,可以确定原木棍的长度 len 在最长木棍的长度与 maxx 之间,且 maxx 能被 len 整除。所以,在搜索原木棍的长度时,可以设定为从截断后所有木棍中最长的长度开始,每次增加长度后,必须能整除 maxx 。这样可以有效地优化程序
从可行性方面分析,可以再做以下七种剪枝:
  1. 一根长木棍肯定比几根短木棍拼成同样长度的用处小,即短小的可以更灵活组合,所以可以对输入的所有木棍按长度从大到小排序
  2. 在截断后的排好序的木棍中,当用木棍i拼合原始木棍时,可以从第 i+1 后的木棍开始搜。因为根据优化1,i前面的木棍已经用过了
  3. 用当前最长长度的木棍开始搜,如果拼不出当前设定的原木棍长度 len ,则直接返回,换一个原始木棍长度 len
  4. 相同长度的木棍不需要搜索多次。用当前长度的木棍搜下去得不出结果时,用一支同样长度的还是得不到结果,所以可以提前返回
  5. 判断搜到的几根木棍组成的长度是否大于原始长度 len ,如果大于,没必要搜下去,可以提前返回
  6. 判断当前剩下的木棍根数是否够拼成木棍,如果不够,肯定拼合不成功,直接返回
  7. 找到结果后,在能返回的地方马上返回到上一层的递归处
    (以上部分文字来自《信息学奥赛一本通-提高篇》码这么多字累死我了
    另外,在洛谷找到了以为很细心很细心的博主写的题解,可以康康

按照《信息学奥赛一本通-提高篇》的代码的话,每次在匹配原始木棍时只要第 k 根木棍匹配成功之后就不管它了,可是可能出现这样一种情况:
假设当前的 len 就是答案,在匹配第 k 根木棍时用了截断的木棍i,可是在匹配后面的第 k+1 根木棍时匹配不成功,就直接返回,把 len 这个正确答案给否掉,为什么呢,因为第 k+1 根木棍要用到这个截断的木棍 i ,可是这根木棍i已经被第k根用了,而第 k 根木棍有两种或者多种匹配方案,书里的程序是只要木棍k一匹配成功就不管它,它用到的木棍i就一直是 has been used ,就轮不到第 k+1 根木棍用了
(上面这个是我用书上的程序走了一遍第7个测试点发现的,有兴趣的小伙伴们可以have a try )
所以要在 dfs(k+1,i,len-a[i]) 后面加一句 use[i]=0 ,回溯,否则程序在逻辑上有错误,至于超时,是因为书中程序还没有完全发挥剪枝 9 的作用,剪枝 9 里面说在能返回的地方马上返回到上一层的递归处,其实在 dfs(k,i,rest-a[i]) 的后面可以判断当前的 len 是否正确,正确的话直接返回,不用再继续下去找匹配(后面的匹配只是在找另一组匹配而已)

code

//在这里用"大木棍"表示原始长度的木棍,用"小木棍"表示砍过后的木棍 
#include
using namespace std;
int n,m,maxx,vis[105],len,a[105];
bool pd;
int cmp(int x,int y) {return x>y;}
void dfs(int k,int last,int rest) 
//k:当前在拼第几根大木棍(原始长度的)
//last:在拼这根大木棍时,用到的上一截小木棍的编号last,这一截就可以直接从last+1开始(因为在last之前的木棍已经用过了)
//rest:要拼成第k跟大木棍还需要的长度 
{
	if (k==m) {pd=true; return;}   //剪枝9,已经拼到最后一根大木棍了,剩余的小木棍长度一定等于len,可以直接返回true 
	if (rest==0) //这根大木棍拼完了
	{
		int i;
		for (i=1;i<=n;i++)
		  if (vis[i]==0) break;//剪枝4,找到一根还没有归位的小木棍 
		vis[i]=1; //标记 
		dfs(k+1,i,len-a[i]); //拼下一根大木棍 
		vis[i]=0;//这一步回溯很关键,因为如果不将这里回溯的话,就会出现刚刚说的情况,大木棍k占了另一根大木棍的小木棍。。 
	} 
	for (int i=last+1;i<=n;i++) //剪枝5和7,寻找可以拼入当前大木棍的小木棍
	{
		if (vis[i]==0&&a[i]<=rest) //第i根小木棍没有用过,而且拼进去不会超出长度
		{
			vis[i]=1; //标记 
			dfs(k,i,rest-a[i]); //继续拼这根大木棍 
			vis[i]=0; //回溯,说明此时需要换一根小木棍来拼这根大木棍 
			if (pd==true) return;//这个优化尤其重要!发现当前的len是答案时就立马返回,不要再继续下去匹配了! 
			while (a[i]==a[i+1]) i++;//剪枝6,跳过与小木棍i长度相同的小木棍 
		} 
	} 
}
void init()
{
	scanf("%d",&n);
	for (int i=1;i<=n;i++) 
	{
		scanf("%d",&a[i]);
		maxx+=a[i]; //大木棍一定小于等于所有小木棍的长度总和 
	}
	sort(a+1,a+1+n,cmp); //剪枝3,将小木棍从大到小排序,因为木棍长度越小越灵活 
}
int main()
{
//	freopen("stick.in","r",stdin);
//	freopen("stick.out","w",stdout);
	init();
	for (len=a[1];len<=maxx;len++) //剪枝2,因为a[]已经排过序了,所以a[1]就是a[]中最大的一个 
	{
		if (maxx%len!=0) continue;//剪枝1,原始木棍一定能够被木棍长度总和给整除,这样才能分成一西恩同样长的木棍
		pd=false;
		vis[1]=1;
		m=maxx/len; //原始木棍长度为len时需要m根木棍 
		dfs(1,1,len-a[1]);//从第一根木棍开始拼,因为len一定大于等于a[i],所以可以直接把a[1]用来作为第一根木棍的第一截 
		if (pd==true)
		{
			printf("%d\n",len); //从小到大搜,第一个可行的len就是答案 
			return 0;
		}
	} 
}

你可能感兴趣的:(题解,搜索)