回溯算法
回溯算法是一种通过穷举来解决问题的方法,核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。
给定一颗二叉树,搜索并记录所有值为7的节点,请返回节点列表。
解法: 用前序遍历即可
/**
*
* 给定一棵二叉树,搜索并记录所有值为7的节点。
*/
function preOrder(root,res){
if(root === null){
return null
}
if(root.val === 7){
res.push(root)
}
preOrder(root.left,res)
preOrder(root.right,res)
}
在二叉树中搜索所有值为7的节点,请返回根节点到这些节点到路径
/**
*
* @param {*} root
* @param {any[]} res
* @param {any[]} path
* @returns
*/
function preOrder(root,res,path){
if(root === null){
return null
}
path.push(root)
if(root.val === 7){
res.push([...path])
}
preOrder(root.left,res)
preOrder(root.right,res)
path.pop()
}
关键点在于尝试和回退
看图解
剪枝
复杂的回溯问题通常包含一个或多个约束条件,约束条件通常可用于"剪枝"。
在二叉树中搜索所有值为 7 的节点,请返回根节点到这些节点的路径,并要求路径中不包含值为 3 的 节点。
/**
*
* 在二叉树中搜索所有值为 7 的节点,请返回根节点到这些节点的路径,并要求路径中不包含值为 3 的 节点。
*/
function preOrder(root, path, res) {
// 剪枝
if (root === null || root.val === 3) {
return;
}
// 尝试 path.push(root);
if (root.val === 7) {
// 记录解
res.push([...path]);
}
preOrder(root.left, path, res); preOrder(root.right, path, res); // 回退
path.pop();
}
框架代码
/**
*
* 在二叉树中搜索所有值为 7 的节点,请返回根节点到这些节点的路径,并要求路径中不包含值为 3 的 节点。
*/
/* 判断当前状态是否为解 */
/* 判断当前状态是否为解 */
function isSolution(state) {
return state && state[state.length - 1]?.val === 7;
}
/* 记录解 */
function recordSolution(state, res) {
res.push([...state]);
}
/* 判断在当前状态下,该选择是否合法 */
function isValid(state, choice) {
return choice !== null && choice.val !== 3;
}
/* 更新状态 */
function makeChoice(state, choice) {
state.push(choice);
}
/* 恢复状态 */
function undoChoice(state) {
state.pop();
}
/* 回溯算法:例题三 */
function backtrack(state, choices, res) {
// 检查是否为解
if (isSolution(state)) {
// 记录解
recordSolution(state, res);
}
// 遍历所有选择
for (const choice of choices) {
// 剪枝:检查选择是否合法
if (isValid(state, choice)) {
// 尝试:做出选择,更新状态
makeChoice(state, choice);
// 进行下一轮选择
backtrack(state, [choice.left, choice.right], res);
// 回退:撤销选择,恢复到之前的状态
undoChoice(state);
}
}
}
全排列
/**
* 输入样例: [1,2,3]
* 输出样例: [1,2,3] [1,3,2] [2,1,3] [2,3,1] [3,1,2]、[3,2,1]
*/
/**
*
* @param {number[]} arr
* 目标是: [1,2,3] [1,3,2]
*/
/**
*
* @param {number[]} state
*/
function isSolution(state,length){
return state.length === length
}
/**
*
* @param {boolean[]} selected // 已经选择的元素
* @param {number} index
* @returns
*/
function isValid(selected,index){
return !selected[index]
}
function makeChoice(state,choice,selected,index){
state.push(choice)
selected[index] = true
}
/* 回溯算法框架 */
function backtrack(state, choices, res,selected,resArr) {
// 判断是否为解
if (isSolution(state,choices.length)) {
// 记录解 recordSolution(state, res); // 不再继续搜索
res.push([...state])
return;
}
// 遍历所有选择
for (let [index,choice] of choices.entries()) {
// 剪枝:判断选择是否合法
if (isValid(selected, index)) {
// 尝试 作出选择,更新状态
makeChoice(state, choice,selected,index);
backtrack(state, choices, res,selected,resArr);
selected[index] = false
state.pop()
}
}
}
/**
*
* @param {number[]} arr
*/
function main(arr){
// 初始化一个selected
const selected = new Array(arr.length).fill(false)
const resArr = []
const state = []
backtrack(state,arr,resArr,selected,resArr)
console.log(resArr)
}
main([1,2,3])
全排列(有重复元素)
/**
* 输入样例: [1,2,3]
* 输出样例: [1,2,3] [1,3,2] [2,1,3] [2,3,1] [3,1,2]、[3,2,1]
*/
/**
*
* @param {number[]} arr
* 目标是: [1,2,3] [1,3,2]
*/
/**
*
* @param {number[]} state
*/
function isSolution(state,length){
return state.length === length
}
/**
*
* @param {boolean[]} selected // 已经选择的元素
* @param {number} index
* @param {Set} set
* @returns
*/
function isValid(selected,index,set,arr){
return !selected[index]
}
function makeChoice(state,choice,selected,index){
state.push(choice)
selected[index] = true
}
/* 回溯算法框架 */
function backtrack(state, choices, res,selected,resArr) {
const set = new Set()
// 判断是否为解
if (isSolution(state,choices.length)) {
// 记录解 recordSolution(state, res); // 不再继续搜索
res.push([...state])
return;
}
// 遍历所有选择
for (let [index,choice] of choices.entries()) {
// 每一轮都要判断一次,如果有重复的,直接结束此次循环即可。
if(set.has(choice)){
continue
}
// 剪枝: 判断选择是否合法
if (isValid(selected, index,set,choices)) {
set.add(choice)
// 尝试 作出选择,更新状态
makeChoice(state, choice,selected,index);
backtrack(state, choices, res,selected,resArr);
selected[index] = false
state.pop()
// set.delete(choice)
}
}
}
/**
*
* @param {number[]} arr
*/
function main(arr){
// 初始化一个selected
const selected = new Array(arr.length).fill(false)
const resArr = []
const state = []
backtrack(state,arr,resArr,selected,resArr)
console.log(resArr)
}
main([1,1,3,2,2])
唯一区别就在于要在每轮循环开始前判断一下,是否可以排列,使用set判断即可。
看图:
在元素中查找目标元素target
给定一个数组和一个目标元素,从数组中选择若干元素,若干元素的相加值为target
/**
*
* @param {number[]} state // 排序的数组
* @param {*} target // 需要匹配的目标元素
* @param {number[]} choices // 需要匹配的数组
* @param {*} start // 开始索引
* @param {*} res // 结果数组
* @returns
*/
function backtrack(state, target, choices, start, res) {
if (target === 0) {
res.push([...state]);
return;
}
// 从start开始
for (let i = start; i < choices.length; i++) {
// 剪枝情况1: 元素相加小于0
if (target - choices[i] < 0) {
break;
}
state.push(choices[i]);
// 进行下一轮选择
backtrack(state, target - choices[i], choices, i, res); // 回退:撤销选择,恢复到之前的状态
state.pop();
}
}
function subsetSumI(nums, target) {
// 定义一个state
const state = [];
// 这个算法必须先进行排序,才可以,不然会有问题
nums.sort((a, b) => a - b);
const start = 0; // 遍历起始点
const res = []; // 结果列表(子集列表) backtrack(state, target, nums, start, res);
backtrack(state, target, nums, start, res);
return res;
}
const res = subsetSumI([3,4,5],9)
console.log('res==',res)
常用术语
优点和局限性
回溯算法本质上是一种深度优先搜索算法,它尝试所有可能的解决方案直到找到满足条件的解。这种方法的优点在于能够找到所有可能的解决方案,在合理的剪枝下,具有很高的效率。