之前自學數據結構和算法的時候,回溯算法一直沒涉及到,當時只聽過,也沒用過,這兩天看到一個數獨問題的博客,看下來竟然一臉懵逼,這確定是不能接受的,因此一氣呵成把回溯算法好好品了品,趕忙記下來,鞏固一下。git
回溯算法,簡單來講,其實就是對解空間的一種深度優先搜索(DFS:Depth-First-Search),採用遞歸的方式,選擇方式就是遞歸樹模型,每次作出選擇並記錄,當進行到某一步,若是因爲約束條件限制,沒法繼續進行時,就退回到上一步從新進行選擇,直到找到知足要求的解,這就是回溯算法。算法
直接上題,作兩題就理解了:數據結構
給定一個僅包含數字 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引用的一些技巧,並且沒有約束條件限制,其實就是進行了一次所有解空間的遞歸遍歷;數據結構和算法
給定一個 沒有重複 數字的序列,返回其全部可能的全排列。函數
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; //回溯 } } }
給定兩個整數 n 和 k,返回 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(); } }
上面這幾題都比較簡單,若是弄懂了這幾題,應該對回溯算法就有了必定的理解了,上面這幾題對於每一步遞歸選擇都沒有使用約束,接下來咱們來考慮帶約束的遞歸選擇;大數據
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; } };
此次貼上了完整代碼,看着很長,其實思路很簡單:人工智能
其實N皇后問題的核心是檢查該點是否有衝突,這裏引入了三個vector<bool>進行標記,col=vector<bool>(n,false)表示的是該列是否被佔用,diag1和diag2分別表示所在對角線是否被佔用,這裏使用了一點小技巧,位於正對角線上的座標,其橫縱座標差值都相等,位於反對角線上的座標,其橫縱左邊相加的和都相等;spa
其實數獨的思路和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計算出此時的座標);
其實最先接觸遞歸的時候,就考慮過是否能夠將每一次遞歸記錄下來,後來學習動態規劃的時候,又考慮是否能對遞歸樹進行剪枝操做,減小遞歸複雜度;而回溯法必定程度就完成了這兩步操做,回溯算法其實就是對解空間進行的一次全局搜索,在必定的約束處理手段下,能夠完成剪枝操做,縮減算法的複雜度;可是不可避免的,這種算法的核心仍是遞歸,算法複雜度較高,而這種思想也是傳統人工智能的思想,依靠的是算力,與如今大火的機器學習、人工智能不同,依靠的是計算機+大數據+統計學;
最後就是回溯算法完成的一些細節的地方: