回溯算法(Backtracking)

回溯算法是「遍历」思维模式的典型应用:在递归树上遍历所有可能的选择,到达叶子节点时收集结果,不合适时撤销选择(回溯)。

定义

回溯算法(Backtracking)是一种通过试错来寻找问题解的算法范式。它在递归树上进行深度优先搜索,尝试一条路径,遇到不合适的情况时撤销上一步的选择(回溯),继续尝试其他路径。

核心本质:回溯算法 = 递归遍历 + 做选择 + 撤销选择

决策树视角

回溯算法本质是遍历一棵决策树的过程(参见 回溯算法框架):

  • 路径:记录已经做出的选择
  • 选择列表:当前可以做的选择
  • 结束条件:到达决策树底层叶子节点

关键点:回溯算法和 DFS 算法基本可认为是同一种算法,细微差异在于实现视角。回溯算法强调"做选择→递归→撤销选择"的模式。

核心特性

特性 说明
试探性 尝试一种选择,看是否能得到解
可撤销 不合适时可以撤销上一步选择
穷举所有 遍历整个递归树(或剪枝后部分)
DFS 性质 本质是深度优先搜索

与递归的关系

回溯算法与递归(遍历思维)的关系:

维度 普通遍历递归 回溯算法
思维模式 遍历 遍历(带状态管理)
状态管理 可能用外部变量 必须维护选择路径(track)
操作 遍历收集 做选择 → 递归 → 撤销选择
示例 简单 DFS 全排列、N 皇后

关键洞察:理解递归的遍历思维是学习回溯算法的前提。回溯算法就是在递归遍历的基础上,加上"做选择"和"撤销选择"的步骤。

标准框架

result = []

void backtrack(路径, 选择列表) {
    if (满足结束条件) {
        result.add(路径的拷贝)
        return
    }

    for (选择 in 选择列表) {
        // 做选择
        路径.add(选择)

        // 递归进入下一层
        backtrack(路径, 选择列表)

        // 撤销选择(回溯)
        路径.removeLast()
    }
}

三个关键点

  1. 路径:记录当前已做的选择
  2. 选择列表:当前还可以做哪些选择
  3. 结束条件:什么时候收集结果

经典问题

问题 难度 说明
全排列 中等 回溯入门经典,每个位置尝试所有可选数字
N 皇后 困难 每行尝试放皇后,检查冲突
组合 中等 从 n 个数中选 k 个,不重复
子集 中等 每个元素可选可不选,生成所有子集
单词搜索 中等 在二维网格中搜索单词,四个方向回溯
数独求解 困难 每个空格尝试 1-9,检查合法性

全排列:回溯入门

问题:给定不重复的数字数组,返回所有可能的排列。

List<List<Integer>> res = new LinkedList<>();

List<List<Integer>> permute(int[] nums) {
    LinkedList<Integer> track = new LinkedList<>();
    boolean[] used = new boolean[nums.length];
    backtrack(nums, track, used);
    return res;
}

void backtrack(int[] nums, LinkedList<Integer> track, boolean[] used) {
    // 结束条件:路径长度 = 数组长度
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }

    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的选择
        if (used[i]) continue;

        // 做选择
        track.add(nums[i]);
        used[i] = true;

        // 递归
        backtrack(nums, track, used);

        // 撤销选择(回溯)
        track.removeLast();
        used[i] = false;
    }
}

递归树理解:全排列的递归树是多叉树,每个节点代表一次选择,叶子节点是一个完整的排列。

剪枝优化

回溯算法可以通过剪枝提高效率:

剪枝场景 方法
重复选择 用 used 数组或 Set 记录已选元素
顺序无关 记录起始位置,避免重复组合
约束条件 提前判断当前路径是否可能得到解
可行性 如 N 皇后中检查冲突

与动态规划的区别

虽然回溯算法和动态规划都抽象为树/图的结构,但两者思路完全不同(参见 回溯算法框架):

维度 回溯算法 动态规划
抽象模型 决策树 状态转移图
核心三要素 路径、选择列表、结束条件 状态、选择、base case
优化空间 无(纯暴力穷举) 有(记忆化/重叠子问题)
时间复杂度 指数级 O(n!)、O(2^n) 多项式级 O(n²)、O(n³)

关键区别:回溯算法必须穷举整棵决策树,无法避免指数级复杂度;而动态规划通过记忆化避免重复计算,可以优化到多项式时间。

与贪心算法的区别

回溯算法与贪心算法都用于搜索/优化问题,但策略完全不同(参见 贪心算法解题套路框架):

维度 回溯算法 贪心算法
优化手段 剪枝优化,提前排除不可能的答案 推导最优,跳过完整穷举
穷举范围 穷举所有可行解(剪枝后可能部分) 不穷举所有可行解
时间复杂度 指数级 $O(2^n)$、$O(n!)$ 通常 $O(n)$ 或 $O(1)$
正确性保证 只要正确穷举就保证找到最优解 需要证明贪心选择性质成立
状态管理 需要维护路径,做选择/撤销选择 通常不需要回溯,直接选择

演进关系:回溯(剪枝优化)→ 动态规划(备忘录优化)→ 贪心(推导最优,不穷举)。每一步都在减少穷举的范围。

时间复杂度

回溯算法通常是指数级时间复杂度,因为要穷举所有可能:

问题 时间复杂度 说明
全排列 O(n!) n 个数字的所有排列
子集 O(2^n) 每个元素可选可不选
组合 C(n,k) O(C(n,k)) 从 n 个选 k 个

注意事项

  • 要撤销选择:递归返回后必须恢复状态,否则路径会累积错误
  • 要拷贝结果:加入结果集时要拷贝路径,不能加入引用
  • 剪枝要早:尽早排除不可能的路径,减少搜索空间
  • 递归深度:深度过大可能栈溢出,注意边界

应用场景

  • 组合枚举:全排列、组合、子集
  • 约束满足:N 皇后、数独、图的着色
  • 路径搜索:单词搜索、迷宫问题
  • 决策问题:0-1 背包(带剪枝)

相关概念

  • 递归:回溯算法的基础,遍历思维
  • DFS:回溯算法本质是深度优先搜索
  • 分治算法:也用递归,但不需要回溯(子问题独立)
  • 动态规划:带记忆化的分解问题,与回溯思路不同
  • 贪心算法:比回溯更进一步,不穷举所有可行解
  • 二叉树:回溯算法的递归树类似多叉树

参考资料


Interactive Graph