回溯算法入門及經典案例剖析(初學者必備寶典)


前言

基於有需必寫的原則,並且當前這個目錄下的文章數量為0(都是因為我懶QAQ),作為開局第一篇文章,為初學者的入門文章,自然要把該說明的東西說明清楚,於是。。。我整理了如下這篇文章,作者水平有限,有不足之處還望大家多多指出~~~

概念

首先,回溯是什么意思?很多初學者都會問這樣的一個問題。我們可以舉這樣一個例子:

1
1
1
1
0
1
0
1
0
1
0
1
0
1
1
1

我們看到了如圖所示的一個4*4的迷宮了,我們假設數字1標記的位置為道路,數字0標記的位置為一堵牆,一個人由起點(0.0)走到終點(3,3),我們有幾種方式可以到達呢?這個是不是很簡單的一個問題,由圖我們可以推出有兩種方式可以到達,並且每條路徑的長度均為6(設單位長度為1)。

眾曰:誒,櫻姐姐,你說的這個問題和我們將要提的有關系嘛?似乎你並沒有提到回溯這個概念啊!!!

櫻姐姐:當然有關系啦!繼續往下看,假如我們把終點由(3,3)換成(1,3),結果是不是會有變化呢?

我們由圖中可以看出有兩條可以到達的路徑:(0.0)->(0,1)->(0,2)->(0,3)->(1,3),(0,0)->(0,1)->(1,1)->(2,1)->(3,1)->(3,2)->(3,3)->(2,3)->(1,3)

兩條路徑長度分別為4和8(設單位長度為1),並且我們可以知道由起點到終點的最短的路徑為(0.0)->(0,1)->(0,2)->(0,3)->(1,3),長度為4

如果把這一過程交給計算機來處理,計算機該怎么辦呢?

此時就要提到我們這個偉大的回溯算法啦!!!

首先回溯算法類似枚舉的搜索嘗試過程,何為枚舉,可參考之前寫過的一篇文章,我們需要在搜索嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。

比如,我們要從(1,1)這點出發,找到(3,3)這個位置,計算機所計算出的可能路徑就不是簡單的兩條了,因為在我們所到達的每一個點,都有上下左右四個方向可以走,而計算機只能去執行我們所設定的參數變量去搜尋可以行走的路線,我們就需要去進行一個設計線路,讓人能從這個迷宮里走出來,一旦發現這條路不通(遇到了牆),就要退回上一步進行重新選擇,這種走不通就退回再走的方法稱為回溯法。

說到這里相信大家都差不多理解了回溯法的概念,個人理解,如果對DFS(Depth-First-Search)和BFS(Breadth-First-Search)有了解的同學對回溯這個概念應該是再熟悉不過了,因為實質就是在問題的解空間進行深度優先搜索。DFS是個圖的算法,但是回溯算法的圖在哪里呢?我們把解空間的一個狀態當做一個節點,由於解空間非常龐大,這個圖就大到無法想象了。

回溯法並不考慮問題規模的大小,而是從問題的最明顯的最小規模開始逐步求解出可能的答案,並以此慢慢地擴大問題規模,迭代地逼近最終問題的解。這種迭代類似於窮舉並且是試探性的,因為當目前的可能答案被測試出不可能可以獲得最終解時,則撤銷當前的這一步求解過程,回溯到上一步尋找其他求解路徑。
為了能夠撤銷當前的求解過程,必須保存上一步以來的求解路徑,這一點相當重要。

對DFS和BFS不了解的同學,請轉到傳送門:這里哦!

解題步驟

  1. 針對所給問題,定義問題的解空間,它至少包含問題的一個(最優)解。
  2. 確定易於搜索的解空間結構,使得能用回溯法方便地搜索整個解空間 。
  3. 以深度優先的方式搜索解空間,並且在搜索過程中用剪枝函數避免無效搜索。

一般寫法:

1 void search(){
2     //回溯條件
3     if(滿足條件){
4          return;
5     }
6     //否則繼續進行搜索
7          ......        
8 }

