回溯法
最近接触到了有关回溯法的题目,一直都感觉有点困惑回溯法和dfs的区别和自身的程序特点(处于应试考虑,当然希望能够总结出相对通用的模板),今天leetcode"全排列"这道题,对回溯法感觉有了初步的认识,接下来我们用题来说话。
题目
给定一个没有重复数字的序列,返回其所有可能的全排列。
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutations
代码
public List<List<Integer>> permute(int[] nums) {
boolean[] visited = new boolean[nums.length];
perBackTracking(nums,visited,new ArrayList<>());
return resP;
}
List<List<Integer>> resP = new ArrayList<List<Integer>>();//最终结果存储
//row存储单行记录,visited进行访问记录,返回空
public void perBackTracking(int[] nums,boolean[] visited,List<Integer> row){
if(row.size() == nums.length){//找到一个结果,就插入到结果集
List<Integer> temp = new ArrayList<>(row);//查结果时创建一个新的arraylist,参数row那个要回溯,一直用来查找
resP.add(temp);
return;
}
//由于使用了回溯法,每次访问时实际上都只记录了单次排列的访问情况,所以可以使用一维数组进行访问情况记录和list记录单次结果,也就是说回溯法也是深度优先的
for(int i = 0;i < nums.length;i++){
if(!visited[i]){
visited[i] = true;
row.add(nums[i]);
perBackTracking(nums,visited,row);
row.remove(row.size() - 1);//回溯
visited[i] = false;//回溯
}
}
}
思考总结
由本题可以看出,回溯法的在思路上仍然是深度优先搜索,所以可以说是dfs的一种情况,因此在访问情况记录和当前结果记录上面仍然是针对单次的。但是在找到一条结果或者找不到的时候,就会采取回溯操作(如程序中注释的两条语句)。
我们也可以看出,回溯法的程序结构和dfs的程序结构时非常相似的。
模板
几点说明
1、最本质的法宝是“画图”,千万不能偷懒,拿纸和笔“画图”能帮助我们更好地分析递归结构,这个“递归结构”一般是“树形结构”,而符合题意的解正是在这个“树形结构”上进行一次“深度优先遍历”,这个过程有一个形象的名字,叫“搜索”;
2、然后使用一个状态变量,一般我习惯命名为 path、pre ,在这个“树形结构”上使用“深度优先遍历”,根据题目需要在适当的时候把符合条件的“状态”的值加入结果集;比如说用path来存储当前一条“可行解”,pre可能存储的是上一次遍历到的加入可行解的一个元素,设为函数的参数。
这个“状态”可能在叶子结点,也可能在中间的结点,也可能是到某一个结点所走过的路径,可能是记录访问情况的visited数组。
3、在某一个结点有多个路径可以走的时候,使用循环结构。当程序递归到底返回到原来执行的结点时,“状态”以及与“状态”相关的变量需要“重置”成第 1 次走到这个结点的状态,这个操作有个形象的名字,叫“回溯”,“回溯”有“恢复现场”的意思:意即“回到当时的场景,已经走过了一条路,尝试走下一条路”。
第 2 点中提到的状态通常是一个列表结构,因为一层一层递归下去,需要在列表的末尾追加,而返回到上一层递归结构,需要“状态重置”,因此要把列表的末尾的元素移除。
4、当我们明确知道一条路走不通的时候,例如通过一些逻辑计算可以推测某一个分支不能搜索到符合题意的结果,可以在循环中 continue 掉,这一步操作叫“剪枝”。
“剪枝”的意义在于让程序尽量不要执行到更深的递归结构中,而又不遗漏符合题意的解。因为搜索的时间复杂度很高,“剪枝”操作得好的话,能大大提高程序的执行效率。
“剪枝”通常需要对待搜索的对象做一些预处理,例如第 47 题、第 39 题、第 40 题、第 90 题需要对数组排序。“剪枝”操作也是这一类问题很难的地方,有一定技巧性。
总结一下:“回溯” = “深度优先遍历” + “状态重置” + “剪枝”,写好“回溯”的前提是“画图”。
模板
public void backtrack(待搜索的集合, 状态变量 1, 状态变量 2, 结果集){//结果集我经常设量
if (层数够深得到了一个可行解)
//打印或者把当前状态添加到结果集中
return;
for(可以执行的分支路径){
//剪枝
if (递归到第几层, 状态变量 1, 状态变量 2, 符合一定的剪枝条件:)
continue
对状态变量状态变量 1, 状态变量 2 的操作(#)
// 递归执行下一层的逻辑
backtrack(待搜索的集合, 递归到第几层, 状态变量 1, 状态变量 2, 结果集)
对状态变量状态变量 1, 状态变量 2 的操作(与标注了 # 的那一行对称,称为状态重置)
}
}