一、回溯算法是什么?
回溯算法:采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
找到一个可能存在的正确的答案;
在尝试了所有可能的分步方法后宣告该问题没有答案。
回溯算法是⼀种试探算法,与暴⼒搜索最⼤的区别:
在回溯算法中,是⼀步步向前试探,对每⼀步探测的情况评估,再决定是否继续,可避免⾛弯路
回溯算法的精华:
出现⾮法的情况时,可退到之前的情景,可返回⼀步或多步
再去尝试别的路径和办法
想要采⽤回溯算法,就必须保证:每次都有多种尝试的可能
二、解题思路
1.决策树:下面讲解题会细说
解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
2.解决问题的套路
function fn(n) {
// 第⼀步:判断输⼊或者状态是否⾮法?
if (input/state is invalid) {
return;
}
// 第⼆步:判读递归是否应当结束?
if (match condition) {
return some value;
}
// 遍历所有可能出现的情况
for (all possible cases) {
// 第三步: 尝试下⼀步的可能性
solution.push(case)
// 递归
result = fn(m)
// 第四步:回溯到上⼀步
solution.pop(case)
}
}
⾸先判断当前情况是否⾮法,如果⾮法就⽴即返回
看看当前情况是否已经满⾜条件?如果是,就将当前结果保存起来并返回
在当前情况下,遍历所有可能出现的情况,并进⾏递归
递归完毕后,⽴即回溯,回溯的⽅法就是取消前⼀步进⾏的尝试
3、模板
List<List<Integer>> res = new LinkedList<>(); 返回结果集合(类型不限)
List<List<Integer>> zhuhanshu (条件){
集合 //记录路径
方法函数;
return res;//返回结果
}
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
# 做选择
将该选择从选择列表移除
路径.add(选择)
backtrack(路径, 选择列表)
# 撤销选择
路径.remove(选择)
将该选择再加入选择列表
三、详解全排列:
为了简单清晰起见,我们这次讨论的全排列问题不包含重复的数字。
1、决策树:
从根遍历这棵树,记录路径上的数字,其实就是所有的全排列。
你现在就在做决策,可以选择 1 那条树枝,也可以选择 3 那条树枝。为啥只能在 1 和 3 之中选择呢?因为 2 这个树枝在你身后,这个选择你之前做过了,而全排列是不允许重复使用数字的。
我们定义的 方法 函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层,其「路径」就是一个全排列。
2、全排列代码:
List<List<Integer>> res = new LinkedList<>();
/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
// 记录「路径」
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums, track);
return res;
}
// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
// 触发结束条件
if (track.size() == nums.length) {
res.add(new LinkedList(track));//添加的时候新建链表,因为每次回溯track也在变
return;
}
for (int i = 0; i < nums.length; i++) {
// 排除不合法的选择
if (track.contains(nums[i]))
continue;
// 做选择
track.add(nums[i]);
// 进入下一层决策树
backtrack(nums, track);
// 取消选择
track.removeLast();
}
}
四、经典问题:
1、组合总和
给定⼀个⽆重复元素的数组 candidates 和⼀个⽬标数 target ,找
出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以⽆限制重复被选取。
说明:
• 所有数字(包括 target)都是正整数。
• 解集不能包含重复的组合
int[][] combinationSum(int[] candidates, int target) {
int[][] results;
backtracking(candidates, target, 0, [], results);
return results;
}
void backtracking = (int[] candidates, int target, int start, int[]
solution, int[][] results) => {
if (target < 0) {
return;
}
if (target === 0) {
results.push(solution);
return;
}
for (int i = start; i < candidates.length; i++) {
solution.push(candidates[i]);
backtracking(candidates, target - candidates[i], i, solution,
results);
solution.pop();
}
}
2、N 皇后 II
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
boolean check(int row, int col, int[] columns) {
for (int r = 0; r < row; r++) {
if (columns[r] == col || row - r ==
Math.abs(columns[r] - col)) {
return false;
}
}
return true;
}
int count;
int totalNQueens(int n) {
count = 0;
backtracking(n, 0, new int[n]);
return count;
}
void backtracking(int n, int row, int[] columns) {
// 是否在所有n⾏⾥都摆放好了皇后?
if (row == n) {
count++; // 找到了新的摆放⽅法
return;
}
// 尝试着将皇后放置在当前⾏中的每⼀列
for (int col = 0; col < n; col++) {
columns[row] = col;
// 检查是否合法,如果合法就继续到下⼀⾏
if (check(row, col, columns)) {
backtracking(n, row + 1, columns);
}
// 如果不合法,就不要把皇后放在这列中(回溯)
columns[row] = -1;
}
}
总结
做题的时候,建议 先画树形图 ,画图能帮助我们想清楚递归结构,想清楚如何剪枝。拿题目中的示例,想一想人是怎么做的,一般这样下来,这棵递归树都不难画出。
在画图的过程中思考清楚:
分支如何产生;
题目需要的解在哪里?是在叶子结点、还是在非叶子结点、还是在从跟结点到叶子结点的路径?
哪些搜索会产生不需要的解的?例如:产生重复是什么原因,如果在浅层就知道这个分支不能产生需要的结果,应该提前剪枝,剪枝的条件是什么,代码怎么写?