回溯原理
-
深度优先搜索:
回溯算法在解决问题时,通常从根节点开始,先选择一条路线(选择一种解决方案)深入到底,也就是说,它优先搜索树或图的深度。 -
遇到阻碍就回退:
当搜索到某一节点时,如果判断这个节点无法达到目标(满足结束条件),就会放弃对这条路线的深入,回溯到上一层,尝试其他可能的路径。 -
穷举所有路径:
回溯算法会逐个试遍所有可能的解决方案,直到找出所有满足条件的解,或者搜索完整棵树(所有可能的路径)。 -
避免重复和剪枝:
为了提高效率,回溯算法在实际操作中,会加入一些剪枝操作,比如某些情况下,如果早点发现这条路径不可能是正确解,就可以提前结束这次搜索,避免无效的遍历。同时,对已经遍历过的状态,一般会进行标记,避免重复遍历。
回溯三部曲
- 确定参数返回值
- 确定终止条件
- 单层循环逻辑
回溯模版
def backtracking(args):
if '终止条件':
# 存放结果
return
for '选择' in '本层集合中元素':
# 处理节点
backtracking('路径', '选择列表') # 递归
# 回溯,撤销处理结果
回溯问题分类
- 组合问题
- 切割问题
- 子集问题
- 排列问题
- 棋盘问题(还没开始)
题目 77. 组合
问题描述
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
解题思路
代码
- 未剪枝优化
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
result=[]
self.backtracking(n,k,1,[],result)
return result
def backtracking(self,n,k,startIndex,path,result):
#终止条件
if len(path)==k:
result.append(path[:])
return
#单层递归逻辑
for i in range(startIndex, n+1):
path.append(i)
self.backtracking(n,k,i+1,path,result)
path.pop()
递归过程详解
调用层级 | i | path | result | 描述 |
---|---|---|---|---|
1 | 1 | [1] | [] | 从1开始, 将1添加到路径中 |
2 | 2 | [1, 2] | [] | 再从2开始, 将2添加到路径中,路径长度等于2,存入结果中 |
1 | 1 | [1] | [[1, 2]] | 回溯到层级1, 撤销路径中的2 |
2 | 3 | [1, 3] | [[1, 2]] | 从3开始, 将3添加到路径中,路径长度等于2,存入结果中 |
1 | 1 | [1] | [[1, 2], [1, 3]] | 回溯到层级1, 撤销路径中的3 |
2 | 4 | [1, 4] | [[1, 2], [1, 3]] | 从4开始, 将4添加到路径中,路径长度等于2,存入结果中 |
1 | 1 | [1] | [[1, 2], [1, 3], [1, 4]] | 回溯到层级1, 撤销路径中的4 |
1 | 1 | [] | [[1, 2], [1, 3], [1, 4]] | 回溯到层级1, 撤销路径中的1 |
1 | 2 | [2] | [[1, 2], [1, 3], [1, 4]] | 从2开始, 将2添加到路径中 |
… | … | … | … | 后面的步骤同理,会继续这样递归下去,直到所有的可能性都被遍历完 |
- 剪枝优化:
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。i就是for循环的起始位置:
- 已经选择的元素 path.size()
- 还需要选择的元素,k-path.size()
- n-(k-path.size)为剩下可以选的元素,应该要大于0,否则不够就要剪枝
- 举个例子,每次循环i都有个startindex,像需要4个元素的和为某个值,但是组合长度为5,startindex等于2,后面的元素都凑不齐4个。当startindex等于1,即第二个元素的时候,刚好凑齐4个元素,所以我们要找到刚好能凑齐目标个数的最大startindex。
- 这个最大startindex就等于,n-(还需要选择的个数)
- 此题注意,因为是append(i),所以i只能从1开始选取,range的最大值也要相应加1,n-(k-len(path))+2
这里是引用
- 可以看到在第一个循环中i只能取1,因为从2开始都凑不成4个,所以从2开始不能取, 第一个循环为for i=(0,4-(4-0)+1),(0,1)
- 第二层已经选了1个(len(path)),所以还要选3个(k-len(path)),最大能从2开始选, index: n-(k-len(path)),但是 是 range,所以接下来要加1。
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
res=[]
self.backtracking(n,k,[],res,1)
return res
def backtracking(self,n,k,path,res,startIndex):
#终止条件
if len(path)==k:
res.append(path[:])
return
#单层递归逻辑
for i in range(startIndex, n-(k-len(path))+2):
path.append(i)
#backtracking
self.backtracking(n,k,path,res,i+1)
path.pop()