力扣hot100—系列8-回溯算法

回溯算法(Backtracking)常被称为"暴力解法的优化版"。它的核心思想是:一条路走到底,走不通就回头,换条路再走。

建议把回溯想象成在遍历一棵多叉树


1. 全排列 (Permutations) - LeetCode 46

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

解题思路:

  • 状态: 我们已经选了哪些数。
  • 决策树: 第一层选一个数,第二层在剩下的数里选一个...
  • 关键点: 使用一个 used 数组记录哪些数已经被选过了,避免重复选择。

代码实现:

python 复制代码
def permute(nums):
    res = []
    path = []
    used = [False] * len(nums)

    def backtrack():
        if len(path) == len(nums):
            res.append(path[:]) # 注意要拷贝一份 path
            return
        
        for i in range(len(nums)):
            if used[i]: continue # 如果用过了,跳过
            
            used[i] = True
            path.append(nums[i])
            backtrack()          # 递归
            path.pop()           # 回溯:撤销选择
            used[i] = False      # 回溯:撤销标记

    backtrack()
    return res

2. 子集 (Subsets) - LeetCode 78

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

解题思路:

  • 直观理解: 每个数字都有"选"或"不选"两个状态。
  • 树形结构: 为了防止重复(比如 [1,2][2,1] 被视为同一个子集),我们规定只能往后选 。引入一个 start 参数。
  • 特点: 树上的每一个节点都是我们要的答案。

代码实现:

python 复制代码
def subsets(nums):
    res = []
    path = []

    def backtrack(start):
        # 每一个节点都是子集,直接加入结果
        res.append(path[:])
        
        for i in range(start, len(nums)):
            path.append(nums[i])
            backtrack(i + 1) # 从下一个数开始选,避免重复
            path.pop()

    backtrack(0)
    return res

3. 电话号码的字母组合 - LeetCode 17

题目: 给定一个数字字符串(2-9),返回它能表示的所有字母组合。

解题思路:

  • 映射: 先建立数字到字母的映射(如 2: "abc")。
  • 层级: 字符串有多长,递归就有几层。第一层选第一个数字对应的字母,第二层选第二个数字的字母。

代码实现:

python 复制代码
def letterCombinations(digits):
    if not digits: return []
    phone = {'2':"abc", '3':"def", '4':"ghi", '5':"jkl", 
             '6':"mno", '7':"pqrs", '8':"tuv", '9':"wxyz"}
    res = []

    def backtrack(index, path):
        if index == len(digits):
            res.append("".join(path))
            return
        
        letters = phone[digits[index]]
        for char in letters:
            path.append(char)
            backtrack(index + 1, path)
            path.pop()

    backtrack(0, [])
    return res

4. 组合总和 (Combination Sum) - LeetCode 39

题目: 给定一个无重复元素的数组 candidates 和一个目标数 target,找出使数字和为 target 的所有组合。数字可以无限制重复选择

解题思路:

  • 去重策略: 同样使用 start 索引,保证搜索顺序,不回回头选前面的数。
  • 可重复选: 在递归时,传进去的索引依然是 i 而不是 i + 1,表示当前数字还能接着选。
  • 剪枝: 如果当前和已经大于 target,直接返回。

代码实现:

python 复制代码
def combinationSum(candidates, target):
    res = []
    path = []

    def backtrack(start, current_sum):
        if current_sum == target:
            res.append(path[:])
            return
        if current_sum > target:
            return

        for i in range(start, len(candidates)):
            path.append(candidates[i])
            # 注意这里还是传 i,因为可以重复使用同一个数字
            backtrack(i, current_sum + candidates[i])
            path.pop()

    backtrack(0, 0)
    return res

5. 括号生成 (Generate Parentheses) - LeetCode 22

题目: 数字 n 代表生成括号的对数,设计一个函数,用于能够生成所有可能的并且有效的括号组合。

解题思路:

  • 本质: 这其实是做选择:放左括号还是右括号?
  • 有效性限制:
    1. 只要左括号数量 < n,就可以放左括号。
    2. 只要右括号数量 < 左括号数量,就可以放右括号(保证括号能闭合)。

