資料結構和演算法躬行記(5)——回溯演算法
回溯演算法(backtracking)是一個類似列舉的搜尋嘗試過程,在尋找問題解的過程中,當發現不滿足求解條件時,就退回一步,嘗試其它路徑,該演算法的時間複雜度都不會低於 O(N!),複雜的例題包括正則表示式匹配、解數獨等。
在《回溯演算法詳解》一文中提到,解決一個回溯問題,實際上就是一個決策樹的遍歷過程,需要思考三個問題:
(1)路徑:已經做出的選擇。
(2)選擇列表:當前可以做的選擇。
(3)結束條件:到達決策樹底層,無法再做選擇的條件。
下面是改編過的演算法通用結構。
function backtrack(路徑, 選擇列表): if 滿足結束條件 console.log(路徑) return for 選擇 of 選擇列表 做選擇 backtrack(路徑, 選擇列表) 撤銷選擇
面試題12 矩陣路徑和麵試題13 機器人運動範圍。在二維方格或矩陣的運動可用回溯法解決。
一、N皇后
N皇后是一道經典的回溯演算法題,將 n 個皇后放置在 n×n 的棋盤上,使皇后彼此之間不能相互攻擊,即每個棋子所在的行、列、對角線都不能有另一個棋子。
在下面的示例中,N是皇后的數量,backtrack()函式是回溯過程(如下所列),isValid()函式判斷是否符合選中條件。
(1)從第一個 row=0 開始。
(2)迴圈列並且試圖在每個 column 中放置皇后。
(3)如果方格 (row, column) 在攻擊範圍內,那麼跳過。
(4)在 (row, column) 方格上放置皇后,繼續尋找下一個位置。
(5)判斷 row 是否和皇后數量相同。
const N = 4; function backtrack(route, row) { if (row == N) { //結束條件 console.log(route); return; } for (let column = 0; column < N; column++) { if (!isValid(route, row, column)) continue; route[row] = column; //做選擇 backtrack(route, row + 1); //下一步 route[row] = null; //撤銷選擇(可省略) } } //從下往上 判斷row行column列放置是否合適 function isValid(route, row, column) { let leftup = column - 1, rightup = column + 1; for (let i = row - 1; i >= 0; i--) { // 逐行往上考察每一行 if (route[i] == column) // 第i行的column列有棋子 return false; if (leftup >= 0) { if (route[i] == leftup) // 考察左上對角線:第i行leftup列有棋子 return false; } if (rightup < N) { if (route[i] == rightup) // 考察右上對角線:第i行rightup列有棋子 return false; } leftup--; rightup++; } return true; }
二、0-1揹包
有一個揹包,揹包總的承載重量是 Wkg。現在有 n 個物品,假設每個物品的重量都不相等,並且不可分割。期望選擇幾件物品,裝載到揹包中。在不超過揹包容量的前提下,如何讓揹包中物品的總重量最大?
把物品依次排列,對於物品選擇裝或不裝,然後遞歸餘下的物品,如下所示。
let max = Number.MIN_VALUE, W = 100; function backtrack(route, goods) { let weight = route.length ? route.reduce((acc, cur) => acc += cur) : 0; if (weight == W || route.length == goods.length) { //結束條件 if (weight > max && weight <= W) { max = weight; } console.log(route); return; } for (let i = 0; i < goods.length; i++) { if(weight + goods[i] > W || route.indexOf(goods[i]) > -1) continue; route.push(goods[i]); //做選擇 backtrack(route, goods); route.pop(); //撤銷選擇 } }
三、全排列
全排列是指輸出給定數字序列的全部可能的排列,假設序列中的數字都是唯一的,利用回溯演算法枚舉出所有排列,如下所示。
function backtrack(route, nums) { if (route.length == nums.length) { //結束條件 console.log(route); return; } for (let i = 0; i < nums.length; i++) { if (route.indexOf(nums[i]) > -1) continue; route.push(nums[i]); //做選擇 backtrack(route, nums); route.pop(); //撤銷選擇 } }
面試題17 列印從 1~n 位的數。將問題轉換成數字排列,用遞迴實現。
&n