30 秒速览

  • 什么时候用:求所有方案、排列组合、子集、棋盘问题
  • 核心思想:带撤销操作的 DFS,遍历决策树
  • 时间复杂度:通常 O(n!) 或 O(2^n)
  • 空间复杂度:O(n)(递归深度)

一、直觉建立

走迷宫

想象你在走迷宫,遇到岔路口:

  1. 选择一条路走下去
  2. 走到死胡同 → 退回岔路口(回溯)
  3. 尝试另一条路
  4. 重复直到找到出口

回溯的本质:系统地尝试所有可能,不行就回退。

回溯 vs 递归

  • 递归:是一种编程技巧(函数调用自己)
  • 回溯:是一种算法思想(尝试 → 失败 → 撤销 → 重试)

回溯通常用递归实现。

决策树

回溯问题都可以抽象成决策树:

求 [1,2,3] 的所有排列:

         开始
        /  |  \
       1   2   3     第 1 层:选谁放第一位
      /\  /\  /\
     2 3 1 3 1 2     第 2 层:选谁放第二位
     | | | | | |
     3 2 3 1 2 1     第 3 层:只剩一个选择

结果:[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]

二、回溯模板

def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.append(路径.copy())  # 注意拷贝
        return

    for 选择 in 选择列表:
        # 做选择
        路径.append(选择)

        # 递归
        backtrack(路径, 新的选择列表)

        # 撤销选择(回溯)
        路径.pop()

核心三要素

  1. 路径:已经做出的选择
  2. 选择列表:当前可以做的选择
  3. 结束条件:到达决策树底层

三、典型例题

例题 1:LC 46. 全排列

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

示例

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

代码

def permute(nums: list[int]) -> list[list[int]]:
    result = []
    path = []
    used = [False] * len(nums)

    def backtrack():
        if len(path) == len(nums):
            result.append(path[:])  # 拷贝
            return

        for i in range(len(nums)):
            if used[i]:
                continue

            # 做选择
            path.append(nums[i])
            used[i] = True

            # 递归
            backtrack()

            # 撤销选择
            path.pop()
            used[i] = False

    backtrack()
    return result

复杂度:时间 O(n × n!),空间 O(n)


例题 2:LC 78. 子集

题目:给你一个整数数组 nums,数组中的元素互不相同。返回该数组所有可能的子集(幂集)。

示例

