ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

最优子结构里藏着的秘密,远比我想象的要多

最优子结构里藏着的秘密,远比我想象的要多

回溯法(Backtracking)详解

回溯法是一种系统地搜索问题解的通用算法,通过深度优先搜索策略,在解空间中尝试所有可能的候选解。当发现当前选择无法通向有效解时,就回溯到上一步,撤销该选择并尝试其他选项。

一、核心思想

回溯法本质上是暴力搜索 + 剪枝优化,其核心过程可以概括为:

  1. 路径:已经做出的选择

  2. 选择列表:当前可以做的选择

  3. 结束条件:到达决策树底部,或找到有效解

关键特性

  • 采用递归实现,递归深度等于决策层数

  • 通过撤销选择(状态重置)实现回溯

  • 可以剪枝提前终止无效分支的搜索

二、经典问题:N皇后问题

问题描述:在n × n的棋盘上放置n个皇后,使它们互不攻击(任意两个皇后不能在同一行、同一列或同一对角线上)。求所有合法放置方案。

三、最优子结构与回溯法的区别

特性最优子结构(动态规划)回溯法
目标求最优值(最大/最小)求所有可行解或一个可行解
依赖子问题最优解递推无依赖,独立尝试所有路径
存储通常用表格存储中间结果通常用递归栈或路径数组
效率多项式时间复杂度指数级时间复杂度
典型应用背包、最短路径八皇后、数独、排列组合

四、N皇后回溯法代码实现(Python)

python

def solveNQueens(n): """ 求解N皇后问题,返回所有合法棋盘布局 """ # 棋盘,'Q'表示皇后,'.'表示空位 board = [['.' for _ in range(n)] for _ in range(n)] result = [] # 存储所有解 # 辅助数组,用于O(1)时间判断冲突 cols = [False] * n # 列是否被占用 diag1 = [False] * (2*n - 1) # 主对角线(r - c + n - 1) diag2 = [False] * (2*n - 1) # 副对角线(r + c) def backtrack(row): """ 回溯函数:在第row行放置皇后 """ # 结束条件:所有行都放置完成 if row == n: # 将棋盘转换为字符串列表并加入结果 result.append([''.join(row) for row in board]) return # 遍历选择列表:当前行的所有列 for col in range(n): # 剪枝:检查当前位置是否合法 if cols[col] or diag1[row - col + n - 1] or diag2[row + col]: continue # 冲突,跳过此列 # 做选择:放置皇后 board[row][col] = 'Q' cols[col] = True diag1[row - col + n - 1] = True diag2[row + col] = True # 递归:进入下一行 backtrack(row + 1) # 撤销选择(回溯):移除皇后,恢复状态 board[row][col] = '.' cols[col] = False diag1[row - col + n - 1] = False diag2[row + col] = False # 从第0行开始搜索 backtrack(0) return result

五、回溯法代码结构详解

1. 核心框架(伪代码)

text

def backtrack(路径, 选择列表): if 满足结束条件: 记录结果 return for 选择 in 选择列表: # 剪枝(可选) if 选择不合法: continue # 1. 做选择 将选择加入路径 # 2. 递归进入下一层 backtrack(新路径, 新的选择列表) # 3. 撤销选择(回溯) 将选择从路径中移除

2. 关键要素说明

要素说明示例(N皇后)
路径已做出的选择集合前row行已放置的皇后位置
选择列表当前层可用的选项当前行的n个列
结束条件到达决策树底部row == n(所有行都放完)
剪枝条件提前排除无效选择列或对角线冲突
撤销操作恢复状态,用于回溯移除皇后,重置标志

六、另一个经典示例:全排列问题

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

python

def permute(nums): """ 生成数组的所有全排列 """ 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皇后:O(n!),因为每行可选择的列数递减

  • 全排列:O(n × n!),n!个排列,每个需要复制路径

空间复杂度

  • 递归栈:O(深度),最大等于决策树高度

  • 路径存储:O(深度) 用于存储当前路径

  • 结果存储:O(解的数量 × 每个解的大小)

八、回溯法 vs 其他算法对比

算法适用场景时间复杂度空间复杂度典型问题
回溯法组合优化、约束满足指数级O(深度)N皇后、数独
动态规划最优子结构、重叠子问题多项式O(状态数)背包、最短路径
贪心算法局部最优即全局最优线性/多项式O(1)活动选择、霍夫曼编码
分支限界带约束的优化问题指数级(但有界)O(搜索树大小)旅行商问题

九、回溯法的优化技巧

1. 剪枝(Pruning)

python

# 示例:数独中的剪枝 def is_valid(board, row, col, num): # 检查行、列、3x3宫格 for i in range(9): if board[row][i] == num: return False if board[i][col] == num: return False if board[3*(row//3) + i//3][3*(col//3) + i%3] == num: return False return True

2. 排序优化

python

# 对选择列表排序,优先尝试约束性强的选择 nums.sort() # 或按某种启发式排序

3. 记忆化(Memoization)

python

# 对于重复子问题,可以结合记忆化 memo = set() def backtrack(state): if state in memo: return memo.add(state) # ... 继续搜索

十、总结

回溯法的核心公式

text

回溯 = 递归 + 深度优先搜索 + 状态重置

使用场景识别

  • 需要所有解一个解

  • 问题可以分解为多步决策

  • 每一步有有限的选择

  • 约束条件需要满足

代码实现模板

  1. 定义递归函数,参数包含当前状态

  2. 编写结束条件

  3. 遍历所有选择

  4. 剪枝排除非法选择

  5. 做选择→递归→撤销选择

返回列表