目录
框架:排列/组合/子集
元素无重不可复选
全排列
子集
组合:[1, n] 中的 k 个数
分割成回文串
元素无重不可复选:排序,多条值相同的只遍历第一条
子集/组合
先进行排序,让相同的元素靠在一起,如果发现 nums[i] == nums[i-1],则跳过
排列
元素无重可复选
子集/组合:sum=target
排列:去除 used 剪枝
N皇后
如果不能成功,那么返回的时候我们就还要把这个位置还原。这就是回溯算法,也是试探算法。
解决一个回溯问题,实际上就是一个决策树的遍历过程。
1、路径:已选择。
2、选择列表:可选择。
3、结束条件:无选择。
框架:排列/组合/子集
result = []
function backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
result.push([...path])或者result.push(path.slice())//path还会改变,所以不能传引用地址
return
for 选择 in 选择列表:
做选择push
backtrack(路径, 选择列表)
撤销选择pop
数组(有一定的剪枝,不用判断是否use)
const backtrack = (start) => {
// 回溯算法标准框架
for (let i = start; i < nums.length; i++) {
// 做选择
track.push(nums[i]);
// 回溯遍历下一层节点
backtrack(i + 1);
// 撤销选择
track.pop();
}
};
backtrack(0);
图
function backtrack(nums, used, track, res) {
for (let i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
track.push(nums[i]);
used[i] = true;
backtrack(nums, used, track, res);
track.pop();
used[i] = false;
}
}
全排列中
做选择/撤销选择 可用if(path.includes(item)) continue;代替
元素无重不可复选
全排列
key:
- path.length == string.length
- path.includes(item)
const _permute = string => {
const res = [];
const backtrace = path => {
if(path.length == string.length){
res.push(path);
return;
}
for(const item of string) {
if(path.includes(item)) continue;
backtrace(path + item);
}
};
backtrace('');
return res;
}
子集
输入一个无重复元素的数组 nums
,其中每个元素最多使用一次,请你返回 nums
的所有子集
比如输入 nums = [1,2,3]
,算法应该返回如下子集:
[ [],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3] ]
/**
* @param {number[]} nums
* @return {number[][]}
*/
var subsets = function(nums) {
// 用于存储结果
const res = [];
// 用于记录回溯路径
const track = [];
/**
* 回溯算法的核心函数,用于遍历子集问题的回溯树
* @param {number} start - 控制树枝的遍历,避免产生重复子集
*/
const backtrack = (start) => {
// 前序遍历位置,每个节点的值都是一个子集
res.push([...track]);
// 回溯算法标准框架
for (let i = start; i < nums.length; i++) {
// 做选择
track.push(nums[i]);
// 回溯遍历下一层节点
backtrack(i + 1);
// 撤销选择
track.pop();
}
};
backtrack(0);
return res;
};
组合:[1, n]
中的 k
个数
返回范围 [1, n]
中所有可能的 k
个数的组合,剪枝
let result = []
let path = []
var combine = function(n, k) {
result = []
combineHelper(n, k, 1)
return result
};
const combineHelper = (n, k, startIndex) => {
if (path.length === k) {
result.push([...path])
return
}
for (let i = startIndex; i <= n - (k - path.length) + 1; ++i) {
path.push(i)
combineHelper(n, k, i + 1)
path.pop()
}
}
分割成回文串
- 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个.....。
- 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段.....。
/**
* @param {string} s
* @return {string[][]}
*/
const isPalindrome = (s, l, r) => {
for (let i = l, j = r; i < j; i++, j--) {
if(s[i] !== s[j]) return false;
}
return true;
}
var partition = function(s) {
const res = [], path = [], len = s.length;
backtracking(0);
return res;
function backtracking(startIndex) {
if(startIndex >= len) {
res.push(Array.from(path));
return;
}
for(let i = startIndex; i < len; i++) {
if(!isPalindrome(s, startIndex, i)) continue;
path.push(s.slice(startIndex, i + 1));
backtracking(i + 1);
path.pop();
}
}
};
元素无重不可复选:排序,多条值相同的只遍历第一条
子集/组合
nums = [1,2,2]
,你应该输出:
[ [],[1],[2],[1,2],[2,2],[1,2,2] ]
如果一个节点有多条值相同的树枝相邻,则只遍历第一条,剩下的都剪掉,不要去遍历:
先进行排序,让相同的元素靠在一起,如果发现 nums[i] == nums[i-1]
,则跳过
排列
// 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permuteUnique = function(nums) {
let res = [];
let track = [];
let used = new Array(nums.length).fill(false);
// 先排序,让相同的元素靠在一起
nums.sort((a, b) => a - b);
backtrack(nums, used, track, res);
return res;
};
/**
* @param {number[]} nums
* @param {boolean[]} used
* @param {number[]} track
* @param {number[][]} res
*/
function backtrack(nums, used, track, res) {
if (track.length === nums.length) {
res.push(track.slice());
return;
}
for (let i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
// 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置
if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) {
continue;
}
track.push(nums[i]);
used[i] = true;
backtrack(nums, used, track, res);
track.pop();
used[i] = false;
}
}
元素无重可复选
子集/组合:sum=target
想让每个元素被重复使用,我只要把 i + 1
改成 i
即可
给之前的回溯树添加了一条树枝,在遍历这棵树的过程中,一个元素可以被无限次使用
这棵回溯树会永远生长下去,所以我们的递归函数需要设置合适的 base case 以结束算法,即路径和大于 target
时就没必要再遍历下去了
排列:去除 used
剪枝
N皇后
在 n * n 的棋盘上要摆 n 个皇后,
要求:任何两个皇后不同行,不同列也不在同一条斜线上,
求给一个整数 n ,返回 n 皇后的摆法数。
要求:空间复杂度 O(1) ,时间复杂度O(n!)
- 要确定皇后的位置,其实就是确定列的位置,因为行已经固定了
- 进一步讲,也就是如何摆放 数组
arr
[0,1,2,3,...,n-1]
- 如果没有【不在同一条斜线上】要求,这题其实只是单纯的全排列问题
- 在全排列的基础上,根据N皇后的问题,去除一些结果
-
arr
:n个皇后的列位置 -
res
:n皇后排列结果 -
ruler:
记录对应的列位置是否已经占用(也是是否有皇后),如果有,那么设为1,没有设为0 -
setPos
:哈希集合,标记正斜线(从左上到右下)位置,如果在相同正斜线上,坐标(x,y)满足 y-x 都相同,(y1 - x1)应该等于(y2 - x2)。 -
setCon
:哈希集合,标记反正斜线(从y右上到左下)位置,如果在相同反斜线上,坐标(x,y)满足 x+y 都相同,(x1 + y1)应该等于(x2 + y2)。 -
是否在同一斜线上,其实就是这两个点的所形成的斜线的斜率是否为±1。点P(a,b) ,点Q(c,d)
(1)斜率为1 (d-b)/(c-a) = 1,横纵坐标之差相等
(2)斜率为-1 (d-b)/(c-a) = -1 ,等式两边恒等变形 a+b = c + d ,横纵坐标之和相等
/**
*
* @param n int整型 the n
* @return int整型
*/
function Nqueen(n) {
let res = []; //二维数组,存放每行Q的列坐标
let isQ = new Array(n).fill(0); //记录该列是否有Q
let setPos = new Set(); //标记正对角线
let setCon = new Set(); // 标记反对角线
//给当前row找一个col
const backTrace = (row, path) => {
if (path.length === n) {
res.push(path);
return;
}
for (let col = 0; col < n; col++) {
if (
isQ[col] == 0 &&
!setPos.has(row - col) &&
!setCon.has(row + col)
) {
path.push(col);
isQ[col] = 1;
setPos.add(row - col);
setCon.add(row + col);
backTrace(row + 1, path);
path.pop();
isQ[col] = 0;
setPos.delete(row - col);
setCon.delete(row + col);
}
}
};
backTrace(0, []);
return res.length;
}
module.exports = {
Nqueen: Nqueen,
};
动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。