C語言算法之回溯法


回溯法

算法介紹

  回溯法(Back Tracking Method)(探索與回溯法)是一種選優搜索法,又稱為試探法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術為回溯法,而滿足回溯條件的某個狀態的點稱為“回溯點”。

基本思想

  在回溯法中,每次擴大當前部分解時,都面臨一個可選的狀態集合,新的部分解就通過在該集合中選擇構造而成。這樣的狀態集合,其結構是一棵多叉樹,每個樹結點代表一個可能的部分解,它的兒子是在它的基礎上生成的其他部分解。樹根為初始狀態,這樣的狀態集合稱為狀態空間樹。
  回溯法對任一解的生成,一般都采用逐步擴大解的方式。每前進一步,都試圖在當前部分解的基礎上擴大該部分解。它在問題的狀態空間樹中, 從開始結點(根結點)出發,以 深度優先搜索整個狀態空間。這個開始結點成為活結點,同時也成為當前的擴展結點。在當前擴展結點處,搜索向縱深方向移至一個新結點。這個新結點成為新的活結點,並成為當前擴展結點。如果在當前擴展結點處不能再向縱深方向移動,則當前擴展結點就成為死結點。此時,應往回移動(回溯)至最近的活結點處,並使這個活結點成為當前擴展結點。回溯法以這種工作方式遞歸地在狀態空間中搜索,直到找到所要求的解或解空間中已無活結點時為止。
  回溯法與窮舉法有某些聯系,它們都是基於試探的。窮舉法要將一個解的各個部分全部生成后,才檢查是否滿足條件,若不滿足,則直接放棄該完整解,然后再嘗試另一個可能的完整解,它並沒有沿着一個可能的完整解的各個部分逐步回退生成解的過程。 而對於回溯法,一個解的各個部分是逐步生成的,當發現當前生成的某部分不滿足約束條件時,就放棄該步所做的工作,退到上一步進行新的嘗試,而不是放棄整個解重來。

算法框架

問題的解空間

  應用回溯法求解問題時, 首先應明確定義問題的解空間,該解空間應 至少包含問題的一個最優解。例如,對於有n種物品的 0-1 背包問題,其解空間由長度為n的 0-1 向量組成,該解空間包含了對變量的所有可能的0-1 賦值。當 n=3 時,其解空間是{ (0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1) }
  在定義了問題的解空間后,還需要將解空間有效地組織起來,使得回溯法能方便地搜索整個解空間,通常將 解空間組織成樹或圖的形式。例如,對於n= 3的0-1 背包問題,其解空間可以用一棵完全二叉樹表示,從樹根到葉子結點的任意一條路徑可表示解空間中的一個元素,如從根結點A到結點J的路徑對應於解空間中的一個元素(1, 0, 1)。

回溯法解題的關鍵要素

  確定了問題的解空間結構后,回溯法將 從開始結點(根結點)出發,以深度優先的方式搜索整個解空間。開始結點成為活結點,同時也成為擴展結點。在當前的擴展結點處,向縱深方向搜索並移至一個新結點,這個新結點就成為一個新的活結點,並成為當前的擴展結點。如果在當前的擴展結點處不能再向縱深方向移動,則當前的擴展結點就成為死結點。此時應往回移動(回溯)至最近的一個活結點處,並使其成為當前的擴展結點。回溯法以上述工作方式遞歸地在解空間中搜索,直至找到所要求的解或解空間中已無活結點時為止。
運用回溯法解題的關鍵要素有以下三點:
  1.  針對給定的問題,定義問題的解空間;
  2.  確定易於搜索的解空間結構;
  3.  以深度優先方式搜索解空間,並且在搜索過程中用剪枝函數避免無效搜索

遞歸和迭代回溯

一般情況下可以用遞歸函數實現回溯法,遞歸函數模板如下:

