详解JavaScript中的回溯算法

前言

回溯算法是一种试探性的算法,它在解决问题时尝试各种可能的解,并通过在每一步选择中进行回退(backtrack)来找到问题的解。在JavaScript中,回溯算法通常用于解决一些组合、排列、子集和搜索等问题。

回溯算法的基本思想:

  1. 选择: 在问题的每一步,算法尝试各种可能的选择。
  2. 判断: 对每个选择进行评估,确定是否满足问题的条件。
  3. 回退: 如果选择导致无解或不满足条件,算法回退到之前的状态,尝试其他选择。

下面我将用LeetCode中的例题进行讲解。

1. 全排列问题(Permutations)

问题描述: 给定一个不含重复数字的数组 nums,返回其所有可能的全排列。

js 复制代码
var permute = function(nums) {
    const result = [];

    function backtrack(path, used) {
        // 终止条件:当当前路径的长度等于数组长度时,说明找到一个全排列
        if (path.length === nums.length) {
            result.push([...path]); // 将当前路径拷贝一份加入结果集
            return;
        }

        // 遍历数组元素
        for (let i = 0; i < nums.length; i++) {
            // 跳过已经使用过的元素
            if (used[i]) continue;

            // 做出选择:将当前元素加入路径,并标记为已使用
            path.push(nums[i]);
            used[i] = true;

            // 递归调用,继续往下选择
            backtrack(path, used);

            // 撤销选择:回溯到上一层状态
            path.pop();
            used[i] = false;
        }
    }

    // 初始调用回溯函数
    backtrack([], []);
    return result;
};

在上述代码中,我们构建了一个backtrack的回调函数,它接受两个参数,path 表示当前的路径,used 用于标记数组元素是否被使用过。并加入了终止条件:当当前路径的长度等于数组长度时,说明找到一个全排列。然后做出选择。以此通过递归的方式,使用回溯算法在不同的选择路径上搜索全排列,确保每个元素都在每个位置上都有机会出现。

2. 子集问题(Subsets)

问题描述: 给定一个不含重复元素的整数数组 nums,返回该数组所有可能的子集。

js 复制代码
var subsets = function(nums) {
    const result = [];

    function backtrack(start, path) {
        // 将当前路径加入结果集
        result.push([...path]);

        // 遍历数组元素
        for (let i = start; i < nums.length; i++) {
            // 做出选择:将当前元素加入路径
            path.push(nums[i]);

            // 递归调用:继续往下选择,从下一个元素开始
            backtrack(i + 1, path);

            // 撤销选择:回溯到上一层状态
            path.pop();
        }
    }

    // 初始调用回溯函数,从第一个元素开始,初始路径为空
    backtrack(0, []);
    
    // 返回最终的结果集
    return result;
};

现在逐步解释这段代码的运行过程:

  1. 初始调用: 最初调用 backtrack(0, []),表示从数组的第一个元素开始选择,当前路径为空。
  2. 遍历数组元素: 进入 for 循环,从 start 开始遍历数组元素。
  3. 做出选择: 将当前元素加入路径 path,这里是 nums[i]
  4. 递归调用: 调用 backtrack(i + 1, path),继续往下选择,下一层的起始位置为 i + 1,表示从当前元素的下一个元素开始选择。
  5. 遍历和递归: 重复上述步骤,遍历数组元素,做出选择,递归调用,直到遍历完成。
  6. 撤销选择: 当递归返回后,回到上一层状态,撤销当前选择,即 path.pop(),这样就回溯到上一层状态。
  7. 将当前路径加入结果集: 在每一层递归返回后,都将当前路径加入结果集,即 result.push([...path])
  8. 重复过程: 重复以上步骤,直到遍历完所有可能的选择,得到所有子集。

3. 组合求和问题(Combination Sum)

问题描述: 给定一个无重复元素的数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。

js 复制代码
var combinationSum = function(candidates, target) {
    const result = [];

    function backtrack(start, target, path) {
        // 终止条件:当目标值为0时,表示找到一个组合,将当前路径加入结果集
        if (target === 0) {
            result.push([...path]);
            return;
        }

        // 遍历数组元素
        for (let i = start; i < candidates.length; i++) {
            // 跳过不符合条件的元素
            if (target - candidates[i] < 0) continue;

            // 做出选择:将当前元素加入路径
            path.push(candidates[i]);

            // 递归调用:继续往下选择,更新目标值,起始位置不变
            backtrack(i, target - candidates[i], path);

            // 撤销选择:回溯到上一层状态
            path.pop();
        }
    }

    // 初始调用回溯函数,从数组的第一个元素开始,目标值为target,初始路径为空
    backtrack(0, target, []);

    // 返回最终的结果集
    return result;
};

下面逐步解释代码的运行过程:

  1. 初始调用: 调用 backtrack(0, target, []),表示从数组的第一个元素开始选择,目标值为 target,初始路径为空。
  2. 终止条件:backtrack 函数内部,首先检查终止条件,当 target 等于 0 时,表示找到一个组合,将当前路径 path 加入结果集 result
  3. 遍历数组元素: 使用循环遍历数组元素,从 start 开始。
  4. 跳过不符合条件的元素: 如果 target - candidates[i] < 0,说明当前元素加入路径后目标值将小于0,跳过当前元素。
  5. 做出选择: 将当前元素加入路径 path
  6. 递归调用: 调用 backtrack(i, target - candidates[i], path),继续往下选择,更新目标值为 target - candidates[i],起始位置不变。
  7. 撤销选择: 在递归返回后,回溯到上一层状态,将当前元素从路径中弹出。
  8. 重复过程: 重复以上步骤,直到遍历完所有可能的选择,得到所有满足条件的组合。

总结

  1. 在每个问题中,都定义了一个backtrack函数,负责执行回溯算法的核心逻辑。
  2. path参数用于保存当前的路径,表示一种可能的解。
  3. 对于全排列和子集问题,通过循环遍历数组元素,做出选择并递归,然后撤销选择。
  4. 对于组合求和问题,同样通过循环遍历数组元素,但在递归时传递的target值会递减,直到target为0时,表示找到一个解。

注意事项:

  • 回溯算法通常使用递归实现。
  • 在每一步的选择中,需要进行选择、递归、回退三个步骤。
  • 需要谨慎处理状态的保存和撤销,确保不影响算法的正确性。
相关推荐
早上好啊! 树哥5 小时前
JavaScript Math(算数) 对象的用法详解
开发语言·javascript·ecmascript
破-风5 小时前
leetcode-------mysql
算法·leetcode·职场和发展
screct_demo6 小时前
通俗易懂的讲一下Vue的双向绑定和React的单向绑定
前端·javascript·html
有心还是可以做到的嘛6 小时前
ref() 和 reactive() 区别
前端·javascript·vue.js
山楂树の8 小时前
xr-frame 通过shader去除视频背景色,加载透明视频
javascript·线性代数·ar·xr·图形渲染
自不量力的A同学9 小时前
如何利用人工智能算法优化知识分类和标签?
人工智能·算法·分类
拖孩9 小时前
💥大家好,我是拖孩🎤
前端·javascript·后端
CodeJourney.9 小时前
PyTorch不同优化器比较
人工智能·pytorch·算法·能源
winner88819 小时前
对比学习损失函数 - InfoNCE
学习·算法·对比学习·infonce
鹿屿二向箔9 小时前
JavaScript (JS) 前端开发
javascript