回溯算法¶
基本原理¶
回溯算法基于树的深度优先遍历
DFS:把全部的情况描述成一个树,不同的解决方法对应不同的子树。根据DFS,遍历到一个节点后继续向下遍历,如果某个路径无法满足条件,那么返回上一个节点。如果一个子树遍历完成,那么开始遍历另一个子树。
function check(node) {
// 需要一个函数判断当前情况是否满足
}
function DFS(root) {
// 这里使用递归思路:递归遍历一个子树的全部节点;满足后,返回这个节点
if (check(root)) {
// 如果满足一个节点,那么继续判断这个节点下面的节点
check(root.children)
} else {
// 如果不满足一个节点,那么回溯到上一个节点
break;
}
}
回溯算法核心代码
function backTrack(current, target, list) {
// 回溯结束条件:临时数组长度大于目标长度;临时数组不满足条件等
if (current.length === target) {
// 如果临时数组满足要求,需要把临时数组深拷贝一份,然后放到结果数组中
list.push([...current]);
return;
}
for (let i = 0; i < len; i++) {
// 全部数组中是否重复?是否排序?这样可以减少回溯的次数
if (current.includes(i)) {
continue;
}
// 把当前的情况增加一个
current.push(i);
// 继续回溯
backTrack(current, target, list);
// 回溯结束后,临时数组去掉这个元素,继续循环
current.pop();
}
}
实际使用¶
78 子集¶
https://leetcode-cn.com/problems/subsets/
/**
* @param {number[]} nums
* @return {number[][]}
* By Michael An
*/
var subsets = function(nums) {
const len = nums.length;
let list = [];
list.push([]);
// 处理特殊长度的数组
if (len === 0) {
return list;
} else if (len === 1) {
list.push(nums);
return list;
}
// 回溯
var backTrack = function(current, target, list) {
if (current.length === target) {
list.push([...current]);
return;
}
nums.forEach((i) => {
if (current.length === 0 || (!current.includes(i) && i > current[current.length - 1])) {
current.push(i);
backTrack(current, target, list);
current.pop();
}
});
}
// 处理长度大于1的数组的子集
list.push(nums);
// 排序,确保正序进入子序列
nums.sort((a, b) => a - b);
// 先循环设置子集的长度,然后回溯,满足长度的可以放入目标数组
for (let i = 1; i < len; i++) {
let target = i;
let current = [];
backTrack(current, target, list);
}
return list;
};
子集2¶
https://leetcode-cn.com/problems/subsets-ii/submissions/
/**
* @param {number[]} nums
* @return {number[][]}
* By Michael An
*/
var subsetsWithDup = function(nums) {
const len = nums.length;
let list = [];
list.push([]);
if (len === 0) {
return list;
} else if (len === 1) {
list.push(nums);
return list;
}
var backTrack = function(current, target, list, lastIndex) {
if (current.length === target) {
list.push([...current]);
return;
}
nums.forEach((i, index) => {
if ((current.length === 0 || i >= current[current.length - 1]) && index > lastIndex) {
current.push(i);
backTrack(current, target, list, index);
current.pop();
}
});
}
nums.sort((a, b) => a - b);
for (let i = 1; i < len; i++) {
let target = i;
let current = [];
backTrack(current, target, list, -1);
}
let dict = {};
for (let i = 1; i < list.length; i++) {
let key = list[i].toString();
if (dict[key]) {
list.splice(i, 1);
i--;
} else {
dict[key] = true;
}
}
list.push(nums);
return list;
};
数独问题¶
一个9*9的数独中,每一行,每一列都是9个数,然后每个小单元格中也是9个数。然后不断回溯。
C++方法,注释 by Michael An
#include<iostream>
using namespace std;
#define LEN 9
int a[LEN][LEN] = {0};
// 辅助函数:判断当前填充这个数,是否满足数独要求?
bool Isvaild(int count) {
int i = count / 9;
int j = count % 9;
// 首先根据一个数,计算余数和商,将一维数据转换成二维数组(i行j列是否满足)
// 检测行
for(int iter = 0; iter != j; iter++) {
if(a[i][iter] == a[i][j]) {
return 1;
}
}
//检测列
for(int iter=0; iter!=i; iter++) {
if(a[iter][j] == a[i][j]){
return 1;
}
}
//检测九宫
for(int p = i/3*3; p < (i/3+1)*3; p++){
for(int q = j/3*3; q < (j/3+1)*3; q++){
if(p == i && j == q) {
continue;
}
if(a[p][q] == a[i][j]) {
return 1;
}
}
}
return 0;
}
// 辅助函数: 打印满足要求的数独
void print() {
cout<<"数度的解集为"<<":"<<endl;
for(int i=0; i<9; i++) {
for(int j=0; j<9; j++) {
cout<<a[i][j]<<" ";
}
cout<<endl;
}
cout<<endl;
}
// 回溯函数
// 参数:当前填写的单元格的序号
void first_chek(int count){
// 如果已经填充了81个,那么直接打印数独(或者返回这个数独)
if (81 == count){
print();
return;
}
// 获取当前的序号所在的行列编号(i-j)
int i = count / 9; //列
int j = count % 9; //行
// 如果当前编号是空,那么
if(a[i][j]==0) {
// 遍历这个空单元格,然后进一步回溯
for(int n=1; n<=9; n++) {
a[i][j] = n;
if(!Isvaild(count)){ //这个值不冲突,回溯下一个单元格(DFS)
first_chek(count+1)
}
}
a[i][j] = 0;
}
// 如果当前编号已经有值,那么回溯下一个编号
else{
first_chek(count+1);
}
}
// 主函数
int main() {
// 设置数独中已有的数字
a[1][2] = 3;
a[5][3] = 9;
a[8][8] = 1;
a[4][4] = 4;
first_chek(0);
return 0;
}
这个思路可以解决复杂情况下,多步骤判断的情况。
8皇后问题¶
八皇后问题:需要把八个皇后放在国际象棋中,这八个皇后不能互相吃(不能在横线、竖线、斜线上)
JS 方法(实际测试有问题!)没看懂
// 初始化棋盘:每一个节点坐标是i,j,属性index表示是否有位置
// 棋盘总行(列)数 8*8(二维数组)
const n = 8;
// 8皇后的解法数
let iCount = 0;
// arr是长度为n*n的一维数组,保存着n*n个对象(li)并有各自的坐标
let arr = new Array(n ** 2);
// 默认index都为-1,表示没有被任何皇后标记过
// arr[ i*n + j ].y = i; arr[ i*n + j ].x = j; 每一个节点的属性
for(var i=0; i<n; i++){
for(var j=0; j<n; j++){
arr[i*n + j] = {};
arr[i*n + j].x = j;
arr[i*n + j].y = i;
}
}
//iQueen从0开始,即皇后0
function setQueen(iQueen){
if( iQueen == n ){
iCount++;
return;
}
for(var i=0; i<n; i++) {
if (arr[iQueen * n + i].index == -1 ) {
arr[iQueen * n + i].index = iQueen;
var x = arr[iQueen*n + i].x;
var y = arr[iQueen*n + i].y;
for(var j=0; j < arr.length; j++) {
if( arr[j].index == -1 && (arr[j].x == x || arr[j].y == y || arr[j].x - arr[j].y == x - y || arr[j].x + arr[j].y == x + y) ){
arr[j].index = iQueen;
}
}
//执行到这里,就会跳到下一层函数中,在执行完下一层的函数后,才会回溯到上一层继续执行for循环(此时的for循环是上一层的for循环),包括后面的所有代码
//需要注意的是,例如当前函数的iQueen=1,跳到下一层函数 iQueen=2,下一层函数执行完后,回溯到上一层,此时的执行环境已经是上一层的执行环境了,即iQueen是等于1,而不是等于2
//递归
setQueen(iQueen + 1);
//回溯
for(var j=0; j<arr.length; j++){
if(arr[j].index == iQueen ){
arr[j].index = -1;
}
}
}
}
}
setQueen(0);
console.log(arr);
n皇后问题¶
没看懂,有问题
function nQueens(n) {
var result = [];
var k = 0;
result[k] = 0;
while (k >= 0) {
//when k<0; there is no solution for this 'n'
result[k]++;
while (result[k] <= n && !place(result, k))
result[k]++;
//find proper position for the current queen
if (result[k] <= n) {
if (k == n - 1) break;
//the last queen is put at a proper position, end
else {
k++;
result[k] = 0;
//turn to next queen and init her position
}
} else {
result[k] = 0;
//before feedback, we should reset the position or it will influence next time we find proper position for her
k--;
}
}
return result;
}
//judge the current position is proper or not
//k is the serial number of the queen
//res is the array of a partial solution
function place(res, k) {
var abs = Math.abs;
for (var i = 0; i < k; i++) {
if (res[i] == res[k] || abs(res[i] - res[k]) == abs(i - k))
return false;
}
return true;
}
// 设置开始和结束时间(测试算法效率),n=30 计算
var start = Date.now();
var result = nQueens(30);
var end = Date.now();
console.log(result, end - start);
// [
// 1, 3, 5, 2, 4, 9, 11, 13, 15,
// 7, 23, 26, 28, 25, 22, 24, 30, 27,
// 29, 16, 12, 10, 8, 6, 18, 20, 17,
// 14, 21, 19
// ]
// node 环境执行时间:30845ms 浏览器执行时间:28664ms
这个方法实际测试可行