好的,我们来总结一下使用深度优先搜索(DFS)解决排列问题、子集问题和组合问题的核心思路与代码模板。这三类问题都是回溯算法的经典应用场景,本质都是通过递归遍历所有可能的解空间,但在具体实现和问题特性上有所区别。
一、排列问题(Permutations)
问题特征 :求一个集合中所有元素的全排列 ,顺序不同视为不同解。
核心思路 :每次递归选择一个未被使用的元素加入当前路径,直到路径长度等于原集合长度。
关键点 :需要标记元素是否已被使用(如 visited 数组)。
代码模板
python
def permute(nums):
res = []
path = []
visited = [False] * len(nums) # 标记元素是否使用
def dfs():
if len(path) == len(nums): # 终止条件:路径长度等于集合大小
res.append(path.copy())
return
for i in range(len(nums)):
if visited[i]: continue # 跳过已使用的元素
visited[i] = True
path.append(nums[i])
dfs() # 递归下一层
path.pop() # 回溯
visited[i] = False
dfs()
return res
二、子集问题(Subsets)
问题特征 :求一个集合的所有子集 (包括空集),顺序无关。
核心思路 :每个元素有"选"或"不选"两种状态,递归遍历所有选择分支。
关键点:无需考虑顺序,通常按索引顺序递归避免重复。
代码模板
python
def subsets(nums):
res = []
path = []
def dfs(start): # start: 当前可选的起始索引
res.append(path.copy()) # 所有路径都是子集
for i in range(start, len(nums)):
path.append(nums[i])
dfs(i + 1) # 下一层从 i+1 开始选
path.pop() # 回溯
dfs(0)
return res
三、组合问题(Combinations)
问题特征 :从集合中选取 k 个元素的所有组合 (顺序无关)。
核心思路 :在子集问题基础上增加长度限制(len(path) == k)。
关键点:可通过剪枝(如剩余元素不足时提前终止)优化效率。
代码模板
python
def combine(n, k):
res = []
path = []
def dfs(start):
if len(path) == k: # 终止条件:路径长度等于 k
res.append(path.copy())
return
for i in range(start, n + 1):
# 剪枝:剩余元素不足时跳过
if n - i + 1 < k - len(path):
break
path.append(i)
dfs(i + 1) # 下一层从 i+1 开始选
path.pop() # 回溯
dfs(1)
return res
四、问题对比与总结
| 问题类型 | 核心区别 | 终止条件 | 去重方式 |
|---|---|---|---|
| 排列 | 顺序敏感 | len(path) == len(nums) |
visited 数组 |
| 子集 | 顺序无关 | 无固定终止(每一步都记录) | 按索引顺序递归(start 参数) |
| 组合 | 顺序无关 | len(path) == k |
按索引顺序递归(start 参数) |
五、补充说明
- 剪枝优化 :在组合问题中,可通过判断剩余元素数量提前终止循环(如
n - i + 1 < k - len(path))。 - 去重处理 :若原集合包含重复元素(如
[1,1,2]),需在循环中跳过重复值(如if i > start and nums[i] == nums[i-1]: continue)。 - 时间复杂度:排列(O(n!))、子集(O(2\^n))、组合(O(C_n\^k)),剪枝可显著减少实际递归次数。
通过灵活调整终止条件、元素选择范围和去重逻辑,可解决大多数变体问题(如重复元素、求和约束等)。