零、回溯算法理论基础
参考:
回溯法解决的问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
理解回溯:
回溯法解决的问题都可以抽象为树形结构,因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。
回溯三部曲:
1. 回溯函数返回值以及参数
-
返回值一般为void
-
参数不易一次性确定,所以一般先写逻辑,然后需要什么参数,就填什么参数。
void backtracking(参数)
2. 回溯函数终止条件
一般搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。其伪代码为:
if (终止条件) {
存放结果;
return;
}
3. 回溯搜索的遍历过程
回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。for循环横向遍历数组中的每一个元素,递归纵向遍历剩余元素,并根据递归终止条件来收获最终结果。

void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
46.全排列
思路:
-
全排列是指数组中全部数字的所有排列方式,因此最终收获结果的条件是路径长度与数组本身长度相等。
-
整体思路如下图所示,用一个used数组来标记这个数字是否被使用,纵向递归,若某数字未被使用,则将其加入到路径并标记为已使用;横向循环,递归完成后进行回溯操作,把当前数字重新标记为未使用并移出路径。(看代码及注释可能更易理解)

注意:
使用 path[:] 而不是直接使用 path ,是为了避免引用问题,确保 result 中存储的是每个排列的独立副本,而不是对同一个列表的多次引用。
-
在 Python 中,列表是可变对象(mutable object)。当你将一个可变对象赋值给另一个变量时,这两个变量会指向同一个对象。
-
在回溯算法中,path 是一个动态变化的列表,它在递归过程中会被不断修改。如果直接将 path 添加到 result 中,那么 result 中存储的将是 path 的引用,而不是它的值。这意味着,当 path 被修改时,result 中对应的排列也会被修改。
-
path[:] 是一种简单且高效的方法来创建列表的浅拷贝。
参考:
初学非常好理解的卡哥:
再刷发现写的也不错的题解,把我第一次刷的问题写了出来,path的拷贝问题
代码:
python
class Solution:
def backtracking(self, nums, path, used, result):
# 最终的排列收获条件
if len(path) == len(nums):
result.append(path[:]) # 注意要创建浅拷贝
# 对数组循环,加入未被使用的数字
for i in range(len(nums)):
# 若已被使用,则跳过
if used[i]:
continue
# 标记为已使用,并加入路径
used[i] = True
path.append(nums[i])
# 递归
self.backtracking(nums, path, used, result)
# 回溯,当前轮次已收获完,重新将该数字标记为未使用,并弹出路径中的该元素
used[i] = False
path.pop()
def permute(self, nums: List[int]) -> List[List[int]]:
used = [False] * len(nums)
result = []
self.backtracking(nums, [], used, result)
return result
78.子集
回溯三部曲:
1. 递归函数参数
全局变量数组path为子集收集元素,二维数组result存放子集组合。(也可以放到递归函数参数里)。递归函数参数需要startIndex,因为是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始。

2. 终止条件
如上图,剩余集合为空的时候,就是叶子节点。即,startIndex大于数组的长度,就终止了,因为没有元素可取了。
3. 单层搜索逻辑
从startIndex,加入该数字,然后回溯。
代码:
python
class Solution:
def backtracking(self, nums, index, path, result):
result.append(path[:]) # 直接收获结果
if index > len(nums): # 其实可以不写,因为循环的index一直加到了len(nums)也会超,自动终止
return
for i in range(index, len(nums)):
path.append(nums[i])
self.backtracking(nums, i + 1, path, result)
path.pop()
def subsets(self, nums: List[int]) -> List[List[int]]:
result = []
self.backtracking(nums, 0, [], result)
return result
17.电话号码的字母组合
思路:
和之前的类似,只是多了一个数字和字母的对应关系,除此之外需要将数字字符转换为int类型。
代码:
python
class Solution:
def __init__(self):
# 创建映射表,按顺序对应0,1,...,9
self.letterMap = [
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz",
]
self.result = [] # 收集结果
self.s = "" # 收集路径
def backtracking(self, digits, index):
if index == len(digits):
self.result.append(self.s)
return
digit = int(digits[index]) # 字符转化为数字
letters = self.letterMap[digit] # 取对应字母用于遍历
for i in range(len(letters)):
self.s += letters[i]
self.backtracking(digits, index + 1)
self.s = self.s[:-1] # 回溯
def letterCombinations(self, digits: str) -> List[str]:
if len(digits) == 0:
return self.result
self.backtracking(digits, 0)
return self.result