剪枝,顧名思義,就是通過一些判斷,砍掉搜索樹上不必要的子樹。有時候,我們會發現某個結點對應的子樹的狀態都不是我們要的結果,那么我們其實沒必要對這個分支進行搜索,砍掉這個子樹,就是剪枝。
可行性剪枝
給定n個整數,要求選出K個數,使得選出來的K個數的和為sum。
在搜索時,如果已經選了k個數,再往后選多的數是沒有意義的。所以我們可以直接減去這個搜索分支。
又比如,如果所有的數都是正數,如果一旦發現當前的和值都已經大於sum了,那么之后不管怎么選和值都不可能回到sum了,我們也可以直接終止這個分支的搜索。
我們在搜索過程中,一旦發現如果某些狀態無論如何都不能找到最終的解,就可以將其“剪枝”了。
最優性剪枝
對於求最優解的一類問題,通常可以用最優性剪枝,比如在求解迷宮最短路的時候,如果發現當前的步數已經超過了當前最優解,那從當前狀態開始的搜索都是多余的,因為這樣搜索下去永遠都搜不到更優的解。通過這樣的剪枝,可以省去大量冗余的計算。此外,在搜索是否有可行解的過程中,一旦找到了一組可行解,后面所有的搜索都不必再進行了,這算是最優性剪枝的一個特例。
重復性剪枝
對於某一些特定的搜索方式,一個方案可能會被搜索很多次,這樣是沒必要的。
再來看這個問題:給定n個整數,要求選出K個數,使得選出來的K個數的和為sum。
如果搜索方法是每次從剩下的數里選一個數,一共搜到第k層,那么1,2,3這個選取方法能被搜索到6次,這是沒必要的,因為我們只關注選出來的數的和,而根本不會關注選出來的數的順序,所以這里可以用重復性剪枝。
我們規定選出來的數的位置是遞增的,在搜索的時候,用一個參數來記錄上一次選取的數的位置,那么此次選擇我們從這個數之后開始選取,這樣最后選出來的方案就不會重復了。
當然,搜索的效率也要比直接二進制枚舉更高。
void dfs(int s, int cnt, int pos) { ... ... for (int i = pos; i <= n; i++) { if (!xuan[i]) { xuan[i] = true; dfs(s + a[i], cnt + 1, i + 1); // i + 1 表示從上一次選取的位置后面開始選 xuan[i] = false; } } }
從1,2,3,4……30這30個數中選取8個數使其和為200
1 #include <iostream> 2 using namespace std; 3 int n, k, sum, ans; 4 int a[40]; 5 bool xuan[40]; 6 void dfs(int s, int cnt, int pos)//多加一個參數,進行重復性剪枝 7 { 8 if (s > sum || cnt > k) return;//可行性剪枝 9 10 if (s == sum && cnt == k) ans++; 11 for (int i = pos; i < n; i++) 12 { 13 if (!xuan[i]) 14 { 15 xuan[i] = 1; 16 dfs(s + a[i], cnt + 1, i + 1); 17 xuan[i] = 0; 18 } 19 } 20 } 21 int main() 22 { 23 n = 30; 24 k = 8; 25 sum = 200; 26 for (int i = 0; i < 30; i++) 27 a[i] = i + 1; 28 ans = 0; 29 dfs(0, 0,0); 30 cout << ans << endl; 31 return 0; 32 }
奇偶性剪枝
我們先來看一道題目:有一個n×m大小的迷宮。其中字符S表示起點,字符D表示出口,字符X表示牆壁,字符.表示平地。你需要從S出發走到D,每次只能向上下左右相鄰的位置移動,並且不能走出地圖,也不能走進牆壁。每次移動消耗1時間,走過路都會塌陷,因此不能走回頭路或者原地不動。現在已知出口的大門會在T時間打開,判斷在0時間從起點出發能否逃離迷宮。數據范圍n,m≤10,T≤50。
我們只需要用DFS來搜索每條路線,並且只需搜到T時間就可以了(這是一個可行性剪枝)。但是僅僅這樣也無法通過本題,還需考慮更多的剪枝。
如上圖所示,將n×m的網格染成黑白兩色。我們記每個格子的行數和列數之和x,如果x為偶數,那么格子就是白色,反之奇數時為黑色。容易發現相鄰的兩個格子的顏色肯定不一樣,也就是說每走一步顏色都會不一樣。更普遍的結論是:走奇數步會改變顏色,走偶數步顏色不變。
那么如果起點和終點的顏色一樣,而T是奇數的話,就不可能逃離迷宮。同理,如果起點和終點的顏色不一樣,而T是偶數的話,也不能逃離迷宮。遇到這兩種情況時,就不用進行DFS了,直接輸出"NO"。
這樣的剪枝就是奇偶性剪枝,本質上也屬於可行性剪枝。
剪枝條件:(sx + sy + ex + ey + T) % 2 != 0
例題
正方形
輸入樣例1
4 1 1 1 1
輸出樣例1
Yes
輸入樣例2
5 10 20 30 40 50
輸出樣例2
No
要一條邊一條邊地搜索,三條邊一起搜索會超時,只需要搜索出前三條邊即可。
對於正方形的每一條邊,我們能事先計算出長度。
一條邊一條邊的進行搜索,當搜索到一條邊滿足長度要求的時候,重新從剩下的木棍中再搜索出一條邊,直到搜索出四條邊。
像三角形那樣同時搜索4條邊會超時的。記得需要用重復性剪枝。
ps:三角形那題三條邊一起搜的寫法

1 #include <iostream> 2 #include <cstdio> 3 using namespace std; 4 int l[30]; 5 int sum, n; 6 bool ok; 7 void dfs(int id, int l1, int l2, int l3) { 8 if (l1 > sum || l2 > sum || l3 > sum) { 9 return; 10 } 11 if (ok) { 12 return; 13 } 14 if (id == n) { 15 if (l1 == sum && l2 == sum && l3 == sum) { 16 ok = 1; 17 } 18 return; 19 } 20 dfs(id + 1, l1 + l[id], l2, l3); 21 dfs(id + 1, l1, l2 + l[id], l3); 22 dfs(id + 1, l1, l2, l3 + l[id]); 23 } 24 25 int main() { 26 freopen("triangle.in", "r", stdin); 27 freopen("triangle.out", "w", stdout); 28 int ca; 29 cin >> n; 30 sum = 0; 31 for (int i = 0; i < n; ++i) { 32 cin >> l[i]; 33 sum += l[i]; 34 } 35 if (sum % 3) { 36 cout << "no" << endl; 37 return 0; 38 } 39 ok = 0; 40 sum /= 3; 41 dfs(0, 0, 0, 0); 42 if (ok) { 43 cout << "yes" << endl; 44 } else { 45 cout << "no" << endl; 46 } 47 return 0; 48 }
另外可以提前判斷一下,如果所有木棍的和不能被4整除,那么肯定不可能。
1 #include <stdio.h> 2 #include <string.h> 3 #include <iostream> 4 #include <string> 5 #include <math.h> 6 #include <algorithm> 7 #include <vector> 8 #include <stack> 9 #include <queue> 10 #include <set> 11 #include <map> 12 #include <sstream> 13 const int INF=0x3f3f3f3f; 14 typedef long long LL; 15 const int mod=1e9+7; 16 const double PI = acos(-1); 17 const double eps =1e-8; 18 #define Bug cout<<"---------------------"<<endl 19 const int maxn=1e5+10; 20 using namespace std; 21 22 int n,flag; 23 int a[30];//存數據 24 int vis[30];//判斷每條邊訪問過沒 25 int le[4];//每條邊的邊長 26 int L;//標准邊長 27 28 void DFS(int num,int pos) 29 { 30 if(num>3)//遞歸出口,已經搜索到了三條邊 31 { 32 flag=1; 33 return ; 34 } 35 if(flag) return ;//最優性剪枝 36 if(le[num]>L) return ;//可行性剪枝 37 if(le[num]==L) DFS(num+1,1);//找到了一條邊 38 else 39 { 40 for(int i=pos;i<=n;i++) 41 { 42 if(vis[i]==0) 43 { 44 vis[i]=1; 45 le[num]+=a[i]; 46 DFS(num,i+1); 47 le[num]-=a[i]; 48 vis[i]=0; 49 } 50 } 51 } 52 } 53 54 int main() 55 { 56 #ifdef DEBUG 57 freopen("sample.txt","r",stdin); 58 #endif 59 ios_base::sync_with_stdio(false); 60 cin.tie(NULL); 61 62 scanf("%d",&n); 63 for(int i=1;i<=n;i++) 64 { 65 scanf("%d",&a[i]); 66 L+=a[i]; 67 } 68 if(L%4==0)//可以分成4條邊 69 { 70 L/=4; 71 DFS(1,1); 72 } 73 if(flag) printf("Yes\n"); 74 else printf("No\n"); 75 76 return 0; 77 }