1. 程式人生 > >回溯演算法(Backtracking)說明與例項

回溯演算法(Backtracking)說明與例項

定義

回溯演算法(Backtracking)在很多場景中會使用,如N皇后,數迷,集合等,其是暴力求解的一種優化。參考https://en.wikipedia.org/wiki/Backtracking 中的說明,定義如下:

Backtracking is a general algorithm for finding all (or some) solutions to some computational problems, notably constraint satisfaction problems, that incrementally builds candidates to the solutions, and abandons each partial candidate c (“backtracks”) as soon as it determines that c cannot possibly be completed to a valid solution

從上文中可以得出, 核心的含義是 回溯演算法是通過一步一步(通常是用遞迴)構建可能”解”,並且回溯不可能”解”來求所有或者部分解決方案的通用演算法。其中“回溯”的具體意思就是將不可能解或者部分解的候選儘早的捨棄掉,“解”需要滿足一定的限制條件(constraint satisfaction)

通用演算法

ALGORITHM try(v1,...,vi)  // 這裡的V1.....V2攜帶的引數說明 “可能解”  
   // 入口處驗證是否是全域性解,如果是,直接返回。 
   // 實際程式設計中也需要檢視是否是無效解,如果是,也是直接返回
   IF (v1,...,vi) is a solution THEN RETURN (v1,...
,vi) FOR each v DO // 對於每一個可能的解,進行檢視 // 下面的含義是形成一個可能解 進行遞迴 IF (v1,...,vi,v) is acceptable vector THEN sol = try(v1,...,vi,v) IF sol != () THEN RETURN sol // 這個地方其實需要增加“回溯” 處理,實際程式設計中通常是函式引數的變化 END END RETURN ()

經典演算法

  • N皇后問題

這個是一個比較經典的問題, 意思是將N個“皇后”放在N*N的棋盤上,每個皇后不能再同一列,同一行,和斜對角 (這個就是限制條件constraint satisfaction)。 下面是回溯演算法的需求所有的可能解。
演算法基本的步驟思想為:
1)從第一行開始
2 )如果所有的皇后已經放置完成, 生成解,並且返回true
3)嘗試當前行的所有列,如果當前行與列是合法的
3.1 修改棋盤讓其成為部分解,
3.2 然後遞迴檢視(主要是2, 3,4)該解是否合法
3.3 Backtrack 棋盤進行回溯
4) 如果上述所有的組合都為非法,返回false

    // 尋找N皇后問題的可能解, 我們用 '.' 表示不放置皇后,用'Q'表示放置皇后
    // 利用vector<string> 表示一個解決方案, vector<vector<string>> 表示                                 
    // 所有的解決方案。
    // 在遞迴初,我們可以生成一個待解的棋盤
    vector<vector<string>> solveNQueens(int n) {
        string tmp (n, '.');
        //生成一個N*N待解的棋盤,沒有任何皇后
        vector<string> broad (n, tmp);
        nQueue = n;
        vector<vector<string>> ans;
        solveNQueensHelper (ans, broad, 0, nQueue );
        return ans;
    }
    // 回溯演算法的遞迴函式,
    bool solveNQueensHelper (vector<vector<string>>& ans, vector<string> &broad, int row, int nQueue)
    {
         // 如果當前的行數大於或者等於皇后數,說明當前棋盤是一個解
         // 直接返回
        if(row >= nQueue){
            ans.push_back(broad);
            return true;
        }
        // 從當前行的列中選取一個可能解
        for (int column = 0; column < nQueue; column++){
            // 檢視一下,當前可能解是否有效,只有有效,才可能繼續遞迴
            if(isOk (broad, row, column))
            {
                // 有效,修改可能解的棋盤
                broad[row][column] = 'Q';
                // 遞迴呼叫是否可能解
                if (solveNQueensHelper (ans, broad, row + 1, nQueue)){
                   // return true;
                }
                // 回溯, 去生成其他解
                broad[row][column] = '.';
            }
        }

        return false;
    }
    // 檢視當前部分解是否有效
    bool isOk (const vector<string> &broad, int row, int column){
        // 檢視
        for (int i = 0; i <= row; i++)
        {
            if(broad [i] [column] == 'Q'){
                return false;
            }
        }

        int tmpRow = row;
        int tmpColumn = column;
        while (tmpRow >= 0 && tmpColumn >= 0)
        {
            if(broad [tmpRow] [tmpColumn] == 'Q'){
                return false;
            }
            tmpRow--,
            tmpColumn--;
        }

        tmpRow = row;
        tmpColumn = column;
        while (tmpRow >= 0 && tmpColumn < nQueue)
        {
            if(broad [tmpRow] [tmpColumn] == 'Q'){
                return false;
            }
            tmpRow--,
            tmpColumn++;
        }
        return true;    
    }
};

