09-回溯
📅 发表于 2026/05/22
🔄 更新于 2026/05/22
👁️ -- 次访问
📝 0 字
⏳ 0 分钟
三大元素
路径:已做出的历史选择。选择列表:当前这一步还可以怎么走。结束条件:什么时候停下来。撤销选择
回到上一个分叉口,尝试其他可能。剪枝
已不满足条件,砍掉错误决策分支。# 伪代码模板
def backtrack(路径, 选择列表):
if 满足结束条件:
保存结果
return
for 选择 in 选择列表:
# 1. 做选择
将选择加入路径
更新选择列表
# 2. 进入下一层决策树(递归)
backtrack(路径, 剩下的选择列表)
# 3. 撤销选择(回溯)
将选择从路径中移除
恢复选择列表res, path 当前路径, used 已使用数字backtrack 回溯函数结束条件:所有数字已用完,len(path) == len(nums) path[:]),复制整份遍历所有候选列表 (num not in used) 选择当前值递归DFS向下查找撤销当前选择开始回溯 backtrack() 更新resdef permute_dfs_backtrack(self, nums: List[int]) -> List[List[int]]:
""" 数字不重复,输出所有可能性。通过dfs回溯,来进行寻找所有答案
"""
# 所有排列结果
res = []
# 当前路径
path = []
# 哪些数字已经使用,避免重复选取
used = set()
def backtrack():
# 结束条件
if len(path) == len(nums):
# 注意:Python中列表是引用传递,需要加上 [:] 拷贝一份当前的值
res.append(path[:])
return
# 遍历所有候选列表
for num in nums:
if num in used:
continue
# 选择当前节点
path.append(num)
used.add(num)
# 回溯
backtrack()
# 撤销当前选择
path.pop()
used.remove(num)
backtrack()
return res示例
[1, 1, 2], [1, 2, 1], [2, 1, 1]]核心思路
位置bool:判断数字是否使用。因为有重复,不能用set对nums排序:相同数字 相邻。候选列表过滤:当前数字和上个数字 相同,且上一个数字未被使用 (刚被释放)。 nums[i] == nums[i-1] and used[i-1] == Falsedef permute_repeatnums_dfs_backtrack(self, nums: List[int]) -> List[List[int]]:
""" 数字可能重复,输出全部可能的全排列答案 """
# 最终结果
res = []
# 当前路径
path = []
# 各位置数字是否使用
used = [False] * len(nums)
# 排序,相同数字相邻
nums.sort()
def backtrack():
# 终止条件
if len(path) == len(nums):
res.append(path[:])
return
# 遍历候选列表
for i in range(len(nums)):
if used[i]:
# 当前数字已在路径中
continue
# 剪枝,当前数字和上一个数字相同,且上一个数刚刚被撤销
if i > 0 and nums[i] == nums[i-1] and used[i-1] == False :
continue
# 选择当前值
path.append(nums[i])
used[i] = True
backtrack()
# 撤销当前选择
path.pop()
used[i] = False
backtrack()
return res结果保存
走到叶子节点 才存储结果。每一步 都需要存储结果。空、单、部分、全部数字都是子集。防重复方式
全排列:[1, 2]和[2, 1]是不同path。顺序 很重要。
子集:{1, 2}和{2, 1} 是相同集合。顺序 不重要。
子集 start_index:为了防{2,1}倒退,规定只能从start_index开始往后看。
每次进入backtrack都 保存结果start_index往后 遍历候选列表保存nums[i]backtrack(i+1)删掉nums[i]def subsets_dfs_backtrack(self, nums: List[int]) -> List[List[int]]:
"""找出所有子集,数组不重复, dfs start_index"""
res = []
path = []
def backtrack(start_index):
# 存储当前内容
res.append(path[:])
# 依次遍历当前候选列表
for i in range(start_index, len(nums)):
# 选择当前值
path.append(nums[i])
# 递归进入下一层
backtrack(i+1)
# 回溯
path.pop()
backtrack(0)
return res数组有重复元素
[1, 2, 2]: [[],[1],[1,2],[1,2,2],[2],[2,2]]
核心思想
对数组排序,相同元素 挨着。used 记录数字是否已使用。当前数字在上一轮刚作为候选项 遍历过,则跳过。def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
""" 数组有重复元素,输出不同子集。 排序+used
"""
res = []
path = []
used = [False] * len(nums)
nums.sort()
def backtrack(start_index):
# 进入则保存结果
res.append(path[:])
# 遍历候选项
for i in range(start_index, len(nums)):
# 如果当前数字,上一轮刚刚使用过,则跳过
if i > 0 and nums[i] == nums[i-1] and used[i-1] == False:
continue
# 选择当前数字
path.append(nums[i])
used[i] = True
# DFS搜索下一层
backtrack(i+1)
# 回溯当前值
path.pop()
used[i] = False
backtrack(0)
return res输入:仅包含2-9的字符串
电话号码:2-abc, 3-def, 4-ghi, 5-jkl, 6-mna, 7-pqrs, 8-tuv, 9-wxyz
输出:能表示的所有字母组合
示例
2:[a, b, c]
23:[ad,ae,af, bd, be, bf, cd, ce, cf]
特点
每一步,在不同的集合里取挑选字母。 [a, b, c],Step2: [d, e, f]DFS 全排列思想
数字-字母集合} 映射,每1步在特定集合里做选择。根据层数,选特定集合。def letterCombinations(self, digits: str) -> List[str]:
"""DFS,回溯, 每一层取不同的集合"""
if not digits:
return []
res = []
path = []
nums = [int(n) for n in digits]
num2chars = {
2: ['a', 'b', 'c'],
3: ['d', 'e', 'f'],
4: ['g', 'h', 'i'],
5: ['j', 'k', 'l'],
6: ['m', 'n', 'o'],
7: ['p', 'q', 'r', 's'],
8: ['t', 'u', 'v'],
9: ['w', 'x', 'y', 'z'],
}
def backtrack(start_index):
if start_index == len(nums):
res.append("".join(path))
return
num = nums[start_index]
choices = num2chars[num]
for ch in choices:
path.append(ch)
backtrack(start_index+1)
path.pop()
backtrack(0)
return res无重复元素的数组 + 目标整数target和为target的所有不同组合,数字可重复使用。组合注意
数字 可重复使用,同一位置数字可多次重复使用。核心思想
backtrack(start_index)当前求和为targetstart_index往后的所有数字传入i,而非i+1,依然可以选择当前数字当前和+candidates[i] >target关键点
从小到大排序,方便做剪枝顺序不重要,所以需使用 start_index 来防止倒车。 [2, 2, 3] 和 [3, 2, 2] 算一个答案索引是i,而非i+1start_index和current_sumcurrent_sum + current_val > target,则直接break,后续数字不用看了def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
"""排序+dfs+剪枝"""
if not candidates:
return []
res = []
path = []
# 从小到大排序,为了后续能用 break 极速剪枝
candidates.sort()
# 推荐将 current_sum 作为参数传入,避免 nonlocal 作用域问题
def backtrack(start_index, current_sum):
# 找到合法组合
if current_sum == target:
res.append(path[:])
return
# 其实有了下面的 break 剪枝,这个判断甚至可以省略,但保留着逻辑更严密
if current_sum > target:
return
# 遍历候选项
for i in range(start_index, len(candidates)):
current_val = candidates[i]
# 终极剪枝:因为已经排序,当前超出 target,后面的必定超,直接 break!
if current_sum + current_val > target:
break # 注意这里改成了 break
# 1. 做选择
current_sum += current_val
path.append(current_val)
# 2. 回溯,依然可以选择当前 i,不往回头选
backtrack(i, current_sum)
# 3. 撤销选择
path.pop()
current_sum -= current_val
# 初始 start_index=0, current_sum=0
backtrack(0, 0)
return resn,即n个左括号+n个右括号。有效括号组合((()))", "(()())", "(())()", "()(())", "()()()"]()"]约束
合法括号left_cnt, right_cnt):传入左右括号数量每一步选择的 左或右括号路径长度==2n左括号:left < n,最多n个左括号右括号:right < left,左括号数量 < 右括号数量def generateParenthesis(self, n: int) -> List[str]:
if n <= 0:
return []
res = []
path = []
# 回溯,传入左右括号数量,方便剪枝
def backtrack(left_cnt, right_cnt):
# 结束条件
if left_cnt + right_cnt == 2*n:
res.append("".join(path))
# 候选列表,选左括号 或 右括号
for ch in ('(', ')'):
if ch == '(':
# 选左括号,最多选择n个左括号
if left_cnt < n:
path.append(ch)
backtrack(left_cnt+1, right_cnt)
path.pop()
if ch == ')':
# 选右括号,最多选择n个右括号,且右括号必须<左括号数量
if right_cnt < left_cnt:
path.append(ch)
backtrack(left_cnt, right_cnt+1)
path.pop()
backtrack(0, 0)
return res核心思想
path长度和word相同4个方向位置合法、位置未被使用、字符相同def exist(self, board: List[List[str]], word: str) -> bool:
"""遍历首字符,相同则 dfs 上下左右搜索"""
if not board or not word:
return False
m, n = len(board), len(board[0])
used = [[False] * n for _ in range(m)]
path = []
valid_paths = []
def backtrack(i, j):
# 终止条件:单词已经拼凑弃权
if len(path) == len(word):
valid_paths.append(path[:])
return
# 找到1个就不找了,如果要找所有的,则注释掉
if valid_paths:
return
# 下一个需要的字符
wpos = len(path)
need_ch = word[wpos]
# 四个方向,候选项
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
for di, dj in directions:
# 新的候选 i, j
ni, nj = i + di, j + dj
# 超出范围
if not 0 <= ni < m or not 0 <= nj < n:
continue
# 字符不相同,或者该位置已被使用
if board[ni][nj] != need_ch or used[ni][nj]:
continue
# 选择当前位置,进行下一层回溯寻找
path.append((ni, nj))
used[ni][nj] = True
backtrack(ni, nj)
path.pop()
used[ni][nj] = False
return
for i in range(m):
for j in range(n):
if board[i][j] == word[0]:
path.append([i,j])
used[i][j] = True
backtrack(i, j)
path.pop()
used[i][j] = False
if len(valid_paths) > 0:
return True
return False矩阵不同字母数量 < 单词不同字母数量正序搜索 首字母少 或 逆序搜索 尾字母少k == len(word),[i,j]越界、字符不等。[i, j] 变为 #,表示used上下左右 去回溯原chdef exist_pruning_backtrack(self, board: List[List[str]], word: str) -> bool:
"""剪枝,优化正序逆序查找,标准backtrack写法"""
if not board or not word:
return False
m, n = len(board), len(board[0])
# board和单词的字母统计
board_counter = Counter([ch for row in board for ch in row])
word_counter = Counter(word)
# 剪枝,单词字母,不在矩阵中
for ch, cnt in word_counter.items():
if ch not in board_counter:
return False
# 尾字符比首字符在矩阵中的次数更少,翻转搜索效率更高
if board_counter[word[-1]] < board_counter[word[0]]:
word = word[::-1]
def backtrack(i, j, k):
# 找到目标单词,匹配成功
if k == len(word):
return True
# ij越界、字符不匹配,则返回
if not 0 <= i < m or not 0 <= j < n or board[i][j] != word[k]:
return False
# 选择当前位置i,j,设置为其他字符,进行上下左右搜索
raw_ch = board[i][j]
board[i][j] = "#"
# 上下左右回溯,使用or,若有1个成功,则不进行下一个搜索
res = backtrack(i+1, j, k+1) or backtrack(i-1, j, k+1) or backtrack(i, j+1, k+1) or backtrack(i, j-1, k+1)
# 还原当前ij
board[i][j] = raw_ch
return res
# 遍历每个字符,若和首字符相同,则进行回溯搜索
for i in range(m):
for j in range(n):
if board[i][j] == word[0]:
# 相同才进行回溯
if backtrack(i, j, 0):
return True
return Falsestart_index哪些位置做切分start_index == len(s)start_index <= i < len(s)子串 s[start_index:i+1] 是回文字符串s == s[::-1]def partition(self, s: str) -> List[List[str]]:
"""分割回文字符串"""
if not s:
return []
res = []
path = []
def backtrack(start_index):
# 终止条件,全部分割完成
if start_index == len(s):
res.append(path[:])
return
# 候选项分割项
for i in range(start_index, len(s)):
# 当前切割子串
sub = s[start_index:i+1]
# 子串不是回文串,说明当前切分位置不行
if sub != sub[::-1]:
continue
# 1. 存储当前分割结果
path.append(sub)
# 2. 递归,从下一个位置i+1继续切分
backtrack(i+1)
# 3. 取消当前分割点,下一轮尝试切分更长子串
path.pop()
return
backtrack(0)
return resn个皇后,放在n*n的棋盘上,使皇后不能相互攻击。行、列、斜 都可攻击。所有的 棋盘放置方案,Q代表皇后,.代表空位。.Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]][i,j] 可攻击范围
used[i][*]used[*][j][i+1][j+1]、[i+2][j+2][i+1][j-1]、[i+2][j-2]三大攻击
主对角线: row-col 不变 ,如00, 11, 22副对角线:row+col 不变,如02, 11, 20垂直攻击:col 不能重复使用def solveNQueens_colrowset(self, n: int) -> List[List[str]]:
"""回溯法,三大攻击:垂直攻击,右斜攻击(主对角线),左斜攻击(副对角线)
主对角线:00, 11, 22, row-col 不变
副对角线:02, 11, 20, row+col 不变
"""
# 结果和单次路径
res = []
# 记录 每一行选择的皇后所在列
path = []
# 三大攻击的set
cols = set()
# 主对角线:值
diag1 = set()
# 副对角线:值
diag2 = set()
def backtrack(row):
# 递归终止条件
if len(path) == n:
board = []
for col in path:
raw_str = "." * col + "Q" + "." * (n-col-1)
board.append(raw_str)
res.append(board)
return
# 筛选候选列
for col in range(n):
if col in cols or col-row in diag1 or col+row in diag2:
# 当前列已使用,或在某主对角线(右斜)、在某副对角线(左斜)
continue
# 选择col列
path.append(col)
cols.add(col)
diag1.add(col-row)
diag2.add(col+row)
# 回溯查找下一层
backtrack(row+1)
# 还原当前col的选择
path.pop()
cols.remove(col)
diag1.remove(col-row)
diag2.remove(col+row)
backtrack(0)
return reslen(path) == n哪一列,必须满足当前条件。def solveNQueens_forpath(self, n: int) -> List[List[str]]:
"""回溯法,选择每一行把皇后放在哪一列,解决N皇后放置问题"""
res = []
path = []
def backtrack(row):
"""回溯处理第row行"""
# 回溯终止条件:path已装满
if len(path) == n:
res_p = ["".join(r) for r in path]
res.append(res_p)
return
# 根据当前path,确定当前行的候选项,可以放置哪些列
# 初始选项
choices = [col for col in range(n)]
for i, cols in enumerate(path):
for j, value in enumerate(cols):
if value == "Q":
# q_postions.append([i, j])
# 1. 垂直攻击
if j in choices:
choices.remove(j)
# 2. 右斜攻击
rnj = j + (i - row)
if 0 <= rnj < n and rnj in choices:
choices.remove(rnj)
# 3. 左斜攻击
lnj = j - (i - row)
if 0 <= lnj < n and lnj in choices:
choices.remove(lnj)
# 从候选项中,依次选择第c列,做选择
for j in choices:
cur_row_s = ["."] * n
cur_row_s[j] = "Q"
path.append(cur_row_s)
backtrack(row+1)
path.pop()
backtrack(0)
return res