之前已經學習過回溯法的一些問題,從這篇文章開始,繼續深入學習一下回溯法以及其他經典問題。
回溯法有通用的解題法之稱。用它可以系統的搜索一個問題的所有解或任一解,回溯法是一個既帶有系統性又帶有跳躍性的搜索算法。
它的問題的解空間樹中,按深度優先策略,從根結點出發搜索解空間樹。算法搜索至解空間樹的任一結點時,先判斷該結點是否包含問題的解。如果肯定不包含,則跳過對以該結點為根的子樹的搜索,逐層向其祖先結點回溯。否則,進入該子樹,繼續按深度優先策略搜索。回溯法求問題的所有解時,要回溯到根,且根結點的所有子樹都已被搜索遍才結束。回溯法求問題的一個解時,只要搜索到問題的一個解就可結束。
這種以深度優先方式搜索問題解的算法稱為回溯法,它適用於解組合數較大的問題。
回溯法的算法框架:
1、問題的解空間
用回溯法解問題時,應明確定義問題的解空間。問題的解空間至少應包含問題的一個(最優解)。
2、回溯法的基本思想
確定了解空間的組織結構后,回溯法從開始結點出發,以深度優先方式搜索整個解空間。這個開始結點稱為活結點,同時也稱為當期那的擴展結點,如果在當前的擴展結點處不能再向縱深方向移動,則當前擴展結點就稱為死結點。此時,應往回移動(回溯)至最近的一個或活結點處,並使這個活結點稱為當前的擴展結點。回溯法以這種工作方式遞歸地在解空間中搜索,直至找到所要求的解或解空間中已無活結點時為止。
3、遞歸回溯
回溯法對解空間作深度優先搜索,因此在一般情況下可用遞歸函數來實現回溯法如下:
void Backtrack(int t) { if(t > n) //t>n時已搜索到一個葉結點,output(x)得到的可行解x進行記錄或輸出處理 Output(x); else //當前拓展結點是解空間樹的內部結點 { for(int i = f(n,t); i <= g(n, t); i++) //函數f和g分別表示當前擴展結點處未搜索子樹的起止編號 { x[t] = h(i); //h(i)表示在當前擴展結點處x[t]的第i個可選值 if(Constraint(t) && Bound(t)) Backtrack(t+1); } //循環結束時,已搜索遍當前擴展結點的所有未搜索子樹 } }
其中,形式參數t表示遞歸深度,即當前擴展結點在解空間樹中的深度。n用來控制遞歸深度,當t>n時,算法已搜索到葉結點,此時,由Output(x)記錄或輸出得到的可行解x。算法BackTrack的for循環中f(n,t)和g(n,t)分別表示在當前擴展結點處未搜索過的子樹的起始編號和終止編號。h(i)表示在當前擴展結點處x[t]的第i個可選值。Constraint(t)和Bound(t)表示在當前擴展結點處的約束函數和限界函數。Constraint(t)返回的值為true時,在當前擴展結點處x[1:t]的取值滿足問題的約束條件,否則不滿足問題的約束條件,可剪去相應的子樹。
Bound(t)返回的值為true時,在當前擴展結點處x[1:t]的取值未使目標函數越界,還需由Backtrack(t+1)對其相應的子樹做進一步搜索。
否則,當前擴展結點處x[1:t]的取值使目標函數越界,可剪去相應的子樹。執行了算法的for循環后,已搜索遍當前擴展結點的所有未搜索過的子樹。Backtrack(t)執行完畢,返回t-1層繼續執行,對還沒有測試過的x[t-1]的值繼續搜索。當t=1時,若已測試完x[1]的所有可選值,外層調用就全部結束。顯然,這一搜索過程按深度優先方式進行,調用一次Backtrack(1)即可完成整個回溯搜索過程。
4、迭代回溯
采用樹的非遞歸深度優先遍歷算法,也可將回溯法表示為一個非遞歸的迭代過程如下:
void IterativeBacktrack() { int t; t = 1; //當前擴展結點在解空間樹中的深度,在這一層確定解向量的第t個分量x[t]的取值 while(t > 0) { if(f(n,t) <= g(n,t)) //f和g分別表示在當前擴展結點處未搜索子樹的起止編號 { for(int i = f(n,t); i <= g(n,t); i++) { x[t] = h(i); //h(i)表示在當前擴展結點處x[t]的第i個可選值 if(Constraint(t) && Bound(t)) { if(Solution(t)) //solution(t)判斷當前擴展結點處是否已得到問題的一個可行解 Output(x); else t++; //solution(t)為假,則僅得到一個部分解,需繼續縱深搜索 } } } else t--; //如果f(n,t)>g(n,t),已搜索遍當前擴展結點的所有未搜索子樹, } //返回t-1層繼續執行,對未測試過的x[t-1]的值繼續搜索 }
上述迭代回溯算法中,用Solution(t)判斷在當前擴展結點處是否已得到問題的可行解。它返回的值為true時,在當前擴展結點處x[1:t]是問題的可行解。此時,由Output(x)記錄或輸出得到的可行解。它返回的值為false時,在當前擴展結點處x[1:t]只是問題的部分解,還需向縱深方向繼續搜索。
算法中f(n,t)和g(n,t)分別表示在當前擴展結點處未搜索過的子樹的起始編號和終止編號。h(i)表示在當前擴展結點處x[t]的第i個可選值。Constraint(t)和Bound(t)是當前擴展結點處的約束函數和限界函數。Constraint(t)的返回的值為true時,在當前擴展結點處x[1:t]的取值滿足問題的約束條件,否則不滿足問題的約束條件,可剪去相應的子樹。Bound(t)返回的值為true時,在當前擴展結點處x[1:t]的取值未使目標函數越界,還需對其相應的子樹做進一步搜索。否則,當前擴展結點處x[1:t]的取值已使目標函數越界,可剪去相應的子樹。算法的while循環結束后,完成整個回溯搜索過程。
5、字集樹與排列樹
當所給的問題是從n個元素的集合S中找出滿足某種性質的子集時,相應的解空間數稱為子集樹。這類子集樹通常有個葉結點,其結點總個數為.遍歷子集樹的任何算法均需的計算時間。
void Backtrack(int t) { if(t > n) Output(x); else { for(int i = 0; i <= 1; i++) { x[t] = i; if(Constraint(t) && Bound(t)) Backtrack(t+1); } } }
當所給的問題是確定n個元素滿足某種性質的排列時,相應的解空間樹稱為排列樹。排列樹通常有n!個葉結點。因此遍歷排列數需要的計算時間。
void Backtrack(int t) { if(t > n) Output(x); else { for(int i = t; i <= n; i++) { swap(x[t], x[i]); if(Constraint(t) && Bound(t)) Backtrack(t+1); swap(x[t], x[i]); } } }
在調用Backtrack(1)執行回溯搜索之前,先將變量數組x初始化為單位排列(1,2,....,n)