输入:nums = [1,2,3]
输出:[[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]

思路

子集问题不需要等到最后才收集结果,每个节点都是一个子集。

代码

def subsets(nums: list[int]) -> list[list[int]]:
    result = []
    path = []

    def backtrack(start):
        # 每个节点都是一个子集
        result.append(path[:])

        for i in range(start, len(nums)):
            path.append(nums[i])
            backtrack(i + 1)  # 从下一个位置开始,避免重复
            path.pop()

    backtrack(0)
    return result

复杂度:时间 O(n × 2^n),空间 O(n)


例题 3:LC 77. 组合

题目:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

示例

输入:n = 4, k = 2
输出:[[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]

代码

def combine(n: int, k: int) -> list[list[int]]:
    result = []
    path = []

    def backtrack(start):
        if len(path) == k:
            result.append(path[:])
            return

        # 剪枝:还需要 k - len(path) 个,所以 i 最多到 n - (k - len(path)) + 1
        for i in range(start, n - (k - len(path)) + 2):
            path.append(i)
            backtrack(i + 1)
            path.pop()

    backtrack(1)
    return result

剪枝优化

如果还需要 3 个数,当前从 4 开始,只剩 [4,5],不够 3 个,可以剪掉

剪枝公式:i <= n - (k - len(path)) + 1

复杂度:时间 O(C(n,k) × k),空间 O(k)


例题 4:LC 39. 组合总和

题目:给你一个无重复元素的整数数组 candidates 和一个目标整数 target,找出 candidates 中可以使数字和为目标数 target 的所有不同组合。candidates 中的同一个数字可以无限制重复被选取。

示例

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]

代码

def combinationSum(candidates: list[int], target: int) -> list[list[int]]:
    result = []
    path = []

    def backtrack(start, remaining):
        if remaining == 0:
            result.append(path[:])
            return
        if remaining < 0:
            return

        for i in range(start, len(candidates)):
            path.append(candidates[i])
            # 可重复选择,所以还是从 i 开始
            backtrack(i, remaining - candidates[i])
            path.pop()

    backtrack(0, target)
    return result

复杂度:时间 O(n^(target/min)),空间 O(target/min)


例题 5:LC 40. 组合总和 II

题目:每个数字只能用一次,candidates 可能有重复元素。

示例

输入:candidates = [10,1,2,7,6,1,5], target = 8
输出:[[1,1,6],[1,2,5],[1,7],[2,6]]

思路

关键是去重:同一层不能选重复的数。

def combinationSum2(candidates: list[int], target: int) -> list[list[int]]:
    candidates.sort()  # 排序,方便去重
    result = []
    path = []

    def backtrack(start, remaining):
        if remaining == 0:
            result.append(path[:])
            return
        if remaining < 0:
            return

        for i in range(start, len(candidates)):
            # 去重:同一层,跳过重复元素
            if i > start and candidates[i] == candidates[i-1]:
                continue

            path.append(candidates[i])
            backtrack(i + 1, remaining - candidates[i])  # 每个数只能用一次
            path.pop()

    backtrack(0, target)
    return result

复杂度:时间 O(2^n),空间 O(n)


例题 6:LC 79. 单词搜索

题目:给定一个 m x n 二维字符网格 board 和一个字符串单词 word。如果 word 存在于网格中,返回 true;否则,返回 false。

思路

DFS + 回溯,标记已访问的格子。

def exist(board: list[list[str]], word: str) -> bool:
    m, n = len(board), len(board[0])

    def dfs(i, j, k):
        if k == len(word):
            return True

        if i < 0 or i >= m or j < 0 or j >= n or board[i][j] != word[k]:
            return False

        # 标记已访问
        temp = board[i][j]
        board[i][j] = '#'

        # 四个方向搜索
        found = (dfs(i+1, j, k+1) or
                 dfs(i-1, j, k+1) or
                 dfs(i, j+1, k+1) or
                 dfs(i, j-1, k+1))

        # 恢复
        board[i][j] = temp

        return found

    for i in range(m):
        for j in range(n):
            if dfs(i, j, 0):
                return True

    return False

复杂度:时间 O(mn × 4^L),L 是单词长度


例题 7:LC 51. N 皇后(Hard)

题目:在 n×n 的棋盘上放置 n 个皇后,使其不能互相攻击。

思路

按行放置皇后,检查列和对角线是否冲突。

def solveNQueens(n: int) -> list[list[str]]:
    result = []
    board = ['.' * n for _ in range(n)]

    def isValid(row, col):
        # 检查列
        for i in range(row):
            if board[i][col] == 'Q':
                return False

        # 检查左上对角线
        i, j = row - 1, col - 1
        while i >= 0 and j >= 0:
            if board[i][j] == 'Q':
                return False
            i -= 1
            j -= 1

        # 检查右上对角线
        i, j = row - 1, col + 1
        while i >= 0 and j < n:
            if board[i][j] == 'Q':
                return False
            i -= 1
            j += 1

        return True

    def backtrack(row):
        if row == n:
            result.append(board[:])
            return

        for col in range(n):
            if isValid(row, col):
                board[row] = board[row][:col] + 'Q' + board[row][col+1:]
                backtrack(row + 1)
                board[row] = board[row][:col] + '.' + board[row][col+1:]

    backtrack(0)
    return result

复杂度:时间 O(n!),空间 O(n)


四、回溯问题分类

问题类型特点遍历起点
排列顺序有关每次从 0 开始,用 used 去重
组合顺序无关从 start 开始,保证不回头
子集收集所有节点从 start 开始
棋盘二维约束按行/列遍历

去重策略

  1. 排列去重:used 数组标记已使用
  2. 组合/子集去重
    • 排序
    • if i > start and nums[i] == nums[i-1]: continue

五、常见错误与陷阱

错误 1:忘记拷贝路径

# 错误:直接 append,后续修改会影响
result.append(path)

# 正确:拷贝
result.append(path[:])

错误 2:去重逻辑错误

# 组合去重:i > start
if i > start and nums[i] == nums[i-1]:
    continue

# 不是 i > 0!因为 i > 0 会把不同层级的相同元素也跳过

错误 3:忘记撤销选择

# 做选择
path.append(x)
backtrack(...)
# 忘记撤销 ← 导致路径不正确

六、复盘清单

触发信号

  • 求所有方案
  • 排列/组合/子集
  • 棋盘问题(N皇后、数独)
  • 需要尝试所有可能

三要素检查

  • 路径定义清晰
  • 选择列表正确
  • 结束条件正确
  • 撤销选择

剪枝优化

  • 提前终止不满足条件的分支
  • 排序后去重

七、题目清单

基础题

题号题目难度类型
46全排列Medium排列
78子集Medium子集
77组合Medium组合
39组合总和Medium组合
40组合总和 IIMedium组合去重

进阶题

题号题目难度类型
47全排列 IIMedium排列去重
90子集 IIMedium子集去重
79单词搜索Medium二维回溯
51N 皇后Hard棋盘
37解数独Hard棋盘
131分割回文串Medium字符串分割