實例分析

1.八皇后問題

該問題是國際西洋棋棋手馬克斯·貝瑟爾於1848年提出:在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。

最容易想到的方法就是有序地從第 1 列的第 1 行開始,嘗試放上一個皇后,然后再嘗試第 2 列的第幾行能夠放上一個皇后,如果第 2 列也放置成功,那么就繼續放置第 3 列,如果此時第 3 列沒有一行可以放置一個皇后,說明目前為止的嘗試是無效的(即不可能得到最終解),那么此時就應該回溯到上一步(即第 2 步),將上一步(第 2 步)所放置的皇后的位置再重新取走放在另一個符合要求的地方…如此嘗試性地遍歷加上回溯,就可以慢慢地逼近最終解。

如果我們逐行放置皇后則肯定沒有任意兩個皇后位於同一行,只需要判斷列和對角線即可。使用一個二維數組vis[3][],其中vis[0][i]表示列,vis[1][i]和vis[2][i]表示對角線。因為(x,y)的y-x值標識了主對角線,x+y值標識了副對角線。由於y-x可能為負,所以存取時要加上n。

參考寫法如下:

 1 void search(int cur)  
 2 {  
 3     int i,j;  
 4     if(cur==8) tot++;
 5     else  
 6     {  
 7         for(i=0;i<8;i++)  
 8         {  
 9             if(!vis[0][i]&&!vis[1][cur-i+8]&&!vis[2][cur+i])  
10             {  
11                 vis[0][i]=1;  
12                 vis[1][cur-i+8]=1;  
13                 vis[2][cur+i]=1;    
14                 search(cur+1);  
15                 //改回輔助的全局變量 
16                 vis[0][i]=0;       
17                 vis[1][cur-i+8]=0;  
18                 vis[2][cur+i]=0;  
19             }  
20         }  
21     }  
22 } 

最終我們可以去得到答案:

1 int vis[3][15],tot;
2 int main()  
3 {  
4     search(0);   
5     cout<<tot<<endl;
6 }

2.圖的着色問題

給定無向連通圖G=(V,E)和m種不同的顏色,用這些顏色為圖G的各頂點着色,每個頂點着一種顏色。如果一個圖最少需要m種顏色才能使圖中每條邊連接的2個頂點着不同顏色,則稱m為該圖的色數。地圖着色問題可轉換為圖的着色問題:以地圖中的區域作為圖中頂點,2個區域如果鄰接,則這2個區域對應的頂點間有一條邊,即邊表示了區域間的鄰接關系。著名的四色定理就是指每個平面地圖都可以只用四種顏色來染色,而且沒有兩個鄰接的區域顏色相同。

給定圖和顏色的數目求出着色方法的數目,可以使用回溯法。

參考函數如下:

1 bool ok(int k)
2 {
3     for(int j=1;j<=v;j++)
4     {
5         if(graph[k][j]&&(color[j]==color[k])) return false;
6     }
7     return true;
8 }
 1 void backtrack(int t)
 2 {
 3     if(t>v) sum++;
 4      else
 5      {
 6         for(int i=1;i<=c;i++)
 7         {
 8             color[t]=i;
 9                if(ok(t)) backtrack(t+1);
10                //改回輔助的全局變量 
11                color[t]=0;
12         }
13      }
14 }

最終我們可以去得到答案:

 1 #define N 100
 2 int v,e,c,graph[N][N],color[N];
 3 //頂點數,邊數,顏色數 
 4 int sum;
 5 int main()
 6 {
 7     int i,j;
 8     cin>>v>>e>>c;                
 9     for(i=1;i<=v;i++)
10     {
11         for(j=1;j<=v;j++)
12         {
13             graph[i][j]=0; 
14         }
15     }           
16     for(int k=1;k<=e;k++)      
17     {
18         cin>>i>>j;
19         graph[i][j]=1;
20         graph[j][i]=1;
21     }
22     for(i=0;i<=v;i++) color[i]=0;
23      backtrack(1);
24       cout<<sum<<endl;
25 }

