回溯算法和解數獨

  之前自學數據結構和算法的時候,回溯算法一直沒涉及到,當時只聽過,也沒用過,這兩天看到一個數獨問題的博客,看下來竟然一臉懵逼,這確定是不能接受的,因此一氣呵成把回溯算法好好品了品,趕忙記下來,鞏固一下。git

  回溯算法,簡單來講,其實就是對解空間的一種深度優先搜索(DFS:Depth-First-Search),採用遞歸的方式,選擇方式就是遞歸樹模型,每次作出選擇並記錄,當進行到某一步,若是因爲約束條件限制,沒法繼續進行時,就退回到上一步從新進行選擇,直到找到知足要求的解,這就是回溯算法。算法

  直接上題,作兩題就理解了:數據結構

  • leetcode17 電話號碼的字母組合

  給定一個僅包含數字 2-9 的字符串,返回全部它能表示的字母組合。給出數字到字母的映射以下(與電話按鍵相同)。注意 1 不對應任何字母。機器學習

  

 

 

 

    //每個數字與所能表明的字母的映射關係
    vector<string> list = {
        "abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
    };

        
    vector<string> res;

    void func(const string & digits , int index , const string& s){
        //設置終止條件
        if(index == digits.size()){
            res.push_back(s); //保存一種組合
            return ;
        }

        char c =digits[index];
        string letters = list[c - '2']; 

        for(int i = 0 ;i < letters.size();++i){    
func(digits , index + 1 , s + letters[i]);  
      }
}

  這題其實沒有體現回溯的步驟,主要是採用了值傳遞和const引用的一些技巧,並且沒有約束條件限制,其實就是進行了一次所有解空間的遞歸遍歷;數據結構和算法

  • leetcode46 全排列

  給定一個 沒有重複 數字的序列,返回其全部可能的全排列。函數

 

 

  

    vector<vector<int>> res;
    void func(const vector<int>& nums , int  index ,vector<int>& one_res , vector<bool>& mark){
        //設置終止條件
        if(index == nums.size())  {
            res.push_back(one_res);
            return;
        }
        
        for(int i = 0 ; i < nums.size() ; ++i){
            if(!mark[i]){ //若是i沒有被使用
                one_res.push_back(nums[i]);
                mark[i] = true;     //標記          
                func(nums , index+1 , one_res , mark);
                one_res.pop_back();
                mark[i] = false;   //回溯
            }
        }
    }

 

 
  • leetcode77 組合

  給定兩個整數 nk,返回 1 ... n 中全部可能的 k 個數的組合。學習

    vector<vector<int>> res;

    void func( const int& n , const  int& k , int start , vector<int>& p){
        if(p.size() == k){
            res.push_back(p);
            return ;
        }

        //start=i,此時還須要k-p.size()個元素完成操做,從i到N的閉區間中選擇k-p.size()個元素
        //i最大爲n-(k-p.size())+1
        //i從start開始,由於包含start以前數字的解已經包含在res中了,好比[1,4]和[4,1]就是同一個解,避免重複
        for(int i = start  ; i <= n-(k-p.size())+1; ++i){
            p.push_back(i);
            func(n , k , i + 1 , p);
            p.pop_back();
        }
    }

  上面這幾題都比較簡單,若是弄懂了這幾題,應該對回溯算法就有了必定的理解了,上面這幾題對於每一步遞歸選擇都沒有使用約束,接下來咱們來考慮帶約束的遞歸選擇;大數據

  • leetcode51 N皇后問題(最出名的就是八皇后問題了)

 

 

 

 

class Solution {

    vector<vector<string>> res;
    vector<bool> col;
    vector<bool> diag1;
    vector<bool> diag2;

    vector<string> transteranswer(int n ,vector<int>& row){
        vector<string> oneway (n,string(n,'.'));

        for(int i = 0 ; i < n ; ++i){
            oneway[i][row[i]] = 'Q';
        }

        return oneway;
    }