void BackTrace(int t) {
    if(t>n)
        Output(x);
    else
        for(int i = f (n, t); i <= g (n, t); i++ ) {
            x[t] = h(i);
            if(Constraint(t) && Bound (t))
                BackTrace(t+1);
        }
}

  其中,t 表示遞歸深度,即當前擴展結點在解空間樹中的深度;n 用來控制遞歸深度,即解空間樹的高度。當 t>n時,算法已搜索到一個葉子結點,此時由函數Output(x)對得到的可行解x進行記錄或輸出處理。用 f(n, t)和 g(n, t)分別表示在當前擴展結點處未搜索過的子樹的起始編號終止編號h(i)表示在當前擴展結點處x[t] 的第i個可選值;函數 Constraint(t)和 Bound(t)分別表示當前擴展結點處的約束函數限界函數。若函數 Constraint(t)的返回值為真,則表示當前擴展結點處x[1:t] 的取值滿足問題的約束條件;否則不滿足問題的約束條件。若函數Bound(t)的返回值為真,則表示在當前擴展結點處x[1:t] 的取值尚未使目標函數越界,還需由BackTrace(t+1)對其相應的子樹做進一步地搜索;否則,在當前擴展結點處x[1:t]的取值已使目標函數越界,可剪去相應的子樹。

  采用迭代的方式也可實現回溯算法,迭代回溯算法的模板如下:

void IterativeBackTrace(void) {
    int t = 1;
    while(t>0) {
        if(f(n, t) <= g( n, t))
            for(int i = f(n, t); i <= g(n, t); i++ ) {
                x[t] = h(i);
                if(Constraint(t) && Bound(t)) {
                    if ( Solution(t))
                        Output(x);
                    else
                        t++;
                }
            }
            else t−−;
    }
}

  在上述迭代算法中,用Solution(t)判斷在當前擴展結點處是否已得到問題的一個可行解,若其返回值為真,則表示在當前擴展結點處x[1:t] 是問題的一個可行解;否則表示在當前擴展結點處x[1:t]只是問題的一個部分解,還需要向縱深方向繼續搜索。用回溯法解題的一個顯著特征是問題的解空間是在搜索過程中動態生成的,在任何時刻算法只保存從根結點到當前擴展結點的路徑。如果在解空間樹中,從根結點到葉子結點的最長路徑長度為 h(n),則回溯法所需的計算空間復雜度為 O(h(n)),而顯式地存儲整個解空間復雜度則需要O(2h(n))或O(h(n)!)。

子集樹與排列樹

  當給定的問題是從n個元素的集合S中找出滿足某種性質的子集時,相應的解空間樹稱為子集樹。例如,n個物品的0-1 背包問題所對應的解空間樹是一棵子集樹,該類樹通常有2n個葉子結點,總結點數為2n+1- 1,遍歷子集樹的任何算法需要的計算時間復雜度均為O(2n)。

  回溯法搜索子集樹的一般算法描述如下:

void BackTrace(int t) {
    if(t>n)
        Output(x);
    else
        for(int i = 0; i <= n; i++) {
            x[t] = i;
            if(Contraint(t) && Bound(t))
                BackTrace (t + 1);
        }
}
  當給定的問題是確定 n 個元素滿足某種性質的排列時,對應的解空間樹稱為排列樹。排列樹通常有n! 個葉子結點,遍歷排列樹需要的計算時間復雜度為O(n!)。
  回溯法搜索排列樹的算法模板如下:
void BackTrace(int t) {
    if(t>n)
        Output(x);
    else
        for(int i = 0; i <= n; i++) {
            Swap(x[t], x[i]);
            if(Contraint (t) && Bound (t))
                BackTrace(t + 1);
            Swap(x[t], x[i]);
        }
}

實例

算法框架

 

 01背包問題

問題描述:

鏈接:http://lx.lanqiao.cn/problem.page?gpid=T287

問題描述
  給定N個物品,每個物品有一個重量W和一個價值V.你有一個能裝M重量的背包.問怎么裝使得所裝價值最大.每個物品只有一個.
輸入格式
  輸入的第一行包含兩個整數n, m,分別表示物品的個數和背包能裝重量。
  以后N行每行兩個數Wi和Vi,表示物品的重量和價值
輸出格式
  輸出1行,包含一個整數,表示最大價值。
樣例輸入
3 5
2 3
3 5
4 7
樣例輸出
8
數據規模和約定
 1<=N<=200,M<=5000.

思路:

01背包屬於找最優解問題,用回溯法需要構造解的子集樹。對於每一個物品i,對於該物品只有選與不選2個決策,總共有n個物品,可以順序依次考慮每個物品,這樣就形成了一棵解空間樹: 基本思想就是遍歷這棵樹,以枚舉所有情況,最后進行判斷,如果重量不超過背包容量,且價值最大的話,該方案就是最后的答案。

  • 在搜索狀態空間樹時,只要左子節點是可一個可行結點,搜索就進入其左子樹。對於右子樹時,先計算上界函數,以判斷是否將其減去(剪枝)。
  • 上界函數bound():當前價值cw+剩余容量可容納的最大價值<=當前最優價值bestp。 
  • 為了更好地計算和運用上界函數剪枝,選擇先將物品按照其單位重量價值從大到小排序,此后就按照順序考慮各個物品

