回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。
回溯法的效率
因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
回溯法解决的问题
回溯法,一般可以解决如下几种问题:
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等
回溯法模板
- 什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
- for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。
backtracking这里自己调用自己,实现递归。从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
77. 组合 - 🔗
讲解 - 🔗
💡递归/回溯三部曲:
1. 明确递归函数参数和返回值
在递归函数中,我们需要一个startIndex
去记录每次循环的起始位置,因为元素不能重复,例如[1, 1]
是不合规的,因此第二个元素不能从1开始,而应该从2开始。用self.res
记录最终结果,无需返回值。
2.明确终止条件
用self.path
去记录每一条路径,当len(self.path) == k
时,说明该路径已经走完了,无需继续递归,因此将路径添加到self.res
中。
3. 单层递归逻辑
进入单层递归说明不满足终止条件,也就是路径还没满,因此需要继续向self.path
中添加元素,直至len(self.path) == k
,所以继续调用递归函数。在这里也需要思考当下一层递归遇到终止条件返回时,应当进行回溯,将self.path
的最后一个元素pop(),这样就可以实现[1, 2] -> [1] -> [1, 3]的过程。
class Solution:
def __init__(self):
self.res = []
self.path = []
def backtracking(self, n, k, startIndex):
if len(self.path) == k:
self.res.append(self.path[:])
return
for i in range(startIndex, n + 1):
self.path.append(i)
self.backtracking(n, k, i + 1)
self.path.pop()
def combine(self, n: int, k: int) -> List[List[int]]:
self.backtracking(n, k, 1)
return self.res