3.裝載問題

有一批共n個集裝箱要裝上2艘載重量分別為c1和c2的船,其中集裝箱i的重量為wi,且。裝載問題要求確定是否有一個合理的裝載方案可將這些集裝箱裝上這2艘船。如果有,找出一種裝載方案。例如當n=3,c1=c2=50且w=[10,40,40]時,則可以將集裝箱1和2裝到第一艘輪船上,而將集裝箱3裝到第二艘輪船上;如果w=[20,40,40],則無法將這3個集裝箱都裝上輪船。容易證明,如果一個給定裝載問題有解,則首先將第一艘船盡可能裝滿再將剩余的集裝箱裝上第二艘船可得到最優裝載方案。將第一艘船盡可能裝滿等價於選取全體集裝箱的一個子集,使該子集中集裝箱重量之和最接近c1。用回溯法解裝載問題,  時間復雜度O(2^n),在某些情況下優於動態規划算法。剪枝方案是如果當前已經選擇的全部物品載重量cw+剩余集裝箱的重量r<=當前已知的最優載重量bestw,則刪去該分支。

 1 void backtrack(int i)  
 2 {        
 3     if(i>n)    
 4     {  
 5         if(ans>bestans) bestans=ans;  
 6         return;  
 7     }  
 8     r-=w[i];  
 9     if(ans+w[i]<=c1)  
10     {   
11       ans+=w[i];  
12       backtrack(i+1);  
13       //改回輔助的全局變量 
14       ans-=w[i];  
15     }  
16     if(ans+r>bestans) backtrack(i+1);    
17     //改回輔助的全局變量 
18     r+=w[i];  
19 }    
1 int maxloading()  
2 {  
3     ans=0;  
4     bestans=0;  
5     backtrack(1);   
6     return bestans;  
7 }

最終我們可以去得到答案:

 1 int n;//集裝箱數  
 2 int w[40];//集裝箱重量
 3 int c1,c2;//兩艘船的載重量  
 4 int ans;//當前載重量  
 5 int bestans;//當前最優載重量  
 6 int r;//剩余集裝箱重量 
 7 int main()  
 8 {    
 9     cin>>n>>c1>>c2;  
10      int i=1;  
11      int sum=0;  
12      //集裝箱總重量 
13      while(i<=n)  
14     {  
15         cin>>w[i];  
16         r+=w[i];  
17         sum+=w[i];  
18          i++;  
19      }    
20     maxloading();  
21     if(bestans>0&&((sum-bestans)<=c2)) cout<<bestans<<endl;  
22      else if(sum<=c2) cout<<bestans<<endl;  
23       else cout<<"No"<<endl;  
24 }

4.批處理作業調度問題

給定n個作業的集合{J1,J2,…,Jn}。每個作業必須先由機器1處理,然后由機器2處理。作業Ji需(1≤i≤n)要機器j(1≤j≤2)的處理時間為tji。對於一個確定的作業調度,設Fji是作業i在機器j上完成處理的時間。所有作業在機器2上完成處理的時間和稱為該作業調度的完成時間和:。要求對於給定的n個作業,制定最佳作業調度方案,使其完成時間和達到最小。

 

tji 機器1 機器2
作業1 2 1
作業2 3 1
作業3 2 3

例如,對於這張表格所示的情況,3個作業有3!=6種可能調度方案,很顯然最壞復雜度即為O(n!)。如果按照2,3,1的順序,則作業2的完成時間為4,作業3的完成時間為8,作業1的完成時間為9,完成時間和為21。最優的作業調度順序為最佳調度方案是1,3,2,其完成時間和為18。

 1 void backtrack(int k)
 2 {
 3     if(k>number)
 4     {
 5         for(int i=1;i<=number;i++) bestorder[i]=xorder[i];
 6           bestvalue=xvalue;
 7     }
 8     else
 9     {
10         for(int i=k;i<=number;i++)
11           {
12            f1+=x1[xorder[i]];
13            f2[k]=(f2[k-1]>f1?f2[k-1]:f1)+x2[xorder[i]];
14            xvalue+=f2[k];
15            swap(xorder[i],xorder[k]);
16            if(xvalue<bestvalue) backtrack(k+1);
17            swap(xorder[i],xorder[k]);
18            xvalue-=f2[k];
19            f1-=x1[xorder[i]];
20         }
21     }
22 }
23     