其要求,1-9的數不能出現在同一列,同一行,或者同一個3*3的格子中。

演算法思想如下:
1 )找到一個空格子,如果找不到, 說明已經填滿,這是一個解,返回true
2 )從1 - 9 的數字中填上找到的空格子,如果合法
2.1 生成部分解,遞迴呼叫 1)2) 3)
2.2 如果遞迴呼叫返回true,說明合法
2.3 回溯部分解
3)上述都不成,直接返回false

c++的程式碼實現如下:

    // board 是一個9*9的格子,如果空格子,用'.' 表示,否則為1-9的數字
    void solveSudoku(vector<vector<char>>& board) {    
        // 遞迴呼叫該函式
        helperSolveSudoku (board);
    }

    bool helperSolveSudoku(vector<vector<char>>& board) {   
      int row = 0;
      int colum = 0;
      // step1, 獲取一個空格, 如果沒找到,說明已經填滿。
      if ( !GetNextUnsignedOne (board, row, colum) )
         return true;
       // Step2: 從1-9 分別填上棋盤
      for (char num = '1'; num <= '9'; num++ ){
         // 檢視當前的數字放入棋盤中是否合法
         if (isNumOk (board, row, colum, num) ){
            // step2.1 生成部分解,遞迴呼叫
            board[row][colum] = num;
            if (helperSolveSudoku (board) )
            {
               // 如果解合法,直接返回 
               return true;
            }
            // 回溯部分解
            board[row][colum] = '.';
         }
      }
      // step3 沒有解,直接返回false
      return false;
    }
    // 幫助函式以獲取一個可能的空位置 
    bool GetNextUnsignedOne(const vector<vector<char>>& board, int &row, int& column){
        for(int i = 0; i < 9; i++){
            for(int j = 0; j < 9; j++){
                if (board [i] [j] == '.'){
                    row = i;
                    column = j;
                    return true;
                }
            }
        }
        return false;
    }

    // 幫助函式以驗證數字在當前是否合法
    bool isNumOk (const vector<vector<char>>& board, int row, int column, char num){
        // check the row is ok or not
        for(int i = 0; i < 9; i++){
            if(board [row] [i] == num){
                return false;
            }
        }
        // check the colum is ok or not
        for(int i = 0; i < 9; i++){
            if(board [i] [column] == num){
                return false;
            }
        }

        int startRow = row - row % 3;
        int startcolumn = column - column %3;     
        // check the 3*3 inbox is ok for not.
        for (int i = 0; i < 3; i++){
            for(int j = 0; j < 3; j++)
            {
                if(board [startRow + i] [startcolumn + j] == num){
                    return false;
                }
            }
        } 
        return true;   
    }
  • 集合問題
    有一個數組,產生一個集合,該集合能包含所有的元素,如 【1,2,3】陣列可以滿足的集合就是【1,2,3】【1,3,2】【2,1,3】【2,3,1】【3,1,2】【3,2,1】.
    這個也可以用回溯的方式實現
    基本演算法思想為:
    1) 檢視當前的集合,是否滿足所有元素的集合,如果滿足,返回
    2) 從陣列挑選一個元素,檢視當前的元素是否屬於部分解
    2.1 如果不屬於部分解,將元素加入到部分解
    2.2 遞迴呼叫 1, 2, 3
    2.3 回溯該元素
    3 陣列元素完成,直接返回
    具體實現的C++程式碼如下:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> ans;
        vector<int> tmpRes;
        permuteHelper (ans, tmpRes, nums);
        return ans;

    }
    // 幫助函式以實現資料集合功能
    // ans 表示幾個陣列
    // tmpRes 表示部分解
    // nums 表示的當前陣列元素個數
    void permuteHelper(vector<vector<int>>& ans, vector<int>&tmpRes, vector<int>& nums){
       // step1 如果部分解的元素個數和數字的元素相同,直接插入
       if(tmpRes.size () == nums.size ()){
           ans.push_back (tmpRes);
           return;
       }  
        //step2 從集合中選定一個元素     
        for(int i = 0; i < nums.size(); i++) 
        {
            if(std::find (tmpRes.begin(), tmpRes.end(), nums [i]) == tmpRes.end()){
                //step 2.1 如果部分解不包含該元素,
                // 其實也可以用一個set<int> 來判定是否含有該元素。
                tmpRes.push_back (nums [i]);
                //step 2.2 遞迴呼叫
                permuteHelper (ans, tmpRes, nums);
                // 回溯部分解
                tmpRes.pop_back();
            }
            else{
                continue;
            }
        }
        // Step3,完成,直接返回
        return;
    }