#10020. 「一本通 1.3 例 3」小木棍

这篇博客介绍了如何解决一个递归问题:根据不超过50长度的木棍碎片,找回原始木棍的最小可能长度。通过理解递归、回溯和剪枝策略,结合最优性和可行性分析,确定搜索区间并应用二分法提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【题目描述】

原题来自:CERC 1995

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

【输入格式】

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

【输出格式】

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

【样例输入】

9
5 2 1 5 2 1 5 2 1

【样例输出】

6

【数据范围与提示】

1≤N≤60

记得看这个递归的解释,看完再看下面,好理解一点感性理解递归

【思路】:

这道题是一道很经典很经典的递归题,因为这道题当中用到的回溯绝对是扛扛的,反倒我觉得这道题的剪枝不特别特别的多,多的只是回溯,难的也是回溯,回溯到上一次的递归,我花了好多时间才理解清楚递归当中return是怎么一会事。

接下来回到这个最正规的思路:

从最优性方面:

  1. 设所有木棍长度和为maxn,那么原长度(也就是需要输出的长度)一定能够被maxn整除,这样得到的木棍根数才是整数
  2. 木棍原来的长度一定不小于所有木棍中最长的那根
  3. 综上两点,可以确定原木棍的长度len在最长木棍的长度minx和maxn之间取值,且maxn能被len整除。所以在搜索原木棍的长度时,可以从砍过以后所有木棍中最长的长度开始,每次增加长度后,必须能整除maxn。这样可以有效优化程序。

从可行性方面:

  1. 短木棍更加灵活,长木棍受到的限制更大,所以可以对输入的所有木棍按长度从大到小排序。
  2. 在砍断后的排好序的木棍中,当用木棍i拼合原始木棍时,可以从i+1的木棍开始往后搜,因为i前面的木棍已经用过了
  3. 从当前最长长度的木棍开始搜,如果拼不出当前设定的原木棍长度len则直接返回,换一个原始木棍长度len
  4. 相同长度的木棍不要搜索多次.用当前长度的木棍搜下去得不出结果时,用一支同样长度的还是得不到结果,所以可以提前返回
  5. 判断搜到的几根木棍组成的长度是否大于原始长度len,如果大于没必要搜下去,可以提前返回
  6. 判断当前剩下的木棍根数是否够拼成木棍,如果不够,肯定拼合不成功,直接返回
  7. 找到结果后,在能返回的地方马上返回到上一层的递归处

从总的思路上:

  1. 标准回溯一定要有
  2. 木棍的区间要确定
  3. 所有木棍长度一定可以被len整除
  4. 几个剪枝要知道
  5. 这道题想要AC还要加上二分,来节省搜索时间

思路大概就是我上面所说的,剩下的细节看代码的解释吧。 

 【代码实现】

