1. 程式人生 > >遞迴、回溯-演算法框架

遞迴、回溯-演算法框架

之前已經學習過回溯法的一些問題,從這篇文章開始,繼續深入學習一下回溯法以及其他經典問題。

回溯法有通用的解題法之稱。用它可以系統的搜尋一個問題的所有解或任一解,回溯法是一個既帶有系統性又帶有跳躍性的搜尋演算法。

它的問題的解空間樹中,按深度優先策略,從根結點出發搜尋解空間樹。演算法搜尋至解空間樹的任一結點時,先判斷該結點是否包含問題的解。如果肯定不包含,則跳過對以該結點為根的子樹的搜尋,逐層向其祖先結點回溯。否則,進入該子樹,繼續按深度優先策略搜尋。回溯法求問題的所有解時,要回溯到根,且根結點的所有子樹都已被搜尋遍才結束。回溯法求問題的一個解時,只要搜尋到問題的一個解就可結束。

這種以深度優先方式搜尋問題解的演算法稱為回溯法,它適用於解組合數較大的問題。

回溯法的演算法框架

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中找出滿足某種性質的子集時,相應的解空間數稱為子集樹。這類子集樹通常有2^{n}個葉結點,其結點總個數為2^{n+1}-1.遍歷子集樹的任何演算法均需\Omega (2^{n})的計算時間。

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!個葉結點。因此遍歷排列數需要\Omega (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)