一、核心知识点清单
1. 排列组合问题分类
-
排列问题(顺序相关):如全排列(LeetCode 46)。
-
组合问题(顺序无关):如子集(LeetCode 78)。
-
关键区别:是否考虑元素的顺序。
2. 回溯模板强化
python
def backtrack(path, choices):
if 满足终止条件:
记录结果
return
for 选择 in choices:
if 选择不合法: # 剪枝
continue
path.append(选择) # 做选择
backtrack(path, 新choices) # 递归
path.pop() # 撤销选择(回溯)
3. 去重技巧
-
排序+跳过重复元素:适用于输入含重复数字的情况(如LeetCode 47)。
-
示例代码:
pythonnums.sort() # 先排序 for i in range(len(nums)): if i > 0 and nums[i] == nums[i-1] and not used[i-1]: continue # 跳过重复分支
4. 剪枝优化场景
-
提前终止:当路径不可能满足条件时(如组合总和超过目标值)。
-
跳过无效分支:如全排列中已使用的数字不再选择。
5. 视频教程:
6. 图文教程:
二、学习安排(2小时)
第1个30分钟:理解排列与组合的区别
对比练习:
-
全排列([1,2,3] → 6种顺序不同的解)。
\[1,2,3\],\[1,3,2\],\[2,1,3\],\[2,3,1\],\[3,1,2\],\[3,2,1\]
-
子集([1,2,3] → 8种包含不同元素的子集)。
\[空集\],\[1\],\[2\],\[3\],\[1,2\],\[1,3\],\[2,3\],\[1,2,3\]
显然**[1,2] [2,1]是两个排列,但是为同一个组合。**
画决策树:
- 用纸笔画出输入为[1,2]时的全排列和子集决策树,体会回溯过程。
第2个30分钟:全排列代码实现
-
题目:LeetCode 46. 全排列(无重复数字)。
-
代码框架:
pythondef permute(nums): def backtrack(path): if len(path) == len(nums): res.append(path.copy()) return for num in nums: if num in path: # 剪枝:已使用的数字跳过 continue path.append(num) backtrack(path) path.pop() res = [] backtrack([]) return res
-
关键点 :用
path
记录已选数字,通过if num in path
剪枝。
-
值得注意的是上面的代码在力扣的运行中频繁报错AttributeError: 'list' object has no attribute 'copy', 这不禁联想到相同情况之前引用split报错,这里的原因是访问类型的不匹配,就好比split中我们试图将字符串分隔,但可能我们要操作的对象并不是字符串类型,而是列表类型。后面我将这句copy改成append("".join(path))也是报错。这里代码尝试把整数列表path连接成字符串。不过join方法要求传入的可迭代对象元素是字符串,而path中的元素是整数,所以会引发 TypeError
。
所以可以得出这一块我们正在操作的是列表类型下的整数元素,最终将代码改成append(path[:]),即为正确答案。
这里不断修改类型是因为参考的是力扣的题目,有输入输出类型的限制。
:type nums: List[int]
:rtype: List[List[int]]
第3个30分钟:子集问题与去重
-
题目:LeetCode 78. 子集。
-
代码框架:
pythondef subsets(nums): def backtrack(start, path): res.append(path[:]) # 所有节点均记录结果 for i in range(start, len(nums)): # 避免重复选择 path.append(nums[i]) backtrack(i + 1, path) # 从i+1开始选 path.pop() res = [] backtrack(0, []) return res
-
与全排列的区别 :通过
start
参数控制选择范围,避免顺序不同但元素相同的重复解。
-
第4个30分钟:全代码去重问题
-
题目:LeetCode 47. 全排列2。
-
剪枝技巧:标记已使用过的数组、相同数字按顺序使用避免重复。
-
代码片段:
pythonclass Solution(object): def permuteUnique(self, nums): """ :type nums: List[int] :rtype: List[List[int]] """ def backtrack(path, used): if len(path) == len(nums): res.append(path[:]) return for i in range(len(nums)): # 剪枝条件: # 1. 当前数字已使用过 # 2. 当前数字与前一个数字相同,且前一个数字未被使用(保证相同数字的顺序) if used[i] or (i > 0 and nums[i] == nums[i-1] and not used[i-1]): continue used[i] = True path.append(nums[i]) backtrack(path, used) path.pop() used[i] = False nums.sort() # 必须先排序,使相同数字相邻 res = [] used = [False] * len(nums) backtrack([], used) return res
-
i > 0 and nums[i] == nums[i-1] and not used[i-1]
:当前数字与前一个数字相同且前一个数字未被使用(保证相同数字按顺序使用,避免重复)
全排序1中代码直接检查num in path
来判断是否使用过,这在有重复数字时会失效。
三、常见错误与调试技巧
-
忘记回溯:
-
错误示例 :在
path.append(num)
后忘记path.pop()
,导致结果重复。 -
调试方法 :在递归前后打印
path
,观察状态变化。
-
-
去重逻辑错误:
-
案例:LeetCode 47(含重复数字的全排列)中未排序直接跳过重复数。
-
修正 :必须先排序,再判断
nums[i] == nums[i-1]
。
-
文章感谢各个CSDN博主、b站up主等多位大佬的细心讲解,以上是我的一些笔记。