回溯相关(WIP)

160 阅读5分钟

回溯算法

回溯算法是一种通过穷举来解决问题的方法,核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。

给定一颗二叉树,搜索并记录所有值为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)

常用术语

优点和局限性

回溯算法本质上是一种深度优先搜索算法,它尝试所有可能的解决方案直到找到满足条件的解。这种方法的优点在于能够找到所有可能的解决方案,在合理的剪枝下,具有很高的效率。