最終我們可以去得到答案:

 1 #define MAX 200
 2 int* x1;//作業Ji在機器1上的工作時間
 3 int* x2;//作業Ji在機器2上的工作時間
 4 int number=0;//作業的數目
 5 int* xorder;//作業順序
 6 int* bestorder;//最優的作業順序
 7 int bestvalue=MAX;//最優的時間
 8 int xvalue=0;//當前完成用的時間
 9 int f1=0;//機器1完成的時間
10 int* f2;//機器2完成的時間
11 int main()
12 {
13     cout<<"請輸入作業數目:";
14      cin>>number;
15     x1=new int[number+1];
16      x2=new int[number+1];
17       xorder=new int[number+1];
18        bestorder=new int[number+1];
19        f2=new int[number+1];
20        x1[0]=0;
21        x2[0]=0;
22        xorder[0]=0;
23        bestorder[0]=0;
24     f2[0]=0;
25     cout<<"請輸入每個作業在機器1上所用的時間:"<<endl;
26     int i;
27     for(i=1;i<=number;i++)
28     {
29         cout<<""<<i<<"個作業=";
30         cin>>x1[i];
31       }
32     cout<<"請輸入每個作業在機器2上所用的時間:"<<endl;
33      for(i=1;i<=number;i++)
34       {
35            cout<<""<<i<<"個作業=";
36          cin>>x2[i];
37       }
38        for(i=1;i<=number;i++) xorder[i]=i;
39     backtrack(1);
40     cout<<"最節省的時間為:"<<bestvalue<<endl;
41     cout<<"對應的方案為:";
42     for(i=1;i<=number;i++) cout<<bestorder[i]<<"  ";
43     cout<<endl;
44 }

5.01背包問題

當然,回溯問題還可以用來解決01背包問題,對01背包不清楚的請移步至這里

下面貼下01背包的模板

 1 void backtrack(int i,int cp,int cw)
 2 {
 3     if(i>n)
 4     {
 5         if(cp>bestp)
 6         {
 7             bestp=cp;
 8             for(i=1;i<=n;i++) bestx[i]=x[i];
 9         }
10     }
11     else
12     {
13         for(int j=0;j<=1;j++)  
14         {
15             x[i]=j;
16             if(cw+x[i]*w[i]<=c)  
17             {
18                 cw+=w[i]*x[i];
19                 cp+=p[i]*x[i];
20                 backtrack(i+1,cp,cw);
21                 cw-=w[i]*x[i];
22                 cp-=p[i]*x[i];
23             }
24         }
25     }
26 }

最終我們可以去得到答案:

 1 int n,c,bestp;//物品個數,背包容量,最大價值
 2 int p[10000],w[10000],x[10000],bestx[10000];//物品的價值,物品的重量,物品的選中情況
 3 int main()
 4 {
 5     bestp=0; 
 6     cin>>c>>n;
 7     for(int i=1;i<=n;i++) cin>>w[i];
 8     for(int i=1;i<=n;i++) cin>>p[i];
 9     backtrack(1,0,0);
10     cout<<bestp<<endl;
11 }

6.最大團問題