分析:

 

我們來根據這個圖來分析基本的思路:首先我們有5個排好序了的(按單位重量價值排序)商品{【7,15】,【8,14】,【3,5】,【9,14】,【6,8】},根據我們上面的思路,首先就因該將【7,15】裝入背包,然后繼續右節點下去,發現【8,14】這個元素背包裝不下了,於是進行下一個判斷,從右節點進入,因為已經判斷好了【8,14】這個元素裝不下去,所以我們直接從他的下一個元素進入【3,5】,然后就進入了上界函數了。

double bound(int t){
    // 計算最優價值的函數,也是上界函數,其功能為剪枝
     int leftW = W - Wsum;    //剩余書包能裝下的重量
     double preV = Vsum;        // 當前的總價值
     while(t<=N && a[t][0]<= leftW){
         leftW -= a[t][0];
         preV += a[t][1];
         t++;
     } 
     if(t<= N){
         preV += (a[t][1]/(a[t][0]*1.0))*leftW;
     }
     return preV;
    
} 
上界函數

 在上界函數中,我們判斷當前背包的總價值Vsum加上背包剩余容量可容納的最大價值是不是小於等於當前的最佳價值,進去之后等到剩余背包容量為5,然后判斷是不是大於最后的一個物品了,假如不大於的話,判斷是否能裝下,如果不能的話並且還不大於最后的物品序號,那么當前總價值就是當前這個物品的單位重量價值* 背包余量加上之前的當前總價值,這里為什么要這么加上單位價值*背包余量呢,就是因為需要跳過裝不下的物品,繼續向下搜索。注意:在這代碼中當前最佳價值永遠是0。

代碼:

# include <iostream>
using namespace std;
/*
對於左右節點來說,左節點代表着不將物品裝入書包;
而右節點是將物品裝進書包,對於每個物品來說都有這兩種選擇
所以由此可以構成一個完全二叉樹結構 
*/
int N, W;            // N:物品總數,W:代表能承受的總量 
int a[201][2];                // 0:表示重量;1:表示價值 
int Vsum = 0, Wsum = 0;      // 統計總體積和總的物品價值
double bestV = 0.0 ;     // 表示最優的價值,也就是當前最佳組合的價值 
void sort(){
    // 冒泡排序,使單位重量價值最大的物品放在最前面 
    for(int i=1; i<=N; i++){
        for(int j=i+1; j<=N; j++){
            if(float(a[i][1] / (a[i][0]*1.0)) < float(a[j][1] / (a[j][0]*1.0))){
                int temp[1][2];
                temp[0][0] = a[i][0];
                temp[0][1] = a[i][1];
                a[i][0] = a[j][0];
                a[i][1] = a[j][1];
                a[j][0] = temp[0][0];
                a[j][1] = temp[0][1];
            }
        }
    }
         
}
double bound(int t){
    // 計算最優價值的函數,也是上界函數,其功能為剪枝
     int leftW = W - Wsum;    //剩余書包能裝下的重量
     double preV = Vsum;        // 當前的總價值
     while(t<=N && a[t][0]<= leftW){
         leftW -= a[t][0];
         preV += a[t][1];
         t++;
     } 
     if(t<= N){
         preV += (a[t][1]/(a[t][0]*1.0))*leftW;
     }
     return preV;
    
} 
void backtrack(int t){            // 這里t代表第幾個物品 
    if(t > N){        // 當t大於總物品的數量時,遞歸就結束了 
        bestV = Vsum;    // 最佳價值就等於目前的總價指數
        return ;    
    }
    // 計算右節點能不能裝下,能裝就裝 
    if(Wsum+a[t][0] <= W){        // 如果書包能裝的下就裝下去 
        Wsum += a[t][0];    // 每次從右節點開始累加,也就是每次都要選擇裝下物品
        Vsum += a[t][1];
        backtrack(t+1);         // 然后繼續深度搜索下一個節點
        Wsum -= a[t][0];        // 當下一次書包裝不下的時候而且他的總價值還不是最優的,把上一次裝的拿出來。 
        Vsum -= a[t][1];
    } 
    // 這里判斷左節點,因為在t這個位置已經判斷他不裝進去了,所以用上一時刻的最優價值加上t下一個物品的
    // 價值大於當前最優價值的話,就從左節點下去 
    if(bound(t+1) > bestV){
        backtrack(t+1);
    }
} 

