回溯法通常用来解决决策和路径问题,使用回溯法来解决的的问题通常返回的结果都是一个集合,在计算的过程中还需要进行剪枝。解决回溯问题实际上就是一个决策树的遍历过程,需要思考下面三个问题:
- 路径:也就是已经做出的选择
- 选择列表:也就是你当前可以做的选择,通常有一个已选择列表和一个可选择列表
- 结束条件:也就是到达决策树的底层,无法再做出选择时(通常会在这里做判断,遍历到当前的路径是否满足条件,如果满足则加入到结果集合里面)
其一般的解题模板如下:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其实会发现这里和DFS的伪代码很像,但是不同之处在于,DFS通常没有撤销选择那一步,DFS一般是一条道路走到黑就可以了,但回溯法因为会记录路径以及当前的选择或被选择情况,当走到其它同级的决策分支时需要将当前分支的决策选择给重置掉
回溯法典型案例
class Solution {
public List<List<Integer>> permute(int[] nums) {
ArrayList<List<Integer>> res = new ArrayList<List<Integer>>();
if (nums.length <0) {
return res;
}
dfs(nums, new HashSet<Integer>(), new ArrayList<Integer>(), res);
return res;
}
private void dfs(int[] nums, Set<Integer> useSet, List<Integer> path, List<List<Integer>> res) {
//dfs的终止条件,就是所有元素都用过了
if (useSet.size() == nums.length) {
res.add(path);
}
//dfs的处理逻辑,要将数组中的每个元素都用到
for (int i=0; i< nums.length; i++) {
//如果没有用过这个元素,则进行dfs
if (!useSet.contains(nums[i])) {
path.add(nums[i]);
useSet.add(nums[i]);
//这里要new一个新的出来,避免下面的清理冲突
dfs(nums, new HashSet<>(useSet), new ArrayList<>(path), res);
//清理资源,为其它的元素的dfs
useSet.remove(nums[i]);
path.remove(path.size()-1);
}
}
}
}
//这个题目和上一个区别就是元素存在重复的,因此就需要对元素进行排序和判重剪枝
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums.length == 0) {
return result;
}
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
dfs(nums, used, nums.length, result, new ArrayList<>());
return result;
}
private void dfs(int[] nums, boolean[] used, int depth, List<List<Integer>> res, List<Integer> path) {
if (depth == 0) {
res.add(path);
return;
}
for (int i=0; i<nums.length; i++) {
//相对于无重复的,这里增加对前一个已经遍历并撤销选中的分支进行减枝
if (i>0 && nums[i] == nums[i-1] && !used[i-1]) {
continue;
}
if (!used[i]) {
path.add(nums[i]);
used[i] = true;
dfs(nums, used, depth-1, res, new ArrayList<>(path));
used[i] = false;
path.remove(path.size()-1);
}
}
}
}
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
if (candidates.length == 0) {
return res;
}
Arrays.sort(candidates);
dfs(candidates,0, target, new ArrayList<>(), res);
return res;
}
private void dfs(int[] candidates,int begin, int target, List<Integer> path, List<List<Integer>> res) {
if (target == 0) {
res.add(path);
return;
}
if (target < candidates[0]) {
return;
}
for (int i=begin; i< candidates.length; i++) {
path.add(candidates[i]);
//当选了i之后,后面的元素就只从i开始,避免返回去又重复
dfs(candidates, i, target-candidates[i], new ArrayList<>(path), res);
path.remove(path.size()-1);
}
}
}
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
if (candidates.length == 0) {
return res;
}
Arrays.sort(candidates);
dfs(candidates,0, target, new ArrayList<>(), res);
return res;
}
private void dfs(int[] candidates,int begin, int target, List<Integer> path, List<List<Integer>> res) {
if (target == 0) {
res.add(path);
return;
}
for (int i=begin; i< candidates.length; i++) {
//剩下的元素都比target大,直接剪枝
if (target < candidates[begin]) {
return;
}
//去除重复的元素,进行剪枝
if (i> begin && candidates[i] == candidates[i-1]) {
continue;
}
path.add(candidates[i]);
//当选了i之后,后面的元素就只从i+1开始,避免i位置的元素重复使用
dfs(candidates, i+1, target-candidates[i], new ArrayList<>(path), res);
path.remove(path.size()-1);
}
}
}