維基百科中對於回溯算法的定義:javascript
回溯法 採用試錯的思想,它嘗試分步的去解決一個問題。在分步解決問題的過程當中,當它經過嘗試發現現有的分步答案不能獲得有效的正確的解答的時候,它將取消上一步甚至是上幾步的計算,再經過其它的可能的分步解答再次嘗試尋找問題的答案。回溯法一般用最簡單的遞歸方法來實現,在反覆重複上述的步驟後可能出現兩種狀況:java
關鍵詞:深度優先、遞歸、嘗試各類可能、回退、遍歷(暴力解法)算法
共同點:編程
用於求解多階段決策問題。多階段決策問題即:數組
不一樣點:markdown
魯迅說過,算法學習,七分練,三分學。接下來就讓咱們一塊兒來康康常見的幾個題目吧~學習
leetcode 46:ui
輸入: [1,2,3]
輸出: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]編碼
回溯算法思路: 說明:spa
path
變量在尾部追加,而往回走的時候,須要撤銷上一次的選擇,也是在尾部操做,所以 path
變量是一個棧;使用編程的方法獲得全排列,就是在這樣的一個樹形結構中完成 遍歷,從樹的根結點到葉子結點造成的路徑就是其中一個全排列。因此,樹的最後一層就是遞歸終止條件。
代碼實現:
var permute = function(nums) {
let len = nums.length, res= [];
if(!len) return res;
let used = []; // boolean[]
let path = []; //number[]
dfs(nums, len, 0, path, used, res);
return res;
function dfs(nums, len, depth, path, used, res){
if(depth === len) {
//path是動態數組,不能直接push,須要拷貝一份當前值保存到結果中
res.push([...path]);
return;
}
// 針對全排列中下標爲depth的位置進行全部可能的嘗試
for(let i=0; i<len; i++){
if(!used[i]){
path.push(nums[i]);
used[i] = true;
// 往下找全排列中的下一個位置
dfs(nums, len, depth+1, path, used, res);
// 造成一個全排列後,進行回退,嘗試其餘答案
used[i] = false;
path.pop();
}
}
}
};
複製代碼
depth
,或者命名爲 index
,表示當前要肯定的是某個全排列中下標爲 index
的那個數是多少;used
,初始化的時候都爲 false
表示這些數尚未被選擇,當咱們選定一個數的時候,就將這個數組的相應位置設置爲 true
,這樣在考慮下一個位置的時候,就可以以 O(1) 的時間複雜度判斷這個數是否被選擇過,這是一種「以空間換時間」的思想。這些變量稱爲「狀態變量」,它們表示了在求解一個問題的時候所處的階段。須要根據問題的場景設計合適的狀態變量。
leetcode 51 如何將 n 個皇后放置在 n×n 的棋盤上,而且使皇后彼此之間不能相互攻擊
輸入:4
輸出:[ [".Q..", Q","Q...","..Q."], ["..Q.","Q...", "...Q", ".Q.."] ]
主對角線規律:x-y=k(行-列=固定值)
副對角線規律:x+y=k(行+列=固定值)
因此,當某個位置放下一個皇后以後,記錄當前行+列的值,和行-列的值,此後的位置若是行+列或行-列有與數組中重複的,都不可以使用。
代碼實現:
var solveNQueens = function(n) {
if(n==0) return res;
let col = [], main = [], sub = []; // boolean[]
let res = []; // string[]
let path = []; //number[]
dfs(0, path);
return res;
function dfs(row, path){
// 深度優先遍歷到下標爲 n,表示 [0.. n - 1] 已經填完,獲得了一個結果
if(row == n){
const board = convert2board(path);
res.push(board);
return;
}
// 針對下標爲 row 的每一列,嘗試是否能夠放置
for(let j=0; j<n; j++){
if(!col[j] && !main[row-j+n-1] && !sub[row+j]){
path.push(j);
// 記錄該位置的攻擊範圍
col[j] = true;
main[row-j+n-1] = true; //加n-1是爲了防止數組索引爲負數
sub[row+j] = true;
// 進入下一行
dfs(row+1, path);
// 回溯, 去掉path中最後一個值,嘗試其餘選項
col[j] = false;
main[row-j+n-1] = false;
sub[row+j] = false;
path.pop();
}
}
}
// 輸出一個結果
function convert2board(path){
let board = []; // string[]
for(let i=0; i<path.length; i++){
let ret = new Array(n).fill('.');
ret[path[i]] = 'Q';
board.push(ret.join(''))
}
return board;
}
};
複製代碼
經過以上幾個問題不難發現,回溯算法解題的要點就是要定義好狀態變量,不斷對狀態進行推動、回退,嘗試全部可能的解,並在狀態處於遞歸樹的葉子節點時輸出此時的方案。
// 簡單模版
function backtrack(){
let res = [];
let used = [];
function dfs(depth, path){ // depth表示當前所在的階段
// 遞歸終止條件
if(depth === len){
res.push(path);
return;
}
// 針對當前depth嘗試全部可能的結果
for(let i=0; i<len; i++){
if(!used[i]){ // 此路不通的標記
path.push(nums[i]);
used[i] = true;
// depth+1 前往下一個階段
dfs(depth+1, path);
// 重置本階段狀態,嘗試本階段的其餘可能
used[i] = false;
path.pop();
}
}
}
}
複製代碼