給定無向圖G=(V, E),U是V的子集。如果對任意u,v屬於U有(u,v)屬於E,則稱U是G的完全子圖。G的完全子圖U是G的當且僅當U不包含在G的更大的完全子圖中。G的最大團是指G中所含頂點數最多的團。如果對任意u,v屬於U有(u, v)不屬於E,則稱U是G的空子圖。G的空子圖U是G的獨立集當且僅當U不包含在G的更大的空子圖中。G的最大獨立集是G中所含頂點數最多的獨立集。G的補圖G'=(V', E')定義為V'=V且(u, v)屬於E'當且僅當(u, v)不屬於E。
如圖所示,給定無向圖G={V, E},其中V={1,2,3,4,5},E={(1,2),(1,4),(1,5),(2,3),(2,5),(3,5),(4,5)}。根據最大團定義,子集{1,2}是圖G的一個大小為2的完全子圖,但不是一個團,因為它包含於G的更大的完全子圖{1,2,5}之中。{1,2,5}是G的一個最大團。{1,4,5}和{2,3,5}也是G的最大團。右側圖是無向圖G的補圖G'。根據最大獨立集定義,{2,4}是G的一個空子圖,同時也是G的一個最大獨立集。雖然{1,2}也是G'的空子圖,但它不是G'的獨立集,因為它包含在G'的空子圖{1,2,5}中。{1,2,5}是G'的最大獨立集。{1,4,5}和{2,3,5}也是G'的最大獨立集。

最大團問題可以用回溯法在O(n2^n)的時間內解決。首先設最大團為一個空團,往其中加入一個頂點,然后依次考慮每個頂點,查看該頂點加入團之后仍然構成一個團。程序中采用了一個比較簡單的剪枝策略,即如果剩余未考慮的頂點數加上團中頂點數不大於當前解的頂點數,可停止回溯。用鄰接矩陣表示圖G,n為G的頂點數,cn存儲當前團的頂點數,bestn存儲最大團的頂點數。當cn+n-i<bestn時,不能找到更大的團,利用剪枝函數剪去。

 1 void backtrack(int i)
 2 {
 3     if(i>v)
 4     {
 5         if(cn>bestn)
 6         {
 7             bestn=cn;
 8             for(int j=1;j<=v;j++) bestuse[j]=use[j];
 9             return;
10         }
11     }
12     bool flag=true;
13     for(int j=1;j<i;j++)
14     {
15         if(use[j]&&!graph[j][i])
16         {
17             flag=false;
18             break;
19         }
20     }
21     if(flag)
22     {
23         cn++;
24         use[i]=true;
25         backtrack(i+1);
26         use[i]=false;
27         cn--;
28     }
29     if(cn+v-i>bestn)  
30     {
31         use[i]=false;
32         backtrack(i+1);
33     }
34 }

最終我們可以去得到答案:

 1 const int maxnum=101;
 2 bool graph[maxnum][maxnum];
 3 bool use[maxnum],bestuse[maxnum]; 
 4 int cn,bestn,v,e;
 5 int main()
 6 {
 7     cin>>v>>e;
 8     for(int i=1;i<=e;i++)
 9     {
10         int p1,p2;
11         cin>>p1>>p2;
12           graph[p1][p2]=true;
13           graph[p2][p1]=true;
14     }
15     backtrack(1);
16     cout<<bestn<<endl;
17     for(int i=1;i<=v;i++) 
18     {
19         if(bestuse[i]) cout<<i<<" ";
20     }
21     cout<<endl;  
22 }

7.圓排列問題

給定n個大小不等的圓c1,c2,…,cn,現要將這n個圓排進一個矩形框中,且要求各圓與矩形框的底邊相切。圓排列問題要求從n個圓的所有排列中找出有最小長度的圓排列。例如,當n=3,且所給的3個圓的半徑分別為1,1,2時,這3個圓的最小長度的圓排列如圖所示。其最小長度為


