题意:乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过50个长度单位。然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。请你设计一个程序,帮助乔治计算木棒的可能最小长度。每一节木棍的长度都用大于零的整数表示。(2362是1011的特例,问一堆木棍能否拼成正方形)
思路:dfs+剪枝。其中剪枝具有相当的技巧性,其中一个地方的剪枝没有想到导致tle多次。
几个明显的剪枝点(设ans为最终的答案):
1、ans>=这堆木棍的最大长度
2、ans<=这堆木棍长度的总和
3、ans必可以被这堆木棍长度总和整除
4、对木棍由长到短排序,先考虑长的能有效剪枝
两个不明显但是最重要的剪枝点,缺一不可:
5、如果某次刚刚加入长度为x的木棍而随后搜索失败,则接下来的搜索只需搜比x小得木棍即可(代码中pre变量的作用)。
6、在每次构建新的长度为ans的木棍时,检查新棒的第一根s[i],若在搜索完所有木棍后都无法组合,则说明s[i]无法在当前组合方式下组合,不用往下搜索(因为再怎么搜索s[i]都不会加入到任何一个木棍当中)。代码中if(!from) return 0;这句话的作用。
1011:
#include <stdio.h> #include <string.h> #include <stdlib.h> #define max(a,b) ((a)>(b)?(a):(b)) #define N 65 int s[N],f[3000],n,used[3005],res[6][100]; int sum,len,stick,perlen; int cmp(const void* a,const void* b){ return (*(int*)b) - (*(int*)a); } void factor(int b,int n){ int i; for(i = b,len=0;i<=n;i++) if(n%i==0) f[++len] = i; } int dfs(int now,int from,int num){//当前木棍已经拼好的长度/从第几个木棍开始搜/已经拼好的木棍数量 int i,pre; if(num == stick) return 1; if(now == perlen) return dfs(0,0,num+1); for(i = from,pre=0;i<n;i++) if(!used[i] && s[i]!=pre && now+s[i]<=perlen){ used[i] = 1; if(dfs(now+s[i],i+1,num)) return 1; used[i] = 0; pre = s[i]; //剪枝5,最重要的剪枝之一 if(!from) //剪枝6,最重要的剪枝之二 return 0; } return 0; } int main(){ while(scanf("%d",&n)&&n){ int i; sum = 0; for(i = 0;i<n;i++){ scanf("%d",&s[i]); sum += s[i]; } qsort(s,n,sizeof(int),cmp); //满足剪枝4 factor(s[0],sum); //筛出因数,也即满足剪枝1~3 for(i = 1;i<len;i++){ memset(used,0,sizeof(used)); stick = sum/f[i]; //当前搜索下得棍子数量 perlen = f[i]; //当前搜索下的每根棍子长度 if(dfs(0,0,0)) break; } printf("%d\n",f[i]); } return 0; }
2362:
#include <stdio.h> #include <string.h> #include <stdlib.h> int s[25],used[25]; int T,n,sum; int cmp(const void* a,const void* b){ return (*(int*)b)-(*(int*)a); } int dfs(int now,int from,int num){ int i,pre; if(num == 4) return 1; if(now == sum/4) return dfs(0,0,num+1); for(i = from,pre=0;i<n;i++) if(!used[i] && s[i]!=pre && now+s[i]<=sum/4){ used[i] = 1; pre = s[i]; if(dfs(now+s[i],i+1,num)) return 1; used[i] = 0; if(!from) return 0; } return 0; } int main(){ scanf("%d",&T); while(T--){ int i; memset(used,0,sizeof(used)); scanf("%d",&n); for(i = sum = 0;i<n;i++){ scanf("%d",&s[i]); sum += s[i]; } qsort(s,n,sizeof(int),cmp); if(sum%4==0 && dfs(0,0,0)) printf("yes\n"); else printf("no\n"); } return 0; }
另一种写法(北大郭炜老师课后写):
#include <cstdio> #include <string> #include <vector> #include <iostream> #include <cstdlib> #include <cmath> #include <algorithm> using namespace std; int n,sum; int s[66],len,used[66]; int cmp(int a,int b){ return a>b; } int dfs(int left,int now,int from){//left:还剩下多少木棍待拼;now:当前木棍还剩下多少待拼;from:从哪个下标开始选择 int i,last; if(left == 0) return 1; if(now == 0) return dfs(left-1,len,1); for(i = from,last = -1;i<=n;i++) if(!used[i] && s[i]!=last && s[i] <= now){//last剪枝,表示相同长度的碎片就不用重复搜索了 used[i] = 1; if(dfs(left, now-s[i], i+1)) return 1; used[i] = 0; last = s[i]; if(now == len || now==s[i])//重要剪枝,木棍的第一个碎片拼接失败,则不用继续搜索了,因为说明这个碎片在后面的拼接中也不可能成立;s[i]作为最后一个碎片失败的话也不用继续搜了,因为如果s[i]出现在之后的木棍中,那么把当前组成s[i]的这些和后面的s[i]对换依然成立,与s[i]在现在拼法中不成立矛盾 return 0; } return 0; } int main(){ while(scanf("%d",&n) && n){ int i; for(i = 1,sum=0;i<=n;i++){ scanf("%d",&s[i]); sum += s[i]; } sort(s+1,s+1+n,cmp);//对木棍长度排序,保证先找长的(从选择少的开始找) for(len = s[1];len<=sum/2;len++){//木棍长度的下界是最长的一个片段,而且木棍长度必然为总长度的约数 if(sum % len) continue; memset(used,0,sizeof(used)); if(dfs(sum/len,len,1)) break; } if(len>sum/2) printf("%d\n",sum); else printf("%d\n",len); } return 0; }