小木棍(爆搜減枝)
題目描述
喬治有一些同樣長的小木棍,他把這些木棍隨意砍成幾段,直到每段的長都不超過5050。
現在,他想把小木棍拼接成原來的樣子,但是卻忘記了自己開始時有多少根木棍和它們的長度。
給出每段小木棍的長度,編程幫他找出原始木棍的最小可能長度。
輸入輸出格式
輸入格式:
共二行。
第一行為一個單獨的整數N表示砍過以後的小木棍的總數,其中N≤65N≤65
(管理員註:要把超過5050的長度自覺過濾掉,坑了很多人了!)
第二行為NN個用空個隔開的正整數,表示NN根小木棍的長度。
輸出格式:
一個數,表示要求的原始木棍的最小可能長度
輸入輸出樣例
輸入樣例#1: 復制9 5 2 1 5 2 1 5 2 1
6
此題明顯是一道搜索剪枝題。此題難度應該不到提高+,但在考場上寫這道題會吃力一些,因為不好調。我詳細說一下這題的全思路,不過略長。
前排提示:第四條的優化7講的是那個不少人不明白的優化,如果你只是不明白那個優化可以空降。
一,管理員已經在題目中告訴你輸入時去掉長度大於50的木棍。
二,想好搜索什麽。很明顯我們要枚舉把哪些棍子拼接成原來的長棍,而原始長度(原來的長棍的長度)都相等,因此我們可以在dfs外圍枚舉拼接後的每根長棍的長度。那枚舉什麽範圍呢?
其長度至少是最長的一根木棍,此時最長的這根木棍恰好單獨組成原來的長棍。如果 原始長度 小於 最長的這根木棍,那麽這根最長的木棍就無法自己或與其它木棍組成原來的長棍。
其長度至多是所有木棍的長度之和,此時所有的木棍拼在一起恰好成為一根原來的長棍。如果 原始長度 大於所有木棍的長度之和,那麽即使所有木棍拼在一起也組不夠原來的長棍了。
這麽大的循環套dfs會超時麽?當然會了。所以我們可以考慮到當 原始長度 不能被 所有木棍的長度之和 整除的話,這些木棍是拼不出整數根的(如果都拼成枚舉的原來長棍的長度)。因此在循環時把它們刷掉。
這裏借鑒了dalao的(小)優化,即原始長度枚舉到 所有木棍的長度之和/2 即可,因為此時所有木棍有可能拼成2根木棍,原始長度再大的話就只能是所有木棍拼成1根了。
三,腦補一下怎麽搜。設dfs(int k,int last,int rest),k表示正在拼第幾根原來的長棍,last表示使用的上一根木棍(輸入的短棍)的編號,rest表示當前在拼的長棍還有多少長度未拼。於是循環枚舉下一根將要使用的木棍。
四,上面的做法不超時說明你太強大了。你開始思考對程序做一些優化。(下面的優化請按順序想)
1.一根長木棍肯定比幾根短木棍拼成同樣長度的用處小,即短的木棍可以更靈活組合,所以對輸入的所有木棍按長度從大到小排序,從長到短地將木棍拼入,這樣短木棍可以更加靈活地接在。
如果你還不太清楚“靈活”的含義,請形象腦補一下——如果先用短木棍,那麽需要很多根連續的短木棍接上一根長木棍才能拼成一根原來的長棍,那麽短木棍都用了後,剩下了大量長木棍,拼起來就不如短木棍靈活,更難接出原始長度。而先用長木棍,最後再用短木棍補刀,這樣就剩下了相對較短的木棍,能更加靈活地拼接出原始長度。
2.根據優化1,將輸入的木棍從大到小排好序後,當用木棍i拼合原始長棍時,從第i+1根木棍開始往後搜。
3.當dfs返回拼接失敗,需要更換當前使用的木棍時,不要再用與當前木棍的長度相同的木棍,因為當前木棍用了不行,改成與它相同長度的木棍一樣不行。這裏我預處理出了排序後每根木棍後面的最後一根與這根木棍長度相等的木棍(程序中的next數組),它的下一根木棍就是第一根長度不相等的木棍了。
這個預處理可以優化時間,不必在循環中慢慢往下找長度不相等的木棍。
4.只找木棍長度不大於未拼長度rest的所有木棍。我看其他大部分人的做法(包括書上的啊)都是直接在循環中判斷,但我認為可以根據木棍長度的單調性來二分找出第一個木棍長度不大於未拼長度rest。它後面的木棍一定都滿足這個條件。
5.用vis數組標記每根木棍是否用過。另外在dfs回溯的時候別忘了去掉這些標記,這樣就不用每次dfs之前memset了(memset用多的話速度可TM慢了)!
優化5的習慣可以沿用到各種競賽
6.由於是從小到大枚舉 原始長度,因此第一次發現的答案就是最小長度。dfs中只要發現所有的木棍都湊成了若幹根原長度的長棍(容易發現 湊出長棍的根數=所有木棍的長度之和/原始長度),立刻一層層退出dfs,不用滯留,退到dfs外後直接輸出原始長度並結束程序。
7.還有一個難想卻特別特別重要的優化:如果當前長棍剩余的未拼長度等於當前木棍的長度或原始長度,繼續拼下去時卻失敗了,就直接回溯並改之前拼的木棍。有些人不太明白這個優化,這裏簡單說一下:
當前長棍剩余的未拼長度等於當前木棍的長度時,當前木棍明顯只能自組一根長棍,但繼續拼下去卻失敗,說明這根木棍不能自組?!這根木棍不自組就沒法用上了,所以不用搜更短的木棍了,直接回溯,改之前的木棍;
當前長棍剩余的未拼長度等於原始長度時,說明這根原來的長棍還一點沒拼,現在正在放入一根木棍。很明顯,這根木棍還沒有跟其它棍子拼接,如果現在拼下去能成功話,它肯定是能用上的,即自組或與其它還沒用的木棍拼接。但繼續拼下去卻失敗,說明現在這根木棍不能用上,無法完成拼接,所以直接回溯,改之前的木棍。
做了這麽多優化可以確保飛跑了……搜索題啊,每招優化都要學,學一招說不定競賽的時候就能跑的快一點。
代碼:
#include<iostream> #include<cstdio> #include<cmath> #include<cstring> #include<algorithm> using namespace std; bool cmp(int x,int y) { return x>y; } int n,t,sum=0,cnt=0,a[70],res,vis[70],pp; int dfs(int len,int sta,int now)// dfs(ê£??μ?3¤?è£?μú??????1÷?aê?£?ò??-°úo?á???×é) { if(now==res) return 1; if(len==0) if(dfs(pp,1,now+1))// μ±?°?aò??ùò??-°úíê ê£??μ?3¤?èó|????D??aê? return 1; for(int j=sta;j<=cnt;j++) if(!vis[j]&&a[j]<=len) { vis[j]=1; // ±£?¤μ??′±?ó?1y if(dfs(len-a[j],j+1,now)) // ????ò???μ? return 1; vis[j]=0; if(len==a[j]||len==pp)// ê×?è£o?ü??DDμ??a±í?÷×?′óμ?a[i]ò22??ü?ú×?ì??t è?oó£oè?1?len==pp ?′ò?×éD?μ???1÷ ???′ò?oóμ?ò22??ü?ú×?ì??t break; // ó?á?μ±?°??1÷oó?T·¨?′o? μ?ê?ê£??μ???1÷3¤?è?1μèóúμ±?°??1÷3¤?è ì?3? while(a[j+1]==a[j]) // μ±?°3¤?è2?DD ????3¤?èò22?DD j++; } return 0; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d",&t); if(t<=50) { sum+=t; a[++cnt]=t; } } sort(a+1,a+1+cnt,cmp); for(int i=a[1];i<=sum;i++) if(sum%i==0) { pp=i; // è???????3¤?è res=sum/i; if(dfs(i,1,0)) { printf("%d",i); return 0; } } return 0; }
謝謝大家!
參考https://www.luogu.org/blog/complexity/solution-p1120
小木棍(爆搜減枝)