维基百科中对于回溯算法的定义: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();
}
}
}
}
复制代码