代码实现:

python 复制代码
def generateParenthesis(n):
    res = []

    def backtrack(S, left, right):
        # 长度达到 2n,说明括号放完了
        if len(S) == 2 * n:
            res.append(S)
            return
        
        # 如果左括号还没放够,就放一个左括号
        if left < n:
            backtrack(S + '(', left + 1, right)
            
        # 如果右括号比左括号少,说明可以放个右括号来匹配
        if right < left:
            backtrack(S + ')', left, right + 1)

    backtrack("", 0, 0)
    return res

继续进阶。回溯算法在处理二维网格和复杂约束(如 N 皇后)时,核心逻辑依然是**"尝试 -> 递归 -> 撤销"**。


6. 单词搜索 (Word Search) - LeetCode 79

题目: 给定一个二维网格 board 和一个字符串单词 word。判断单词是否存在于网格中(必须按顺序,通过相邻单元格连接,不能重复使用同一单元格)。

解题思路:

  • 搜索方式: 遍历网格的每一个格子作为起点。从起点开始,向上下左右四个方向进行 DFS(深度优先搜索)
  • 回溯核心:
    1. 检查当前格子是否匹配 word 的第 k 个字母。
    2. 为了防止"回头路",在递归前把当前格子改一个临时字符(如 '#'),递归结束之后再改回来。
  • 剪枝: 越界、字符不匹配、或已访问过的格子直接返回 False

代码实现:

python 复制代码
def exist(board, word):
    rows, cols = len(board), len(board[0])

    def backtrack(r, c, k):
        # 递归出口:单词所有字母都匹配完了
        if k == len(word):
            return True
        # 边界检查及字符匹配检查
        if r < 0 or r >= rows or c < 0 or c >= cols or board[r][c] != word[k]:
            return False
        
        # 1. 做选择:标记当前格子已访问
        temp = board[r][c]
        board[r][c] = '#' 
        
        # 2. 递归:向四个方向探索
        res = (backtrack(r+1, c, k+1) or 
               backtrack(r-1, c, k+1) or 
               backtrack(r, c+1, k+1) or 
               backtrack(r, c-1, k+1))
        
        # 3. 撤销选择:恢复现场
        board[r][c] = temp
        return res

    for r in range(rows):
        for c in range(cols):
            if backtrack(r, c, 0): return True
    return False

7. 分割回文串 (Palindrome Partitioning) - LeetCode 131

题目: 给你一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回所有可能的分割方案。

解题思路:

  • 视角转换: 想象你在字符串的缝隙中"切几刀"。
  • 决策树:
    • 第一刀可以切在第 1 个字符后、第 2 个...直到末尾。
    • 限制条件: 只有当切下来的"左侧部分"是回文串时,才继续对"右侧剩余部分"进行递归切割。
  • 终止条件: 切到了字符串的末尾。

代码实现:

python 复制代码
def partition(s):
    res = []
    path = []

    def is_palindrome(sub):
        return sub == sub[::-1]

    def backtrack(start):
        if start == len(s):
            res.append(path[:])
            return
        
        for i in range(start, len(s)):
            # 切下 s[start:i+1]
            sub = s[start : i+1]
            if is_palindrome(sub):
                path.append(sub)      # 1. 做选择
                backtrack(i + 1)      # 2. 递归
                path.pop()            # 3. 撤销选择

    backtrack(0)
    return res

8. N 皇后 (N-Queens) - LeetCode 51

题目: 在 n×nn \times nn×n 的棋盘上放置 nnn 个皇后,使其不能互相攻击(同一行、同一列、同一条斜线都不能有两个皇后)。返回所有可能的方案。