注意,下面代碼中圓排列的圓心橫坐標以第一個圓的圓心為原點。所以,總長度為第一個圓的半徑+最后一個圓的半徑+最后一個圓的橫坐標。

 1 //計算當前所選擇圓的圓心橫坐標
 2 float center(int t)
 3 {
 4     float temp=0;
 5     for(int j=1;j<t;j++)
 6     {
 7         //由x^2=sqrt((r1+r2)^2-(r1-r2)^2)推導而來
 8         float valuex=x[j]+2.0*sqrt(r[t]*r[j]);
 9         if(valuex>temp) temp=valuex;
10     }
11     return temp;
12 }
 1 //計算當前圓排列的長度
 2 void compute()
 3 {
 4     float low=0,high=0;
 5     for(int i=1;i<=n;i++)
 6     {
 7         if(x[i]-r[i]<low) low=x[i]-r[i];
 8         if(x[i]+r[i]>high) high=x[i]+r[i];
 9     }
10     if(high-low<minlen) minlen=high-low;
11 }
 1 void backtrack(int t)
 2 {
 3     if(t>n) compute();
 4     else
 5     {
 6         for(int j=t;j<=n;j++)
 7         {
 8             swap(r[t],r[j]);
 9             float centerx=center(t);
10             if(centerx+r[t]+r[1]<minlen)
11             {
12                 x[t]=centerx;
13                 backtrack(t+1);
14             }
15             swap(r[t],r[j]);
16         }
17     }
18 }

最終我們可以去得到答案:

 1 float minlen=10000,x[4],r[4];//當前最優值,當前圓排列圓心橫坐標,當前圓排列
 2 int n;//圓排列中圓的個數
 3 int main()
 4 {
 5     n=3; 
 6     r[1]=1,r[2]=1,r[3]=2;
 7     cout<<"各圓的半徑分別為:"<<endl;
 8     for(int i=1;i<=3;i++) cout<<r[i]<<" ";
 9     cout<<endl;
10     cout<<"最小圓排列長度為:";
11     backtrack(1);
12     cout<<minlen<<endl;
13 }

上述算法尚有許多改進的余地。例如,像1,2,…,n-1,n和n,n-1, …,2,1這種互為鏡像的排列具有相同的圓排列長度,只計算一個就夠了。而且,如果所給的n個圓中有k個圓有相同的半徑,則這k個圓產生的k!個完全相同的圓排列,也只需要計算一個。

8.連續郵資問題

假設國家發行了k種不同面值的郵票,並且規定每張信封上最多只允許貼h張郵票。連續郵資問題要求對於給定的k和h的值,給出郵票面值的最佳設計,在1張信封上可貼出從郵資1開始,增量為1的最大連續郵資區間。例如,當k=5和h=4時,面值為(1,3,11,15,32)的5種郵票可以貼出郵資的最大連續郵資區間是1到70。UVA165就是一道這樣的典型例題。用stampval來保存各個面值,用maxval來保存當前所有面值能組成的最大連續面值。那么,stampval[0] 一定等於1,因為1是最小的正整數。相應的,maxval[0]=1*h。接下去就是確定第二個,第三個......第k個郵票的面值了。對於stampval[i+1],它的取值范圍是stampval[i]+1~maxval[i]+1。 stampval[i]+1是因為這一次取的面值肯定要比上一次的面值大,而這次取的面值的上限是上次能達到的最大連續面值+1, 是因為如果比這個更大的話, 那么就會出現斷層, 即無法組成上次最大面值+1這個數了。 舉個例子, 假設可以貼3張郵票,有3種面值,前面2種面值已經確定為1,2, 能達到的最大連續面值為6, 那么接下去第3種面值的取值范圍為3~7。如果取得比7更大的話會怎樣呢? 動手算下就知道了,假設取8的話, 那么面值為1,2,8,將無法組合出7。直接遞歸回溯所有情況, 便可知道最大連續值了。

1 //標記每種取到的錢數 
2 void mark(int n,int m,int sum)
3 {  
4     if(m>h) return;  
5     vis[sum]=true;
6     for(int i=1;i<=n;++i) mark(n,m+1,sum+stampval[i]);    
7 } 
 1 void backtrack(int cur)
 2 {  
 3     if(cur>k)
 4     {  
 5         if(maxval[cur-1]>maxstampval)
 6         {  
 7             maxstampval=maxval[cur-1];  
 8             memcpy(ans,stampval,sizeof(stampval));  
 9         }  
10         return;  
11     }  
12     for(int i=stampval[cur-1]+1;i<=maxval[cur-1]+1;++i)
13     {  
14         memset(vis,0,sizeof(vis));  
15         stampval[cur]=i;  
16         mark(cur,0,0);  
17         int num=0,j=1;  
18         while(vis[j++]) ++num;  
19         maxval[cur]=num;  
20         backtrack(cur+1);  
21     }  
22 }  

