算法思想
回溯实际上是一种试探算法,这种算法跟暴力搜索最大的不同在于,在回溯算法里,是一步一步地小心翼翼地进行向前试探,会对每一步探测到的情况进行评估,如果当前的情况已经无法满足要求,那么就没有必要继续进行下去,也就是说,它可以帮助我们避免走很多的弯路。
回溯算法的特点在于,当出现非法的情况时,算法可以回退到之前的情景,可以是返回一步,有时候甚至可以返回多步,然后再去尝试别的路径和办法。这也就意味着,想要采用回溯算法,就必须保证,每次都有多种尝试的可能。
解题步骤
-
判断当前情况是否非法,如果非法就立即返回;
-
当前情况是否已经满足递归结束条件,如果是就将当前结果保存起来并返回;
-
当前情况下,遍历所有可能出现的情况并进行下一步的尝试;
-
递归完毕后,立即回溯,回溯的方法就是取消前一步进行的尝试。
代码模板
function fn(n) {
// 第一步:判断输入或者状态是否非法?
if (input/state is invalid) {
return;
}
// 第二步:判读递归是否应当结束?
if (match condition) {
return some value;
}
// 遍历所有可能出现的情况
for (all possible cases) {
// 第三步: 尝试下一步的可能性
solution.push(case)
// 递归
result = fn(m)
// 第四步:回溯到上一步
solution.pop(case)
}
}
例子
Leetcode39:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
说明:
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合。
画图
假设candidates=[2,3,6,7],target=7,画图如下:

为了简便,我们只画了2的子节点。遍历顺序是2->2,2->2,2,2->2,2,2,2,返回上一层,接着遍历2,2,2,3->2,2,2,6->2,2,2,7。2,2,2的子节点遍历完,返回到2,2,开始遍历2,2,3->2,2,6->2,2,7。然后返回到2,像这样一直遍历下去即可。遇到满足要求的节点(2,2,3和7)就保存。
你有没有发现这就是深度优先遍历?
代码如下:
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
res=[]
candidates.sort()
cur=[]
def back(start):
if sum(cur)>target:
return
if sum(cur)==target:
res.append(cur[:])
return
for i in range(start,len(candidates)):
cur.append(candidates[i])
back(i)
cur.pop()
back(0)
return res
首先定义res数组来保存最终结果,cur数组表示当前节点。在back函数里,检查cur元素总和是否已经超出了target。如果总和已经超出了target,就立即返回,去尝试其他的数值;如果总和刚好等于target,就把cur添加到结果中,然后返回。
在循环里用start记录当前元素位置,依次添加当前位置之后的元素,递归结束后pop当前元素,返回上一层。
剪枝
观察上面的图可以发现,有些节点是不需要遍历的,因为这是一个有序数组(无序的话可以先排序)。当遍历到2,2,2,2时,2,2,2,2的元素和比7大,2后面的元素都比它大,所以是不用再去遍历2,2,2,3->2,2,2,6->2,2,2,7的。因此就需要剪枝。

在代码里我们只需要在循环里加一个判断。假如cur元素和加上要遍历的下一个元素的值大于target,就跳出当前循环,返回上一层。
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
res=[]
candidates.sort()
cur=[]
def back(start):
if sum(cur)>target:
return
if sum(cur)==target:
res.append(cur[:])
return
for i in range(start,len(candidates)):
if sum(cur)+candidates[i]>target:
break
cur.append(candidates[i])
back(i)
cur.pop()
back(0)
return res
可以在循环里加一句print(cur)看一下结果。

而且我们可以发现,加上了这个判断后,我们连2,2,2,2(不满足条件)都不用遍历了。访问2,2,2后可以直接到2,2,3。
你明白了吗?

1万+

被折叠的 条评论
为什么被折叠?