解题思路:

  • 逐行放置: 每一行只放一个皇后。这样就天然解决了"行冲突"。
  • 列与斜线冲突判断:
    • 列: 用一个集合 cols 记录哪些列放过了。
    • 正斜线 (左上到右下): 同一条线上的行索引 rrr 和列索引 ccc 的差值 r - c 是固定的。
    • 副斜线 (右上到左下): 同一条线上的行索引 rrr 和列索引 ccc 的和 r + c 是固定的。
  • 回溯过程: 尝试在当前行的每一列放皇后 -> 检查冲突 -> 递归下一行 -> 撤销。

代码实现:

python 复制代码
def solveNQueens(n):
    res = []
    board = [['.'] * n for _ in range(n)]
    
    # 记录哪些列、正斜线、副斜线已被占用
    cols = set()
    diag1 = set() # r - c
    diag2 = set() # r + c

    def backtrack(r):
        if r == n:
            # 找到一个解,把棋盘转成字符串格式
            res.append(["".join(row) for row in board])
            return
        
        for c in range(n):
            if c in cols or (r - c) in diag1 or (r + c) in diag2:
                continue
            
            # 1. 做选择
            board[r][c] = 'Q'
            cols.add(c)
            diag1.add(r - c)
            diag2.add(r + c)
            
            # 2. 递归下一行
            backtrack(r + 1)
            
            # 3. 撤销选择(回溯)
            board[r][c] = '.'
            cols.remove(c)
            diag1.remove(r - c)
            diag2.remove(r + c)

    backtrack(0)
    return res

💡 综合总结:如何攻克"回溯"?

  1. 确定"路径"和"选择列表"

    • 全排列:选择列表是还没用过的数字。
    • 单词搜索:选择列表是上下左右。
    • N 皇后:选择列表是当前行所有不冲突的列。
  2. 判断"终止条件"

    • 通常是索引达到数组边界,或者路径长度达到目标。
  3. 理解"回溯"的精髓

    • 回溯不是为了"删掉数据",而是为了**"恢复现场"**。
    • 因为所有的递归分支都共享同一个状态变量(如 path 数组、棋盘 board),如果不撤销上一条路径的操作,下一条路径就会被污染。
  4. 去重与剪枝(进阶)

    • 如果题目包含重复元素(如 LeetCode 90. 子集 II),通常需要先排序 ,然后在循环中判断 if i > start and nums[i] == nums[i-1]: continue

💡 刷题总结(给新手的建议):

  1. 回溯模版:

    python 复制代码
    def backtrack(参数):
        if 终止条件:
            存放结果
            return
    
        for 选择 in 选择列表:
            做选择
            backtrack(下一个路径)
            撤销选择 (回溯)
  2. 何时用 start

    • 如果是组合/子集 问题(不考虑顺序,[1,2][2,1] 一样),需要 start 防止往回走。
    • 如果是排列 问题(考虑顺序,两者不一样),不需要 start,但需要 used 数组记录。
  3. 画图: 第一次做回溯题,一定要在纸上画出那棵"递归树"。理解了树的深度(递归)和广度(for循环),回溯就掌握了一半。

相关推荐
!停1 小时前
数据结构二叉树—堆(2)&链式结构(上)
数据结构·算法
phltxy1 小时前
Vue核心进阶:v-model深度解析+ref+nextTick实战
前端·javascript·vue.js
三小河1 小时前
React 样式——styled-components
前端·javascript·后端
C++ 老炮儿的技术栈1 小时前
万物皆文件:Linux 抽象哲学的开发之美
c语言·开发语言·c++·qt·算法
im_AMBER2 小时前
Leetcode 120 求根节点到叶节点数字之和 | 完全二叉树的节点个数
数据结构·学习·算法·leetcode·二叉树·深度优先
1027lonikitave2 小时前
FFTW的expr.ml怎么起作用
算法·哈希算法
TracyCoder1232 小时前
LeetCode Hot100(54/100)——215. 数组中的第K个最大元素
算法·leetcode·排序算法
We་ct2 小时前
LeetCode 92. 反转链表II :题解与思路解析
前端·算法·leetcode·链表·typescript
春日见2 小时前
如何查看我一共commit了多少个,是哪几个,如何回退到某一个版本
vscode·算法·docker·容器·自动驾驶