所谓的回溯,实际上就是一个决策树的遍历过程,一种选优搜索法,又称试探法。利用试探性的方法,在包含问题所有解的解空间树中,将可能的结果搜索一遍,从而获得满足条件的解。搜索过程采用深度遍历策略,并随时判定结点是否满足条件要求,满足要求就继续向下搜索,若不满足要求则回溯到上一层,这种解决问题的方法称为回溯法。
经常刷leetcode的同学应该深有感触。其他的基本定义这里就不做说明了,为了方便其他人了解,这里会提供一个具体的case,然后一步步带大家深入浅出的理解回溯算法。
废话不多说,直接上回溯算法框架。解决一个回溯问题,我们正常思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
代码方面,回溯算法的框架:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,怎么理解这个呢,看一下下面的例子就知道了。
例子:给定一个无重复元素的数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
输入:candidates = [2,3,5],
target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
我们按照上面的套路模版,来一步步解题:
1:题目给出的算法结构为
class Solution {
public List> combinationSum(int[] candidates, int target) {
}
}
2:首先题目要求返回的类型为 List>,那么我们就新建一个 List
> 作为全局变量,最后将其返回。
class Solution {
List> lists = new ArrayList<>();
public List> combinationSum(int[] candidates, int target) {
return lists;
}
}
3:再看看返回的结构,List>。因此我们需要写一个包含 List
class Solution {
List> lists = new ArrayList<>();
public List> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0 || target < 0) {
return lists;
}
List
process(candidates, target, list);
return lists;
}
private void process(int[] candidates, int target, List
}
}
4:重点就是如何进行递归。递归的第一步,当然是写递归的终止条件啦,没有终止条件的递归会进入死循环。那么有 哪些终止条件呢?由于条件中说了都是正整数。因此,如果 target<0,当然是要终止了,如果 target==0,说明此时找到了一组数的和为 target,将其加进去。此时代码结构变成了这样。
class Solution {
List> lists = new ArrayList<>();
public List> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0 || target < 0) {
return lists;
}
List
process(candidates, target, list);
return lists;
}
private void process(int[] candidates, int target, List
if (target < 0) {
return;
}
if (target == 0) {
lists.add(new ArrayList<>(list));
}
}
}
5:我们是要求组成 target
的组合。因此需要一个循环来进行遍历。每遍历一次,将此数加入 list
,然后进行下一轮递归。在每次递归完成,我们要进行一次回溯。把最新加的那个数删除(java 中除了几个基本类型,其他的类型可以算作引用传递。如果不删,会导致 list 数字一直变多。因此,在每次递归完成,我们要进行一次回溯。把最新加的那个数删除。
)。此时代码结构变成这样。
class Solution {
List> lists = new ArrayList<>();
public List> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0 || target < 0) {
return lists;
}
List
process(candidates, target, list);
return lists;
}
private void process(int[] candidates, int target, List
if (target < 0) {
return;
}
if (target == 0) {
lists.add(new ArrayList<>(list));
} else {
for (int i = 0; i < candidates.length; i++) {
list.add(candidates[i]);
//因为每个数字都可以使用无数次,所以递归还可以从当前元素开始
process( candidates, target - candidates[i], list);
list.remove(list.size() - 1);
}
}
}
}
6:看代码结果不难能看出,本次结果的主要问题包含了重复的组合。为什么会有重复的组合呢?因为每次递归我们都是从 0 开始,所有数字都遍历一遍。所以会出现重复的组合。改进一下,只需加一个 start 变量即可。最后的代码:
List> lists = new ArrayList<>();
public List> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0 || target < 0) {
return lists;
}
List
process(0, candidates, target, list);
return lists;
}
private void process(int start, int[] candidates, int target, List
//递归的终止条件
if (target < 0) {
return;
}
if (target == 0) {
lists.add(new ArrayList<>(list));
} else {
for (int i = start; i < candidates.length; i++) {
list.add(candidates[i]);
//因为每个数字都可以使用无数次,所以递归还可以从当前元素开始
process(i, candidates, target - candidates[i], list);
list.remove(list.size() - 1);
}
}
}