洛谷P 1120 小木棍
题目大意:给出一堆一已知长度的木棍,它们是由一些木棍随意切割而来,之前的木棍长度一样,问之前木棍的最短长度。
这道题目方向比较明显,就是遍历所有可能的数值,进行 dfs 搜索,但是这个问题中有很多可以优化的细节,比如遍历的数值,这个一般从0到木棍的长度之和,但是这里面有许多是达不到的,所以只用遍历所有的因数即可。在对木棍进行深度搜索时,同时要注意运算顺序,从长木棍到短木棍的运算速度要比乱序快,因为短的木棍更加灵活,这个是在洛谷的题解上看到的,虽然原理很简单,但是被我忽视了,短的木棍如果都被拼完了,长的木棍肯定很难组合成长度正合适的木棍,所以要先从长的开始,这个时候搜索的起点就是当前木棍的位置的下一个,这样也可以减少相当的运算量。还有一点是对于长度重复的木棍的省略,因为如果这一个木棍不成功,那么接下来和它长度相同的木棍也就不用再进行深度搜索了,注意的是,这一点说的是在他之后的木棍,可能存在当前木棍成立,下一个木棍所需的短棍就用光的情况。最后一个也是我觉得最厉害的一个,这个是我确实没有考虑过的问题,就是可以用一个数组来标记用过的木棍,dfs回溯的时候再把标记删除,这样可以不用再使用 memset 函数,这点在dfs中可以减去很多不必要的运算。(看了很多题解,这些都被称之为剪枝)
#include<bits/stdc++.h>
using namespace std;
int n,m,a[66],next[66],cnt,sum,len;
bool used[66],ok; //used数组即优化5的vis数组,记录每根木棍是否用过;ok记录是否已找到答案。
bool cmp(int a,int b){return a>b;}
void dfs(int k,int last,int rest){ //k为正在拼的木棍的编号,last为正在拼的木棍的前一节编号,rest为该木棍还未拼的长度
int i;
if(!rest){ //未拼的长度为0,说明这根原始长棍拼完了,准备拼下一个
if(k==m){ok=1; return;} //优化6,全部拼完并符合要求,找到答案,直接返回
for(i=1;i<=cnt;i++) //找一个还没用的最长的木棍打头即可。反正要想全都拼接成功,每根木棍都得用上
if(!used[i]) break;
used[i]=1;
dfs(k+1,i,len-a[i]);
used[i]=0;
if(ok) return; //优化6,找到答案就退出
}
//优化4,二分找第一个 木棍长度不大于未拼长度rest 的位置
int l=last+1, r=cnt, mid;
while(l<r){
mid=(l+r)>>1;
if(a[mid]<=rest) r=mid;
else l=mid+1;
}
for(i=l;i<=cnt;i++){
if(!used[i]){ //优化5,判断木棍是否用过
used[i]=1;
dfs(k,i,rest-a[i]);
used[i]=0;
if(ok) return; //优化6,找到答案就退出
if(rest==a[i] || rest==len) return; //优化7
i=next[i]; //优化3
if(i==cnt) return;
}
}
//到了这里,说明这时候拼不成当前这根原始木棍了,传回失败信息并修改之前拼的木棍
}
int main(){
cin>>n;
int d;
for(int i=1;i<=n;i++){
cin>>d;
if(d>50) continue;
a[++cnt]=d;
sum+=d;
}
sort(a+1,a+cnt+1,cmp); //优化1,木棍按长度从大到小排序
//优化3,预处理next数组
next[cnt]=cnt;
for(int i=cnt-1;i>0;i--){
if(a[i]==a[i+1]) next[i]=next[i+1];
else next[i]=i;
}
for(len=a[1];len<=sum/2;len++){ //枚举原始长度
if(sum%len!=0) continue; //如果不能拼出整数根 就跳过
m=sum/len; //优化6中的那个计算
ok=0;
used[1]=1;
dfs(1,1,len-a[1]);
used[1]=0;
if(ok){printf("%d\n",len); return 0;} //优化6,输出答案,退
}
printf("%d\n",sum); return 0;
}
这个是在洛谷看到的一个题解,但是他应用了二分来找下一根合适的的木棍,而且遍历的范围是按从零到总长的一半来的。