void Print(){
    for(int i=1; i<=N; i++){
        cout<< a[i][0]<< " "<< a[i][1]<< endl;
    }
}
int main(){
    cin >> N >> W;
    for(int i=1; i<=N; i++)
        cin>> a[i][0]>> a[i][1];
    sort();
    backtrack(1);    // 回溯函數 
//    Print();
//    cout<< "bestV:"<< bestV<< endl;
    cout<< bestV<< endl;
    return 0;
} 
01背包-回溯法

三羊獻瑞(2015年藍橋杯第3題)

問題描述

鏈接:https://blog.csdn.net/softwareldu/article/details/45022413

觀察下面的加法算式:

      祥 瑞 生 輝
  +   三 羊 獻 瑞
-------------------
   三 羊 生 瑞 氣

其中,相同的漢字代表相同的數字,不同的漢字代表不同的數字。

答案:first:9567 second:1085 sum:10652

思路分析:

將祥瑞生輝和三羊獻瑞還有三羊生瑞氣用分別用字母表示祥瑞生輝用:abcd表示,三羊獻瑞用:efgb表示,三羊生瑞氣用:efcbh表示,注意相同的漢字使用相同的字母表示的,那現在用上面的框架來試試這個代碼是怎么寫的了。首先確定這里有8個字母,是用0-9 10個數字組成的,那么我們應該先定義一個一位數組里面存放這8個數據,和一些變量i=1,g什么的,按照模板上說,先來一個for,給一個限定條件,控制g的值(g是一個表示數據),那這個條件是什么呢?就是判斷當前的數是不是與之前的數相等因為數據是不能重復,重復了的話就設置g=0;否則設置g=1;如果g=1的話,則繼續下一步,判斷g&&i==7,如果這個條件滿足了的話就打印出數據,記住這是數組中的數,要進行處理在輸出;然后接下來則要繼續進行的就是判斷i是不是走到底了,也就是7,i==7就代表已經走到底部了,如果還是小於7的話就是代表還有數組沒有賦值,則在這里先i++(注意一定小於7,因為在下面有執行一遍i++了),然后在跟這個數據附一個初值,然后下面的就不用走了;接下來的一部f分就是回溯了,也就是當a[x]取得最大值的時候在這里是9,這時候a[x]增加不了了,並且這個時候i還是大於0的這個時候就要考慮是不是要退一步去更改上面的數據了,並且要用上while因為可能在回溯的時候不只是退一步,可能退幾步;在接下來就是判斷a[x]是不是達到最大值,並且這時候i==0了,如果是這種情況則,沒有可回溯的條件了,程序就可以退出來;如果那個條件沒有滿足的話則對a[x]進行自增,大致流程就是這樣。

總結詳細:

  1. 利用for對數據進行去重操作
  2. 當滿足條件i到達了底部時候,這時就可以進行輸出、標記操作了
  3. 當i沒有到達底部,就可以產生新數據了
  4. 判斷某個數據是不是到達了極限,也就是數據是不是到達最大值了,並且i是大於0的,滿足條件的話就可以進行回溯操作
  5. 當數據到達了極限並且i==0,程序就可以退出了
  6. 最后一步就是進行數據的自增了

代碼:

#include <iostream>
using namespace std;
int main(){
    int bcde,fegh,bcgei;
    int a[8];
    int i,k,g;
    i = 0;
    a[i] = 0;
    while(1){
        for(g = 1,k = i -1;k >=0;k--){
            if(a[i] == a[k]) g = 0;
        }
        if(g&& i==7){
            bcde = a[0]*1000 + a[1]*100 + a[2]*10 + a[3];
            fegh = a[4]*1000 + a[3]*100 + a[5]*10 + a[6];
            bcgei = a[0]*10000 + a[1]*1000 +a[5]*100 + a[3]*10 + a[7];
            if((a[0] !=0 && a[4] != 0)&&(bcde + fegh ==bcgei)){
                cout << bcde << " " << fegh << " " << bcgei << endl;
            }
        }
        if(g && i < 7){
            i++;
            a[i] = 0;
            continue;
        }
        while(a[i] == 9&& i>0) i--;
        if(a[i] == 9&&i ==0) break;
        else 
        a[i]++;
        
        
    }
     
    return 0;
}
三羊獻瑞

 

 參考資料:

百度百科

https://blog.csdn.net/qian2213762498/article/details/79420269 


免責聲明!

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



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