算法研討會-含有回溯的遞歸算法設計探討

含有回溯的遞歸程序設計

目錄

回溯

1.1 概念

  • 遞歸是一種算法結構、技巧,而回溯是一種算法思想。
  • 本質上是一種枚舉思想,採用深度優先策略來枚舉全部可能解,而且服從必定的擇優條件。
  • 遵循設定好的擇優條件不斷深刻試探,最終達到目標,可是在試探過程當中,若發現當前狀況不是最優或者必定沒法達到目標時,將返回到上一個狀態,對上一個狀態進行從新選擇後,繼續深刻試探。

1.2 基本思路

從一條路往前走,能進則進,不能進則退回到最近的岔路,換一條路再試。算法

1.3 問題舉例

1.3.1 N皇后問題

  • 要求

在N × N的棋盤上放置N個皇后,這些皇后之間不能相互攻擊。(合法位置)編程

  • 思路(不考慮優化)
    • 遞歸:繼續深刻試探下一行的每一列。
    • 遞歸邊界:N個皇后所有擺放完成。
    • 回溯條件:當前皇后已經不存在合法位置(越界)。
    • 具體思路:
      1. 若是可以將前面全部的皇后放置在合法位置,那麼就繼續試探下一個皇后,從第1個皇后開始依次類推。
      2. 若是第k個皇后已經不存在合法位置(越界),那麼判斷第k-1個皇后是否還存在其餘合法位置,若存在,則移動到下一個合法位置,從新試探第k個皇后;若不存在,則重複步驟2。
      3. 第N個皇后同理,只是在第N個皇后放置在合法位置後,到達了遞歸邊界,須要輸出可行解,而後重複步驟2。
  • 回溯過程圖解

(圖片轉自博客園,目前未通過做者贊成,若有侵權,將當即刪除!)函數

  • 核心代碼
void Put_The_Queen_On_The_NOk_Row_And_The_NOj_column(int k, int n) {//須要擺放n個皇后,當前擺放到了第k行。
    int j;
    if(k > n)//發現可行解
        print(n); //輸出可行解
    else {
         for(j = 1;j <= n;j++) {//試探這一行的每一列
            if(Find_The_Valid_Pos(k, j)) {//判斷該位置是否合法
                q[k] = j;   //保存位置
                Put_The_Queen_On_The_NOk_Row_And_The_NOj_column(k + 1, n);  //繼續試探下一行
            }
        }
    }
}

1.4 算法設計

  1. 清楚遞歸邊界(何時中止遞歸)
  2. 清楚遞歸函數的功能(參數的功能、返回值的功能)
  3. 回溯算法設計基本思路
    1. 只要當前層還存在合法選項,那麼就在保證當前層合法的前提下,進入試探下一層。
    2. 若是當前層已經不存在合法選項,那麼就回溯到上一層,並重復判斷1./2.。
    3. 若是發現可行解,那麼重複判斷1./2.。

1.5 PTA編程題

1.5.1 整數分解爲若干項之和

  • 要求

將一個正整數N分解成幾個正整數相加,能夠有多種分解方法,例如7=6+1,7=5+2,7=5+1+1,…。編程求出正整數N的全部整數分解式子。優化

  • 思路設計

    • 遞歸:
    • 遞歸邊界:當前result(填入的全部數之和)與N(目標值)相等
    • 回溯條件:當前result(填入的全部數之和)與N(目標值)相等(在此以後不可能再出現可行解,繼續試探沒有意義)。
    • 具體思路:
      1. 每個位置的值,均從1開始試探。
    1. 若是當前的result(已經填入的全部數之和)仍小於N(輸入的目標值),那麼就使 第n個數 從 上一個數的數值 往上 試探。
      1. 若是當前result與N相等,到達遞歸邊界,輸出可行解,而且回溯到上一個狀態(此時只有n-2個數),繼續使第n-1個數自加,並判斷result與N的關係。
      2. 依次類推,直到遍歷全部可行解。
  • 核心代碼code

void Division(int x, int pos, int result){
    static int counter, array[32];
    if(result != N) {
        for (int i = x; result + i - 1 < N; i++) {
            array[pos] = i;
            Division(i, pos + 1, result + i);
        }
    }
    else{
        counter++;
        std::cout << N << '=';
        for (int i = 0; i < pos - 1; i++)
            std::cout << array[i] << '+';
        if (counter % 4 == 0 || array[pos - 1] == N)
            std::cout << array[pos - 1] << std::endl;
        else
            std::cout << array[pos - 1] << ';';
    }
}

1.5.2 輸出全排列

  • 要求blog

    • 輸出前n個正整數的全排列(n<10)。
    • 排列的輸出順序爲字典序,即序列a1,a2,⋯,an排在序列b1,b2,⋯,bn以前,若是存在k使得a1=b1,⋯,ak=bk 而且 ak+1<bk+1。
  • 思路遞歸

    • 這題與N皇后問題更加相似,並且運行流程更加簡單。由於回溯只會出如今發現可行解以後,即試探直到發現可行解的過程不會被回溯打斷。
    • 遞歸:試探下一位的數字,而且判斷擬填入的數字是否被用過
    • 遞歸邊界:全排列數的長度與N(目標值)相等。
    • 回溯條件:擬填入的數字已經所有被使用過了。
    • 具體思路(「合法」即當前層待填入的數字還未被使用過 )
      1. 只要當前層還存在合法選項,那麼就在保證當前層合法的前提下,進入試探下一層。
      2. 若是當前層已經不存在合法選項,那麼就回溯到上一層,並重復判斷1./2.。
      3. 若是發現可行解,那麼就重複判斷1./2.。
  • 注意圖片

    輸入的目標值與全排列數的位數始終是一致的。

  • 核心代碼

/*全局變量*/
int whole_array[32]; // 存儲當前的全排列數
int sub[32]; //記錄每個數字是否被用過
int N; //目標值

/*遞歸函數*/
void Perm (int x){
    static int length = 0; //當前全排列的長度
    if(N <= x - 1){ //判斷全排列樹的長度是否等於目標值
        for(int i = 1;i <= N;i++) //輸出
            printf("%d",whole_array[i]);    
        printf("\n");
    }
    else //只要全排列數的長度小於目標值
        for(int i = 1;i <= N;i++) //因爲要輸出字典序,每個位置從1開始試探
            if(sub[i] == 0){ //判斷這個數是否被用過
                whole_array[x] = i; //將這個數
                sub[i] = 1; //填入以後將這個數標記爲1,即在該全排列數中已經出現
                Perm(x + 1); //到下一個位置繼續試探
                sub[i] = 0; //若是發生回溯,那麼須要從新將這個數字標記爲沒有出現過
            }
}

(博客內容爲原創,未經容許禁止轉載!)

相關文章
相關標籤/搜索