回溯算法,采用试错的思想,尝试分步地去解决一个问题;在分步解决问题的过程中,当它发现现有的分步答案不能得到有效的正确的解答时,它将取消上一步甚至是上几步的计算;再通过其他的可能的分步解答再次尝试寻找问题的解答。
回溯算法通常使用递归的方式实现,在重复上述步骤之后,会有两种结果:
回溯算法实际上是深度优先搜索的一种特殊情况
深度优先搜索(
DFS
,Depth-First-Search
):
是一种用于遍历或搜索树或者图的算法,这个算法会 尽可能深 地搜索树的分支。
- 当节点
v
的所在的一条边的所有其他节点都已经被探寻过之后,搜索将回溯到节点v
,去探寻从节点v
出发的其他所在边;- 上述过程一直进行到已经发现从源节点可以达到的所有节点为止;
- 如果此时,还存在未探寻过的节点,则选择其中一个作为源节点,重复上述过程;
- 直到所有的节点都被访问到为止
广度优先搜索(
BFS
,Breadth-First-Search
) :
也是一种用于遍历或搜索树或者图的算法,这个算法会 尽可能广 地搜索节点的邻居。
将一个节点看作是源节点,按与源节点相邻节点数量作为标准,将其他节点分为一层节点(邻居节点),二层节点(邻居节点的邻居节点),逐层探寻所有节点
共同点:
用于求解多阶段决策问题
多阶段决策问题,即,
不同点:
由于回溯算法的时间复杂度很高,因此在遍历的时候,如果能够提前知道这一条分支不能搜索到满意的结果,就可以提前结束,这一步操作称为 剪枝
回溯算法应用 剪枝 技巧可以加快搜索速度。有些时候,需要做一些预处理工作(例如排序)才能达到剪枝的目的。预处理工作虽然也消耗时间,但能够剪枝节约的时间更多;
提示:
剪枝是一种技巧,通常需要根据不同问题场景采用不同的剪枝策略,需要在做题的过程中不断总结。
由于回溯问题本身时间复杂度就很高,所以能用空间换时间就尽量使用空间。
leetcode 46题
给定一个 没有重复 数字的序列,返回其所有可能的全排列
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
这个问题可以看作是,对于 n n n 个排成一行的空格,从左往右依次填入序列中给定的 n n n 个数字,每个数字只能使用一次
因此,可以使用回溯算法模拟穷举过程,即,从左到右每一个位置都依次尝试填入一个数字,看能不能填完 n n n 个空格
可以定义一个递归函数 BackTrack(int begin, int len, List
表示从左往右填到第 > result)
begin
位置,当前排列为 permutation
,len
表示 n
个数字序列的长度, result
表示所有排列的序列
那么递归函数中存在两种情况,
begin == len
,则说明已经填写完成 n n n 个位置(下标从 0 0 0 开始),即找到了一个可行解,将此时的 permutation
放入 result
中,递归结束;begin < len
,则需要考虑第 b e g i n begin begin 个位置填写的数字;遍历题目中给出的还没有填写的数字,尝试填写,然后继续尝试填写下一个位置,即调用函数 BackTrack(begin + 1, len, permutation, result);
;回溯时,撤销填写的数字,并继续尝试其他没有被标记过的数字因为重复遍历寻找没有填写过的数字效率不高,复杂度也比较高,所以可以尝试一种简化方法;
将题目给定的 n n n 个数字的序列分为左右两部分,左边表示已经填过的数字,右边表示待填的数字,在回溯过程中维护这个数组即可。
假设,已经填到第 b e g i n begin begin 个位置,那么 nums
序列中的 [ 0 , b e g i n − 1 ] [0, begin - 1] [0,begin−1] 是已经填过的数字的集合, [ b e g i n , n − 1 ] [begin, n - 1] [begin,n−1] 是待填的数字集合;
用 [ b e g i n , n − 1 ] [begin, n−1] [begin,n−1] 里的数去填第 b e g i n begin begin 个数,假设待填的数的下标为 i i i ,那么填完以后我们将第 i i i 个数和第 b e g i n begin begin 个数交换,即能使得在填第 b e g i n + 1 begin + 1 begin+1 个数的时候, nums
序列的 [ 0 , b e g i n ] [0, begin] [0,begin] 部分为已填过的数, [ b e g i n + 1 , n − 1 ] [begin + 1, n−1] [begin+1,n−1] 为待填的数,回溯的时候交换回来即能完成撤销操作。
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<List<Integer>> ();
List<Integer> permutation = new ArrayList<Integer>();
for(int num : nums) {
permutation.add(num);
}
int len = nums.length;
BackTrack(0, len, permutation, result);
return result;
}
public void BackTrack(int begin, int len, List<Integer> permutation, List<List<Integer>> result) {
// 所有的数字都填完了
if(begin == len) {
result.add(new ArrayList<Integer>(permutation));
}
for(int i = begin; i < len; i++) {
// 动态维护数组
Collections.swap(permutation, begin, i);
// 继续递归填下一个数字
BackTrack(begin + 1, len, permutation, result);
// 撤销
Collections.swap(permutation, begin, i);
}
}
}
permutation
是对象类型,不同于基本数据类型
result.add(permutation)
是把permutation
的 地址添加到result
中;
new ArrayList<>()
的作用是复制,把遍历到叶子节点的时候的permutation
的样子复制出来,到一个新的列表,实现题目要求
给定一个无重复元素的数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的数字可以无限制重复被选取。
说明:
所有数字(包括target
)都是正整数。
解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
提示:
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都是独一无二的。
1 <= target <= 500
定义递归函数 dfs(candidates, target, res, combine, index)
表示当前在 candidates
数组的第 index
位还剩下 target
要组合,已经组合的列表为 combine
递归的终止条件是 target <= 0
或者 candidates
数组已经用完了
那么,在当前的函数中,每次我们可以选择跳过或者不跳过第 index
个数,执行代码分别为 dfs(candidates, target, res, combine, index + 1)
和 dfs(candidates, target - candidates[index], res, combine, index)
;因为每个数字都可以被无限制重复选取,则搜索的下标仍然是 index
不考虑剪枝:
class Solution {
/*
combine 是对象类型,不同于基本数据类型
res.add(combine) 是把 combine 的 地址添加到 res 中
而combine 在遍历之后为空列表,最后只会看到 res 中是一个又一个的空列表,都指向一块内存
new ArrayList<>() 的作用是复制,把遍历到叶子节点的时候的 combine 的样子复制出来,到一个新的列表,实现题目要求
*/
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
List<Integer> combine = new ArrayList<Integer>();
dfs(candidates, target, res, combine, 0);
return res;
}
public void dfs(int[] candidates, int target, List<List<Integer>> res, List<Integer> combine, int index) {
if(index == candidates.length) {
return;
}
if(target == 0) {
res.add(new ArrayList<Integer>(combine));
return;
}
// 直接跳过当前元素
dfs(candidates, target, res, combine, index + 1);
// 选择当前元素
if(target - candidates[index] >= 0) {
combine.add(candidates[index]);
dfs(candidates, target - candidates[index], res, combine, index);
combine.remove(combine.size() - 1);
}
}
}
考虑剪枝:
public class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
int len = candidates.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}
// 排序是剪枝的前提
Arrays.sort(candidates);
Deque<Integer> path = new ArrayDeque<>();
dfs(candidates, 0, len, target, path, res);
return res;
}
private void dfs(int[] candidates, int begin, int len, int target, Deque<Integer> path, List<List<Integer>> res) {
// 由于进入更深层的时候,小于 0 的部分被剪枝,因此递归终止条件值只判断等于 0 的情况
if (target == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = begin; i < len; i++) {
// 重点理解这里剪枝,前提是候选数组已经有序,
if (target - candidates[i] < 0) {
break;
}
path.addLast(candidates[i]);
dfs(candidates, i, len, target - candidates[i], path, res);
path.removeLast();
}
}
}
给你一个整数数组 nums
,数组中的元素互不相同 。返回该数组所有可能的子集(幂集)。
解集不能包含重复的子集。你可以按任意顺序返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums 中的所有元素 互不相同
我们同样可以定义一个函数 dfs(int cur, int[] nums)
表示对子集的构成中,是否选择第 cur
个元素
使用 ans
表示最终包含所有子集的解集,使用 t
表示当前的子集
显然,当 cur == nums.length
时, 数组中的所有元素都已经被选择过(可能被放入子集,也可能没有被放入子集),解集 ans
的元素加 1,并且退出函数
对于第 cur
个元素,如果选择放入子集,则,先增加子集中的元素,然后执行代码 dfs(cur + 1, nums)
,表示对下一个元素是否放入子集进行选择;
对于第 cur
个元素,如果选择不放入子集,则,可以直接执行代码 dfs(cur + 1, nums)
,表示对下一个元素是否放入子集进行选择;
class Solution {
List<Integer> t = new ArrayList<Integer>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
public List<List<Integer>> subsets(int[] nums) {
dfs(0, nums);
return ans;
}
public void dfs(int cur, int[] nums) {
if (cur == nums.length) {
ans.add(new ArrayList<Integer>(t));
return;
}
t.add(nums[cur]);
dfs(cur + 1, nums);
t.remove(t.size() - 1);
dfs(cur + 1, nums);
}
}