#include<cstdio>
#include<cstring> 
#include<algorithm>
using namespace std;
int n,d,a[70],cnt,minx,maxn,len,nxt[70],m;
//a[i]是用来记录每个标号(cnt)所代表的数 //minx和maxn分别表示下限和上限 //len表示的是我们的区间中的值 
//nxt[i]表示的就是记录当前相同的数的版块 //m表示我们预估的这个原来的根数
bool vis[70],flag;//vis[i]判断i是否用过(1表示用过的(true),0表示没用过的(false)) //flag表示我们要找到的目标 
bool cmp(int x,int y) 
{
	return x>y;
}
void dfs(int k,int last,int res)
{
	//k表示当前木棍编号,last为正在拼的木棍的前一节编号,res为还需长度
	if(res==0)//还需长度为0,证明拼完了
	{
		if(k==m)//m根木棍都拼好了
		{
			flag=true;//记录为成功,也就是找到了一个可以执行目标 
			return;//返回这个答案到上一个递归 
		}
		int i;
		for(i=1;i<=cnt;i++)//又找到一个还没用过的木棍
			if(vis[i]==false) break;//如果我们判断这是没用过的就退出这个循环,进入到下面 
		vis[i]=true;//定义为用过的(占用资源) 
		dfs(k+1,i,len-a[i]);//用它拼接
		vis[i]=false;//拼接完之后就又可以用了(释放资源) 
		if(flag==1) return;//找到答案就可以层层退出(也就是返回上一层递归)
	}
	int l=last+1,r=cnt,mid;
	while(l<r)//二分查找下一次拼接的木棍,该木棍是不大于所需长度的第一根木棍
	{
		mid=(l+r)>>1;//等于:mid=(l+r)/2; 
		if(a[mid]<=res) r=mid;//如果小于我们所需要的就要往左边找,因为我们的a数组是排序过从大到小 
		else l=mid+1;//否则往右边找,排序过的从大到小 
	}
	for(int i=l;i<=cnt;i++)/*从l开始缩短搜索范围,之间在这个还需要的长度的区间当中找合适的,
	由于所有木棍是按从大到小的顺序排的,因此从上述找到的木棍向右枚举*/
	{
		if(vis[i]==false)//如果没有用过 
		{
			vis[i]=true;//记录为用过的(占用资源) 
			dfs(k,i,res-a[i]);
			vis[i]=false;//搜索完又变成没用过的了(释放资源) 
			if(flag==true) return;//如果是我们找到目标就返回答案 
			if(res==a[i] || res==len) return;
			/*当前正在拼的长棍剩余的未拼长度等于当前小木棍的长度,
			说明它只能自组,但继续拼下去却失败,说明它不能自组。*/
			
			/*当前木棍剩余的未拼长度等于原始长度,说明这根原来的长棍还一点没拼。
			还需要继续拼接,但继续拼下去却失败 ,所以无法用上它。*/
			//flag=0 表示 继续拼下去失败
			i=nxt[i];//把我们找到的这个i的值记录到nxt数组的模块当中 
			if(i==cnt) return;/*如果两个是相同的话,
			就是说我们已经到了这个循环的最后一个*/ 
		}
	}
	return; 
}
int main()
{
	scanf("%d",&n); 
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&d);
		if(d<=50)/*限制条件:每段的长不能超过这个50*/
		{
			a[++cnt]=d;/*cnt是用来记录这个a算组中的顺序,就是把我们的输入存储到这个a数组当中,*/ 
			minx=max(minx,a[cnt]);//下线,最最最小的长度一定是这一组当中最大的 
			maxn+=a[cnt];//上限,最最最大的长度就是所有的长度和 
		}
	}
	sort(a+1,a+cnt+1,cmp);/*把所选的小木棍从长到短排序,从长的开始选择,因为短的灵活好用*/
	nxt[cnt]=cnt;/*记录当前有多少个相同的数的版块只,能记录就是nxt[cnt]=cnt
	所以刚开始只有最后一个是成立的*/ 
	for(int i=cnt-1;i>0;i--)//cnt是我们拥有的个数,所以我们的这个版块的排序是从小到大,而不是从大到小 
	{
		if(a[i]==a[i+1]) nxt[i]=nxt[i+1];/*如果前后两个数是一样的话,
		那么他们所代表的大小顺序也是一样的,就把他们分成同一版块*/ 
		else nxt[i]=i;//否则就各自一个占用一个模块 
	}
	for(len=minx;len<=maxn/2;len++)/*长度最小就是我们定义的下线,
	最大也不可能超过总和的一半,因为至少要分成两组啊*/
	{
		if(maxn%len==0)//若当前的长度恰能被总长度整除,则这个长度是合法的 
		{
			m=maxn/len;//假设原先有m根木棍
			flag=0;/*我们还没有找到合适的目标*/
			vis[1]=1;/*初始化全部都可以用*/
			dfs(1,0,len-a[1]);/*从第一根木棍开始*/
			vis[1]=0;/*被占用了*/
			if(flag==1)//找到了目标(最上面的那一层递归返回来的) 
			{
				printf("%d\n",len);//就输出当前我们找到的这么len的值 
				return 0; 
			}
		}
	}
	printf("%d\n",maxn);/*如果找不到这个区间当中的len的话,
	就直接输出全部长度,也就是只有一个木棍*/ 
	return 0;
}

大概就是这样了,回溯什么的是这道题当中最难的 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值