    void func(int n ,int index, vector<int>& row){
        if(index == n){
            res.push_back(transteranswer(n,row));
            return ;                
        }

        for(int i = 0 ;i < n; ++i){
            if(!col[i] && !diag1[index+i] && !diag2[index-i+n-1]){//判斷是否能夠放置皇后
                col[i] = true;
                diag1[index+i] = true;
                diag2[index-i+n-1] = true;  //這三步是標記,代表這一列和兩條斜對角線都佔用
                row.push_back(i);
                func(n , index+1 , row);
                col[i] = false;
                diag1[index+i] = false;
                diag2[index-i+n-1] = false;
                row.pop_back(); //這三步是表示選擇錯誤,回溯
            }
        }

        return ;
    }
public:
    vector<vector<string>> solveNQueens(int n) {
        if(n == 0 ) return res;
        col=vector<bool>(n,false);
        diag1 = vector<bool>(2*n-1,false);
        diag2 = vector<bool>(2*n-1,false);
        vector<int> row;

        func(n,0,row);

        return res;

    }
};

  此次貼上了完整代碼,看着很長,其實思路很簡單:人工智能

  1. 遍歷每一行全部位置,嘗試在每個位置放置皇后
  2. 檢查皇后放置的位置和以前放置的皇后是否有衝突
  3. 若是沒有衝突,跳轉到下一行繼一、2操做,直到遍歷完最後一行,結束

  其實N皇后問題的核心是檢查該點是否有衝突,這裏引入了三個vector<bool>進行標記,col=vector<bool>(n,false)表示的是該列是否被佔用,diag1和diag2分別表示所在對角線是否被佔用,這裏使用了一點小技巧,位於正對角線上的座標,其橫縱座標差值都相等,位於反對角線上的座標,其橫縱左邊相加的和都相等;spa

  • leetcode37 解數獨

  其實數獨的思路和N皇后問題十分類似,先上代碼:

  

class Solution {

    char list[9] = {'1','2','3','4','5','6','7','8','9'};

    bool check(vector<vector<char>>& board, int x, int y ,char ch){
        //檢查第x行中無重複
        for(int i = 0 ; i <9 ; i++){
            if(board[x][i] == ch){
                return false;
            }
        }

        //檢查第y行中無重複
        for(int i = 0 ; i < 9 ; i++){
            if(board[i][y] == ch){
                return false;
            }
        }

        //檢查座標(x,y)所位於的3*3中無重複
        for(int  i = 3*(x/3) ; i <3*(x/3+1);++i){
            for(int  j = 3*(y/3) ; j <3*(y/3+1);++j){
                if(board[i][j]==ch){
                    return false;
                }
            }
        }

        return true;
    }

    bool func(vector<vector<char>>& board,  int num ){
        if(num == 81) return true ;
      
        int i = num / 9;    //當前行數
        int j = num % 9;    //當前列數

        if(board[i][j] != '.'){
            if(func(board , num+1)) return true;                
        }else{
            for(int k = 0 ; k < 9; k ++){
                if(check(board, i , j , list[k])){
                    board[i][j] = list[k] ;
                    if(func(board , num+1 )) return true;
                    board[i][j] ='.';
                }
            }
        }

        return false;

    }

public:
    void solveSudoku(vector<vector<char>>& board) {
        func(board , 0);
    }
};

  總體思路仍是很簡單的(小技巧:使用num來記錄遍歷深度,同時也能夠用num計算出此時的座標);

  1. 依次遍歷表格上全部的點;
  2. 檢查是否知足約束條件(行、列以及3*3的表格內均不含有該數字),若是知足,遞歸到下一個座標點,若是不知足,回溯;
  3. 遍歷完成結束。
  • 總結

  其實最先接觸遞歸的時候,就考慮過是否能夠將每一次遞歸記錄下來,後來學習動態規劃的時候,又考慮是否能對遞歸樹進行剪枝操做,減小遞歸複雜度;而回溯法必定程度就完成了這兩步操做,回溯算法其實就是對解空間進行的一次全局搜索,在必定的約束處理手段下,能夠完成剪枝操做,縮減算法的複雜度;可是不可避免的,這種算法的核心仍是遞歸,算法複雜度較高,而這種思想也是傳統人工智能的思想,依靠的是算力,與如今大火的機器學習、人工智能不同,依靠的是計算機+大數據+統計學;

  最後就是回溯算法完成的一些細節的地方:

  1. 遞歸終止條件:這是全部使用遞歸的前提,必須考慮清楚遞歸終止條件;
  2. 解的保存形式:好比組合問題,須要的是全部解的集合,因此在終止時,將一個解push_back到res裏面進行保存,而解數獨問題,只須要找到一個解,全部將每一次遞歸函數的返回值設置爲bool型,若是找到正確解,就逐層返回true,結束遞歸;
相關文章
相關標籤/搜索