【题目链接】
ybt 1442:【例题3】小木棍
洛谷 P1120 小木棍
注:【ybt 1442:【例题3】小木棍】不保证每个输入都 <=50,所以需要将>50的数据过滤掉,不考虑。
【题目考点】
1. 深搜剪枝
【解题思路】
所有的小木棍是由等长的原木棍切成的,该问题求的是原木棍的最小长度。
如果两根木棍长度相同,其作用也是相同的。因此长度相同的木棍可以作为一类元素。因此想到可以使用“桶”来保存各个木棍。设数组b,b[i]
表示长度为i的木棍的数量。
先输入所有木棍,将相关信息保存在b数组中,同时记录最长木棍长度maxLen和最短木棍长度minLen,木棍总长度totLen。
所有小木棍是由若干原木棍切分而成的,那么木棍总长度,一定是原木棍长度的倍数。反过来,原木棍长度,一定是木棍总长度的约数。同时:原木棍的长度必须大于等于最长木棍的长度maxLen。
从小到大取木棍总长度totLen的约数l,看原木棍长度为l时,能否让所有小木棍拼成totLen/l个长为l的原木棍。如果成功拼成,则找到了可行的原木棍的最小长度。
深搜时需要记录的状态有:这一次要选择的木棍长度st,当前拼出的木棍长度len,剩余要拼的原木棍数量stickNum,要拼成的原木棍长度stickLen。
在拼一根原木棍的过程中
- 应该先选较长的小木棍,再选较短的小木棍,这样可以较快得到拼成木棍的方案。
- 每选择一根长为i的木棍,下一次要选择的木棍长度还为i,当前拼出木棍长度为len+1。如果长为i的木棍已经选过了,或不存在,则尝试选择下一个长度更短的木棍。
- 如果当前木棍长度len已经为原木棍长度stickLen,则拼好了一根木棍,接下来拼下一根木棍,从最长的木棍开始选择木棍,将st设为maxLen,当前拼出木棍长度len设为0,剩余要拼木棍数量stickNum减少1。
- 如果剩余要拼木棍数量stickNum变为了0,则使用小木棍成功拼出了若干个长为stickLen的原木棍,原木棍长度的最小值就是stickLen。
可以进行的剪枝有:
-
已经提到的:先选较长的小木棍,再选较短的小木棍(优化搜索顺序)
-
如果当前木棍长度len已经比原木棍长度stickLen更长,就结束搜索。(可行性剪枝)
-
如果已经得到可行的方案,即最小原木棍长度ans已经有值,后面即便得到可行方案,原木棍长度stickLen也会大于等于ans,因此就不用再搜索了。(最优性剪枝)
-
假设当前拼好的长为len的木棍再拼接长为i的木棍就能拼成原木棍,即
len+i == stickLen
,将这种情况搜索完毕后,就不需要再进行搜索了。
原因如下:接下来如果不用长为i的木棍,而是再向后搜索长度更短的木棍,那么接下来做的事情就是找长度加和为i的几根木棍。-
如果后面不存在长度加和为i的几根木棍,则无法拼成这根原木棍,则当前的拼接过程应该结束,应该返回上一层,改变上一根选择的木棍。
-
如果接下来还存在长度加和为i的几根木棍,对比两种情况。
- 情况1:是已经搜索过的情况,将长为i的木棍和长为len的拼好的木棍进行拼接,得到一根原木棍。留下几根长度加和为i的木棍。
- 情况2:是将几根长度加和为i的木棍和长为len的拼好的木棍进行拼接,得到一根原木棍。留下长为i的木棍。
如果是情况1,留下几根长度加和为i的木棍,这些木棍可以合起来,当做一个长为i的木棍,此时这几根长度加和为i的木棍的作用和一根长度为i的木棍的作用相同。这几根木棍也可以拆开分别使用,会比留下长为i的木棍更加灵活。因此情况1接下来的搜索过程一定已经涵盖了情况2接下来的搜索过程,接下来没有再进行搜索的必要了。(排除等效冗余)
-
-
如果当前刚开始要要拼一根原木棍,即已经拼成的木棍长度
len==0
。在确定使用的一根长为i的木棍后进行搜索,直至这一趟搜索结束。在这一趟搜索中:- 如果已经成功得到将小木棍拼成若干长为stickLen的原木棍的方案,那么已经找到解,自然不用再进行搜索了。
- 如果没有得到可行的方案,接下来在拼当前原木棍的过程中,第一根木棍就该尝试使用长为i+1的木棍。而长为i的木棍还是存在的,它即便不参与拼接当前的原木棍,也一定会参与拼接下一根原木棍。
而刚才的搜索过程中,对于长为i的木棍参与拼接原木棍的所有情况都已搜索完成,也没有找到一种可行的拼接方案。也就是说,在当前情况下,使用长为i的木棍的所有方案都是失败的,因此不用继续进行搜索了,应该回到上一层,改变上一根木棍的使用情况。(排除等效冗余)
【题解代码】
解法1:深搜剪枝
#include <bits/stdc++.h>
using namespace std;
#define N 70
int b[N];//b[i]:长度为i的木棍个数(桶)
int n, totLen, minLen = 70, maxLen, ans;
//st:遍历起始数字 len:当已前凑的木棍长度 stickNum:剩余需要凑的原木棍数量 stickLen:原木棍长度
void dfs(int st, int len, int stickNum, int stickLen)
{
if(ans > 0)//如果已经求出结果,则不再搜索
return;
if(stickNum == 0)//如果已经拼成指定数量的原木棍,则找到一种拼接方案
{
ans = stickLen;//记录最短原木棍长度,结束整个搜索过程
return;
}
if(len == stickLen)//如果已经拼成一根原木棍
{
dfs(maxLen, 0, stickNum-1, stickLen);//开始拼下一根原木棍,初始长度len为0,需要拼的木棍数量减1
return;
}
if(len > stickLen)//剪枝:如果当前木棍长度len已经比原木棍长度stickLen更长,就不再搜索
return;
for(int i = st; i >= minLen; --i) if(b[i] > 0)//木棍长度i,从大到小遍历
{
b[i]--;//使用了一根长为i的木棍
dfs(i, len+i, stickNum, stickLen);//刚才使用了长为i的木棍,下面还是从长为i的木棍看起
b[i]++;//状态还原
if(len == 0 || len+i == stickLen)//解题思路中剪枝第4、第5点。
return;
}
}
int main()
{
int a;
cin >> n;
for(int i = 1; i <= n; ++i)
{
cin >> a;
if(a <= 50)//ybt 1442:【例题3】小木棍 不保证每个输入都 <=50,所以需要将>50的数据过滤掉
{
b[a]++;//b[i]:长为i的木棍的数量
totLen += a;//木棍总长
maxLen = max(maxLen, a);//最长木棍长度
minLen = min(minLen, a);//最短木棍长度
}
}
for(int l = maxLen; l <= totLen; ++l) if(totLen%l == 0)//l:原木棍长度 必须是总长度的约数
{
dfs(maxLen, 0, totLen/l, l);
if(ans > 0)//找到解,就结束
break;
}
cout << ans;
return 0;
}