回溯算法

  1. 1. 1. 什么是回溯法
  2. 2. 2. 什么情况需要回溯
  3. 3. 3. 回溯模板(java)
  4. 4. 4. 名词释义
  5. 5. 5. 例子
    1. 5.1. 5.1 LeetCode40 组合总和 II
    2. 5.2. 5.2 LeetCode46 全排列

回溯(backtrack)简介

1. 什么是回溯法

  回溯(backtrack)是按条件沿着树的深度去搜索,直到达到满足目标条件或不再可能达到目标,就回退一步树的深度,选择树的另一个分支,这种走不通就退回的换分支技术为回溯。
  回溯的本质是一个穷举的深度优先搜索(DFS),每一种深搜都会对应一棵树,其时间复杂度较高,与穷举的区别在于满足条件或者知道后面不可能满足条件后,就进行回退,寻找另一种可能的解。其降低了重复的穷举的步骤,时间复杂度比穷举低。

2. 什么情况需要回溯

  当只需要最优解时与最终解时,很容易想到贪心与动态规划,但是一旦要给出所有可能的解,这时就应该使用回溯了。

3. 回溯模板(java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 并不是回溯,根据题目不同,会有不同
public List<List<Integer>> getResult(int[] nums) {
// 存储结果的List
List<List<Integer>> resList = new ArrayList<>();
// 存储临时结果、并用来回溯的List,这里使用了ArrayDeque(数组实现的队列),便于弹出最后一个元素。
Deque<Integer> tempDeque = new ArrayDeque;
// ? 代表可能有或没有
// ?1 : 这里指 记录元素是否被使用,可能是数组或者set,这个可能由该方法传入,也可能方法内部创建(如果此情况,每一次递归深搜都会创建),需要根据具体情况确定。
// ?2 : 这里指 需要达成的目标,有时目标需要传入,如:所有可能的target和。
backTrack(nums, 0, resList, tempDeque, ?1, ?2);
return resList;
}

public void backTrack(int[] nums, int deepth, List<List<Integer>> resList, Deque<Integer> tempDeque) {
if (到达终点/达到条件) {
// 按条件存放结果
resList.add(new ArrayList<Integer>(temList));
return;
}
if (越界或者不合法状态) { // 例如 deepth == nums.length;
return;
}
// 剪枝(降低遍历的时间复杂度,写更好,可以不写)
if (特殊状态) { // 例如,例如要找和为5的,此时结果已经大于5了
// 此时return,触发递归父级的回溯。
return;
}
// 真正的深度递归与回溯
for (扩展方式) { // 扩展方式可能多种,目前有 int i = 0; 也有int i = deepth的。。。
if (扩展方式达到合法状态) { // 有时回溯递归也可能在if判断之外
修改操作; // 根据题意
标记使用;
backTrack(nums, deepth + 1, resList, tempDeque, ?1, ?2); // 深度需要+1
还原标记;
tempList.pollLast(); // 回溯,将结果的最后一个元素弹出
}
}
}

4. 名词释义

  1. 回溯:搜索到结果后,撤销上一步的搜索,进行另一个可能结果的下一步搜索
  2. 剪枝:在不可继续搜索的情况下,绝对不可能满足目标情况下,提前return,触发递归父级的回溯,从而提前回溯。
  3. 记忆集:有时结果不需要重复的,这时可以用set存储结果,或者boolean数组标记对应的下标是否被使用,避免重复使用。
  4. 排序:由于回溯本质是一个优化过后的穷举,其时间复杂度较高,因此在处理之前,可以考虑对其进行O(NlogN)O(NlogN)时间复杂度排序,由于时间复杂度只取最高的,因此该时间复杂度可以忽略不记。

5. 例子

5.1 LeetCode40 组合总和 II

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。
注意:解集不能包含重复的组合。

例:输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<Integer> tempList = new ArrayList<>();
List<List<Integer>> resList = new ArrayList<>();
Arrays.sort(candidates);
backTrack(candidates, tempList, resList, target, 0);
return resList;
}

/**
* @param candidates: 给定的数组
* @param idx: 此时处于树地深度
* @param resList: 存储地最终结果
* @param temList: 存储地单个结果,用来回溯
* @param target: 要达到的目标结果
*/
public void backTrack(int[] candidates, List<Integer> tempList, List<List<Integer>> resList, int target, int idx) {
// target == 0时,达到结果了,添加结果,return触发父级的回溯
if (target == 0) {
resList.add(new ArrayList<Integer>(tempList));
return;
}
// 如果到达树的最深处,return触发父级的回溯
if (idx == candidates.length) {
return;
}

// set建立在回溯方法内部,因此每一次new都是不一样的,所以每一次递归都不一样
// 只有在出递归后,在同一级递归里,同一个for循环里才是一样的。(同一个递归层级里有多次for循环,这个多次的for循环用的是同一个set,但是递归后用的就是新的了)
Set<Integer> tempSet = new HashSet<>();
for (int i = idx; i < candidates.length; i++) {
// 如果接下来要进行比较的num比目标大,则之后的深度遍历永远不可能达到目标,进行return剪枝,触发父级回溯
if (candidates[i] > target) {
break;
}
// 如果包含已经使用的nums,则跳过该数
if (tempSet.contains(candidates[i])) {
continue;
}
tempList.add(candidates[i]);
// 将该数接入结果
tempSet.add(candidates[i]);
// 深度递归,target - num,同时深度 + 1
backTrack(candidates, tempList, resList, target - candidates[i], i + 1);
// 回溯
tempList.remove(tempList.size()-1);
}

}
}

5.2 LeetCode46 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

例:输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> resList = new ArrayList<>();
Deque<Integer> tempList = new ArrayDeque<>();
boolean[] used = new boolean[nums.length];
if (nums.length == 0) {
return resList;
}
backTrack(nums, 0, resList, tempList, used);
return resList;
}

/**
* @param nums: 给定的数组
* @param idx: 此时处于树地深度
* @param resList: 存储地最终结果
* @param temList: 存储地单个结果,用来回溯
* @param used: boolean 数组,用来标记对应下标的元素是否被使用
*/
public void backTrack(int[] nums, int idx, List<List<Integer>> resList, Deque<Integer> tempList, boolean[] used) {
// 如果到达最后,添加结果并return,触发父级的回溯。
if (idx == nums.length) {
resList.add(new ArrayList<Integer>(tempList));
return;
}
for (int i = 0; i < nums.length; i++) {
if (!used[i]) {
// 将该下下标的元素标记为使用
tempList.addLast(nums[i]);
used[i] = true;
// 深度递归
backTrack(nums, idx + 1, resList, tempList, used);
// 将该下标的元素标记为未使用
used[i] = false;
// 弹出最后一个元素,回溯
tempList.pollLast();
}
}
}
}