回溯算法
回溯算法是一步步的向前试探,对每一步的情况进行分析,再决定是否继续。
回溯算法主要解决求解一个问题有多少种可能的解法。
核心思想
- 当出现非法情况时,回退到之前的场景,可以返回一步或者多步。再去尝试别的路径和办法。
- 回溯算法必须保证每次都有一种或多种尝试的可能。
模板
f(n)是回溯方法
1)判断当前输入是否合法,不合法就返回
2)判断递归是否应当结束(一般是一种可能的解)
3)遍历所有可能出现的情况,尝试下一步的可能性
4)调用递归
5)回溯到上一步(取消前部进行的尝试)
例:
给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。 (力扣39)
这是一个标准的回溯算法例子,其他回溯算法可以参考这里的模板。
List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
backtracking(candidates, res, new ArrayList<>(), 0, target, 0);
return res;
}
void backtracking(int[] candidates, List<List<Integer>> res, List<Integer> solution, int sum, int target, int index){
//判断输入是否合法,和大于目标值,表示不合法
if (sum > target){
return;
}
//等于目标值,表示一种有效的解,保存下来
if (target == sum){
res.add(new ArrayList<>(solution));
return;
}
for(int i = index; i < candidates.length; i++){
//加入一个元素,尝试下一步
solution.add(candidates[i]);
//调用递归
backtracking(candidates, res, solution, sum+candidates[i], target, i);
//一般是递归return后,执行这一步,表示找到一个合理的解或者
//不合理的解,将上一步的场景回溯,尝试其他解
solution.remove(solution.size() - 1);
}
}
剪枝
当回溯时,某些条件不满足,就要提前终止回溯操作,进行下一次循环。这个操作叫做剪枝,一般剪枝是在for循环中进行的,满足剪枝条件使用continue跳出本次循环,进行下次循环。
比如N皇后问题,当皇后所在的位置的对角线或者本列有皇后时,我们需要continue跳出本次循环,对下一列进行查找,判断是否满足条件。
同时要注意回溯操作要和前面的对称。
for(int i = 0; i < n; i++){
//剪枝操作,判断当前列已有皇后,进行下一列搜索
if(column.contains(i)){
continue;
}
//剪枝操作,判断斜45度是否有皇后
int sum1 = row + i;
if(d1.contains(sum1)){
continue;
}
//剪枝操作,判断斜135度是否有皇后
int sum2 = row - i;
if (d2.contains(sum2)){
continue;
}
solution.add(i);
column.add(i);
d1.add(sum1);
d2.add(sum2);
dfs1(res, n, solution,row+1, column, d1, d2);
//注意,回溯操作要和前面的对称
d2.remove(d2.size() - 1);
d1.remove(d1.size() - 1);
column.remove(column.size() - 1);
solution.remove(solution.size() - 1);
}