1.HTML 編寫規則 和 語義化寫法
回溯演算法解題套路框架
其實回溯演算法其實就是我們常說的 DFS 演算法,本質上就是一種暴力窮舉演算法。
解決一個回溯問題,實際上就是一個決策樹的遍歷過程。
站在回溯樹的一個節點上,你只需要思考 3 個問題:
1、路徑:也就是已經做出的選擇。
2、選擇列表:也就是你當前可以做的選擇。
3、結束條件:也就是到達決策樹底層,無法再做選擇的條件。
回溯演算法框架:
result = [] def backtrack(路徑, 選擇列表): if 滿足結束條件: result.add(路徑) return for 選擇 in 選擇列表: 做選擇 backtrack(路徑, 選擇列表) 撤銷選擇
其核心就是 for 迴圈裡面的遞迴,在遞迴呼叫之前「做選擇」,在遞迴呼叫之後「撤銷選擇」
一、全排列問題
力扣第 46 題「 全排列」就是給你輸入一個數組 nums
,讓你返回這些數字的全排列。
解題程式碼:
class Solution { public: //回溯 vector<vector<int>>res;//儲存所有可能排列 vector<vector<int>> permute(vector<int>& nums) { vector<bool>used(nums.size(),false);//記錄是否走過這個數 vector<int>track;//記錄路徑 backtrack(nums,track,used); return res; } void backtrack(vector<int>nums,vector<int>&track,vector<bool>&used){ //結束條件 if(track.size()==nums.size()){ res.push_back(track); return; } //遍歷 for(int i=0;i<nums.size();i++){ if(used[i]==true){//如果這個數已經被選擇過 continue; } used[i]=true;//選擇這個數 track.push_back(nums[i]);//記入路徑 backtrack(nums,track,used);//對其他數進行遍歷 used[i]=false;//撤銷選擇 track.pop_back(); } } };
本質上就是暴力窮舉,要深入理解【決策樹】這一概念。在這裡不做深入解釋,想要了解可以去回溯演算法解題套路框架 :: labuladong的演算法小抄 (gitee.io)自行了解。
注意:
但是必須說明的是,不管怎麼優化,都符合回溯框架,而且時間複雜度都不可能低於 O(N!),因為窮舉整棵決策樹是無法避免的。這也是回溯演算法的一個特點,不像動態規劃存在重疊子問題可以優化,回溯演算法就是純暴力窮舉,複雜度一般都很高。
二、N 皇后問題
力扣第 51 題「 N 皇后」就是這個經典問題,簡單解釋一下:給你一個 N×N
的棋盤,讓你放置 N
個皇后,使得它們不能互相攻擊。
class Solution { public: vector<vector<string>>res; vector<vector<string>> solveNQueens(int n) { vector<string>board(n,string(n,'.')); backtrack(0,board); return res; } void backtrack(int row,vector<string>&board){ //結束條件 if(row==board.size()){//當選擇完的行數等於棋盤的行數時 res.push_back(board); return; } //遍歷 一行中的每個數也就是改變列即可 for(int col=0;col<board.size();col++){ //除去不合法的選擇 if(!isValid(board,row,col)){ //不符合遊戲規則 直接跳過 continue; } //符合規則選擇 board[row][col]='Q'; //對下一行進行選擇 backtrack(row+1,board); //撤銷選擇 board[row][col]='.'; } } bool isValid(vector<string>&board,int row,int col){ int n=board.size(); //檢查一列中是否有衝突 for(int i=0;i<row;i++){ if(board[i][col]=='Q')return false; } //檢查右上是否有衝突 for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++){ if(board[i][j]=='Q')return false; } //檢查左上是否有衝突 for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) { if (board[i][j] == 'Q') return false; } return true; } };
PS:肯定有讀者問,按照 N 皇后問題的描述,我們為什麼不檢查左下角,右下角和下方的格子,只檢查了左上角,右上角和上方的格子呢?
因為皇后是一行一行從上往下放的,所以左下方,右下方和正下方不用檢查(還沒放皇后);因為一行只會放一個皇后,所以每行不用檢查。也就是最後只用檢查上面,左上,右上三個方向。
三、最後總結
回溯演算法就是個多叉樹的遍歷問題,關鍵就是在前序遍歷和後序遍歷的位置做一些操作,演算法框架如下:
def backtrack(...):
for 選擇 in 選擇列表:
做選擇
backtrack(...)
撤銷選擇
寫 backtrack
函式時,需要維護走過的「路徑」和當前可以做的「選擇列表」,當觸發「結束條件」時,將「路徑」記入結果集。
其實想想看,回溯演算法和動態規劃是不是有點像呢?我們在動態規劃系列文章中多次強調,動態規劃的三個需要明確的點就是「狀態」「選擇」和「base case」,是不是就對應著走過的「路徑」,當前的「選擇列表」和「結束條件」?
某種程度上說,動態規劃的暴力求解階段就是回溯演算法。只是有的問題具有重疊子問題性質,可以用 dp table 或者備忘錄優化,將遞迴樹大幅剪枝,這就變成了動態規劃。而今天的兩個問題,都沒有重疊子問題,也就是回溯演算法問題了,複雜度非常高是不可避免的。