最終我們可以去得到答案:

 1 #define MAXN 200 
 2 int h,k,ans[MAXN],stampval[MAXN],maxval[MAXN],maxstampval;  
 3 bool vis[MAXN];  
 4 int main()
 5 {   
 6     while(scanf("%d %d",&h,&k),h+k)
 7     {  
 8         maxval[1]=h;  
 9         stampval[1]=1;  
10         maxstampval=-1;  
11         backtrack(2);  
12         for(int i=1;i<=k;++i) printf("%3d",ans[i]);  
13         printf("->%3d\n",maxstampval);  
14     }    
15 } 

直接遞歸的求解復雜度太高,不妨嘗試計算用不超過m張面值為x[1:i]的郵票貼出郵資k所需的最少郵票數y[k]。通過y[k]可以很快推出r的值。事實上,y[k]可以通過遞推在O(n)時間內解決。這里就不再講解了。

9.符號三角形問題

下圖是由14個“+”和14個“-”組成的符號三角形,第一行有n個符號。2個同號下面都是“+”,2個異號下面都是“-”。

符號三角形問題要求對於給定的n,計算有多少個不同的符號三角形,使其所含的“+”和“-”的個數相同。在第1行前i個符號x[1:i]確定后,就確定了1個由i(i+1)/2個符號組成的三角形。下一步確定第i+1個符號后,在右邊再加1條邊,就可以擴展為前i+1個符號x[1:i+1]對應的新三角形。這樣依次擴展,直到x[1:n]。最終由x[1:n]所確定的符號三角形中含"+"號個數與"-"個數同為n(n+1)/4。因此,當前符號三角形所包含的“+”個數與“-”個數均不超過n*(n+1)/4,可以利用這個條件剪支。對於給定的n,當n*(n+1)/2為奇數時,顯然不存在包含的"+"號個數與"-"號個數相同的符號三角形。在回溯前需要簡單的判斷一下。

 1 void backtrack(int t)  
 2 {  
 3     if((counts>half)||(t*(t-1)/2-counts>half)) return;  
 4     if(t>n) sum++;    
 5     else  
 6     {  
 7         for(int i=0;i<2;i++)   
 8         {  
 9             p[1][t]=i;//第一行符號  
10             counts+=i;//當前"+"號個數  
11             for(int j=2;j<=t;j++)   
12             {  
13                 p[j][t-j+1]=p[j-1][t-j+1]^p[j-1][t-j+2];  
14                 counts+=p[j][t-j+1];  
15             }  
16             backtrack(t+1);  
17             for(int j=2;j<=t;j++)  
18             {  
19                 counts-=p[j][t-j+1];  
20             }  
21             counts-=i;  
22         }  
23     }  
24 }   

最終我們可以去得到答案:

 1 int n,half,counts,p[100][100],sum;
 2 //第一行的符號個數,n*(n+1)/4,當前"+"號個數,符號三角矩陣,已找到的符號三角形數   
 3 int main()  
 4 {
 5     cin>>n;      
 6     half=n*(n+1)/2;  
 7     if(half%2==1)
 8     {
 9         cout<<"共有0個不同的符號三角形。"<<endl;
10         return 0;
11     }
12     half=half/2;   
13     backtrack(1);  
14     cout<<"共有"<<sum<<"個不同的符號三角形。"<<endl;
15 } 

10.集合划分問題

給定一個圖,圖中任意兩點的距離已知,請你把這個圖的所有的點分成兩個子集,要求兩個子集之間的所有點的距離和最大。對於圖中的每一個點,我們可以設一個數組,用0和1表示屬於哪個子集。

 1 void backtrack(int x,int sum) 
 2 {  
 3     int temp;
 4     if(x>n) 
 5     {  
 6         if(sum>ans) ans=sum; 
 7         return; 
 8     } 
 9     //不選
10     set[x]=0;
11     temp=0;
12     for(int i=1;i<=x;i++) 
13     { 
14         if(!set[i]) continue;
15         temp+=graph[i][x];
16     } 
17     backtrack(x+1,sum+temp); 
18     //
19     set[x]=1;
20     temp=0; 
21     for(int i=1;i<=x;i++) 
22     { 
23         if(set[i]) continue; 
24         temp+=graph[i][x];
25     }   
26     backtrack(x+1,sum+temp);   
27 }  

最終我們可以去得到答案:

 1 int graph[25][25]; 
 2 int set[25]; 
 3 int ans,n;
 4 int main() 
 5 {  
 6     cin>>n;  
 7     for(int i=1;i<=n;i++) 
 8     { 
 9         for(int j=1;j<=n;j++) 
10         { 
11             cin>>graph[i][j];  
12         } 
13     }   
14     backtrack(1,0); 
15     cout<<ans<<endl;
16 }

例題練習推薦

  • N皇后問題是一個經典的問題,在一個N*N的棋盤上放置N個皇后,每行一個並使其不能互相攻擊(同一行、同一列、同一斜線上的皇后都會自動攻擊),求出有多少種合法的放置方法。輸出N皇后問題所有不同的擺放情況個數。---九度OJ1254
  • 給定無向連通圖G和m種不同的顏色。用這些顏色為圖G的各頂點着色,每個頂點着一種顏色。如果有一種着色法使G中每條邊的2個頂點着不同顏色,則稱這個圖是m可着色的。圖的m着色問題是對於給定圖G和m種顏色,找出所有不同的着色法。---洛谷P2819
  • 現已知yifenfei隨身攜帶有n種濃度的萬能葯水,體積V都相同,濃度則分別為Pi%。並且知道,針對當時幽谷的瘴氣情況,只需選擇部分或者全部的萬能葯水,然后配置出濃度不大於 W%的葯水即可解毒。如何配置此葯,能得到最大體積的當前可用的解葯呢?---hdu2570
  • 將一堆正整數分為2組,要求2組的和相差最小.---51Nod 1007 參考題解在這里,關於01背包問題更多題目推薦參考這里
  • 已知班級有g個女孩和b個男孩,所有女生之間都相互認識,所有男生之間也相互認識,給出m對關系表示哪個女孩與哪個男孩認識。現在要選擇一些學生來組成一個團,使得里面所有人都認識,求此團最大人數。---POJ3692
  • 某國家發行k種不同面值的郵票,並且規定每張信封上最多只能貼h張郵票。 公式n(h,k)表示用從k中面值的郵票中選擇h張郵票,可以組成面額為連續的1,2,3,……n, n是能達到的最大面值之和。例如當h=3,k=2時, 假設兩種面值取值為1,4, 那么它們能組成連續的1……6,  雖然也可以組成8,9,12,但是它們是不連續的了。---UVA165
  • 符號三角形的 第1行有n個由“+”和”-“組成的符號 ,以后每行符號比上行少1個,2個同號下面是”+“,2個異 號下面是”-“ 。計算有多少個不同的符號三角形,使其所含”+“ 和”-“ 的個數相同 。---hdu2510
  • 給定正整數n,計算出n 個元素的集合{1,2,L, n }可以划分為多少個不同的非空子集。---南郵oj1215

更多題目推薦未來待續更新

參考文獻

  1. 百度百科:https://baike.baidu.com/item/回溯算法/9258495
  2. 回溯算法:http://www.docin.com/p-191320525.html?docfrom=rrela
  3. 三個典型問題的回溯算法(PDF):http://www.doc88.com/p-2935311069314.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM