P1120 小木棍 [数据加强版]
原题链接 https://www.luogu.org/problemnew/show/P1120
不得不说,这题真的是一道深度搜索毒瘤题qwq,整整坑了我一个上午。
先说一下大体思路:
1.读入数据的同时(注意过滤掉长度大于50的木棍),算出所有小木棍的总和sum,因为至少所有的小木棍会拼成一根长度为sum的超大木棍;
2.找出所有小木棍中长度最大的那根max,原始长度len一定大于等于这个max,所以我们从max开始搜索,若找到一个符合条件的原始长度len就立刻返回,此时这个len一定是最小值;
考虑一下怎么搜:我们设search(int k,int last,int rest) 表示当前正在拼接第k根木棍,上一根用到的小木棍是last,当前还有rest没拼接完;
当然若这个题不加任何剪枝技巧进行深度优先搜索的话,时间效率是指数级别的,效率非常低,程序将严重超时。对于此题我们可以从可行性和最优性上加以剪枝:
最优性剪枝:
1.所以木棍的总长度为sum,那么原始长度len一定能够被sum整除,即len | sum ,因为你要拼出来整数根木棍,不可能拼出来小数根木棍;
2.木棍的原始长度一定大于等于所有小木棍中最长的那根;
可行性剪枝:
3.一根长木棍肯定比几根短木棍拼成同样长度的用处小,即短的木棍可以更灵活组合,所以对输入的所有木棍按长度从大到小排序,从长到短地将木棍拼入,这样短木棍可以更加灵活,拼成原始长度len的成功率更高;
4.在截断后的排好序的木棍中,当用第i根木棍拼接时,可以从i+1后的木棍开始搜。因为根据优化3,你总是先用长度更大的木棍,所以前i个木棍已经用过了;
5.当dfs返回拼接失败,需要更换当前使用的木棍时,不要再用与当前木棍的长度相同的木棍,因为当前木棍用了不行,改成与它相同长度的木棍一样不行。这里我开了个数组nxt[i]表示与第 i 根长度相同的所有木棍中最后一根,那么nxt[i]的下一根就是和i长度不相同的木棍;这个预处理可以优化时间,不必在循环中慢慢往下找长度不相等的木棍;
6.我们拼接木棍的时候只找长度小于等于剩余长度rest的木棍(长了接不上啊),所以我们可以二分查找到第一根长度小于等于rest的木棍,那么它后面的木棍都是小于rest的木棍;
7.当我们搜到了最后一根木棍的时候,我们直接返回,因为剩下的一定能拼成原式长度len;
证明: 所有木棍的长度总和为sum,当前枚举的原式长度为len,那么能拼成m=sum/len 根木棍;若当前正在拼第m根木棍的话,那么说明前面的(m-1)根木棍已经拼好了,用了len*(m-1)的长度,那么剩下的长度就是:sum-len*(m-1)=sum-(len*m-len)=sum-sum+len=len,说明剩下的所有木棍的总和就是len,正好就是枚举的原式长度len,那么我们就不用搜了,直接返回就好了;
8.用vis数组标记每根木棍是否用过。另外在dfs回溯的时候别忘了去掉这些标记,这样就不用每次dfs之前memset了(memset用多的话速度可慢了)!
9.我们其实只需枚举到sum/2就行了,如果还拼不成功的话,那么答案只能是sum了;
10.还有一个挺重要的剪枝,但是容易忽略:
当我们剩余长度rest等于当前拼接的木棍的长度时,若拼接失败了,那么直接返回改用之前的木棍;
这一点很难想也很难理解,当时我也特别的懵啊,现在好歹明白了,现在就给你们解释一波吧(解释得不是很到位,可能你们也挺不懂qwq):
当前长度rest等于正在拼的这个小木棍的长度,所以是不是我们只要把这个小木棍接上去就又拼完了一根木棍?但是我们去拼其他的木棍的时候却拼接失败了,如果我们不返回的话,继续换木棍往下拼,一定是要用几根和为rest的小木棍一块拼才能把那个rest的空补上,那么你原来那个剩下的长度为rest的木棍就搁在那儿了,你想想,前面说过短木棍比长木棍更灵活更好拼,那么你刚刚的操作就相当于,用了好几根短木棍去换下来了一根长木棍,但是那些短木棍都拼不成功,你这一根长木棍不就更拼不成功了吗?
有了这么多剪枝,代码跑起来就快多了,下面就是AC代码啦:
#include<iostream> #include<cstdio> #include<cmath> #include<cstring> #include<algorithm> using namespace std; int read() //读入优化 { char ch=getchar(); int a=0,x=1; while(ch<'0'||ch>'9') { if(ch=='-') x=-x; ch=getchar(); } while(ch>='0'&&ch<='9') { a=(a<<3)+(a<<1)+(ch-'0'); ch=getchar(); } return a*x; } int cmp(int x,int y) //将小木棍长度从大到小排序 { return x>y; } int n,sum,minn,m,len,flag,a[70],vis[70],nxt[70],x,cnt; //a数组是合法的每根小木棍的长度 //vis数组是看看每根小木棍是否已经用过 //nxt[i]数组是看看和第i根小木棍出长度相同的所有木棍中的最后一根 void search(int k,int last,int rest) //正在拼第k根大木棍,上一根用的小木棍是last,还剩rest { int i,j; if(k==m) {flag=1;return ;} //剪枝7,如果当前正在拼最后一根,那么肯定能拼成,就直接返回吧 if(rest==0) //拼完了第k根,换下一根 { for(i=1;i<=cnt;i++) //剪枝3,尽量让更长的小木棍打头 if(!vis[i]) //如果小木棍没用过就试一下 { vis[i]=1; //用过这根木棍了 search(k+1,i,len-a[i]); //搜索第k+1根,上一根用的小木棍是i,还剩len-a[i] vis[i]=0; //剪枝8,回溯操作 return ; } } int l=last+1,r=cnt; while(l<r) //剪枝6,二分找第一根小于等于rest的木棍 { int mid=(l+r)>>1; if(a[mid]>rest) l=mid+1; else r=mid; } for(i=l;i<=cnt;i++) //剪枝4,后面的小木棍都比rest小 { if(!vis[i]&&rest>=a[i]) { vis[i]=1; search(k,i,rest-a[i]); if(flag) return ; //如果能拼成就返回 vis[i]=0; //剪枝8,回溯操作 if(rest==a[i]) return ; //剪枝10,如果rest和当前的小木棍的长度相同,那就说明后面都拼不成了,直接返回 i=nxt[i]; //剪枝5,跳到和当前木棍长度相同的所有木棍中的最后一根,这样就不用再重复的搜索长度相同的木棍了 } } return ; } int main() { n=read(); for(int i=1;i<=n;i++) { x=read(); if(x>50) continue; //注意过滤掉长度大于50的小木棍 a[++cnt]=x; //cnt是实际剩下来的小木棍的数量 sum+=x; //求出所有长度小于50的小木棍的总和 } sort(a+1,a+1+cnt,cmp); //剪枝3,让小木棍从大到小排序 nxt[cnt]=cnt; //最后一根小木棍的nxt只能是自己,后面没有小木棍了 for(int i=cnt-1;i>0;i--) { if(a[i]==a[i+1]) nxt[i]=nxt[i+1]; //如果和后面的那根的长度相同,那么nxt值相同 else nxt[i]=i; //如果后面没有长度和它相同的木棍,那么nxt就是自己 } vis[1]=1; for(int i=a[1];i<=sum/2;i++) //剪枝9,枚举可能的每根长度 { if(sum%i==0) //剪枝1 { m=sum/i; //有m根木棍 len=i; search(1,1,len-a[1]); if(flag) { printf("%d",len);return 0; } } } if(!flag) cout<<sum; return 0; }
好一道剪枝神搜题啊qwq!累死我了!