回溯算法理论基础
什么是回溯法
- 回溯法也可以叫做回溯搜索法,它是一种搜索的方式
- 回溯是递归的副产品,只要有递归就会有回溯
回溯法的效率
- 回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案
- 如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质
回溯法解决的问题
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
- 组合是不强调元素顺序的,排列是强调元素顺序。
如何理解回溯法
- 回溯法解决的问题都可以抽象为树形结构
- 因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。
- 递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。
回溯法模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
- for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历
77. 组合
回溯法三部曲
-
递归函数的返回值以及参数
在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。
-
回溯函数终止条件
什么时候到达所谓的叶子节点了呢?
path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。
-
单层搜索的过程
for循环每次从startIndex开始遍历,然后用path保存取到的节点i。
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
path = []
res = []
def _backtracking(n, k, start_index):
nonlocal path
nonlocal res
if len(path) == k:
res.append(path[:])
return
for i in range(start_index, n+1):
path.append(i)
_backtracking(n, k, i+1)
path.pop()
_backtracking(n, k, 1)
return res
剪枝
- 如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
path = []
res = []
def _backtracking(n, k, start_index):
nonlocal path
nonlocal res
if len(path) == k:
res.append(path[:])
return
# trim here
for i in range(start_index, n - (k - len(path)) + 2):
path.append(i)
_backtracking(n, k, i+1)
path.pop()
_backtracking(n, k, 1)
return res