回溯之全排列
https://leetcode.cn/problems/permutations/
比如数字列表1,2,3的全排列可以画出上面的"决策树"的样式来穷举所有的结果.
可以发现到达最底层的时候每一条下来的路径都是可以作为众多结果中的一个,此处就要利用回溯算法
回溯算法是站在每个节点上想三件事:
- 我还有哪些路可以选
- 我已经选择了哪些路段
- 什么情况我就不用再走了,也就是结束条件
伪代码:
void helper(路径,选择列表){
//1.结束条件
if(结束条件成立){
res.add(当前的路径);
return;
}
//2.穷举
for(选择:选择列表){
//2.1做选择
helper(路径,选择列表);
//2.2撤销选择
}
}
其实这已经是回溯算法的框架了,我的理解:
dfs的这个伪代码最重要的还是前面提到的三件事,而且dfs本质就是一个穷举的过程,结束条件视情况而定,较难理解的就是做选择和撤销选择,关键我们要理解helper这个递归函数在干什么,或者说你写一个helper的定义是什么,比如针对全排列问题,我们定义的helper函数,可以给它这么个定义:给出已经选择过的路径,已经选择列表,它能帮我递归完成在已选择路径的基础之上的后续全排列,这么讲应该已经非常清楚了,比如我们的第一层for循环,路径为空的时候,for应当可以遍历到1,2,3才可以,因为我们要让1,2,3都可以做开头,Ok,也就是说我们此时作了第一层的选择了,开始进入递归了,因为我们做完选择,必须必须维护好已走路径和选择列表,所以进入下一层递归函数的时候,可以保证此处的节点不重复;我们不用管第二层helper具体干了啥,但是它递归结束之后,我们必须来一个撤销动作,为什么呢?你不撤销,回头2开始做第一层节点的时候,你1始终还是在已选择过的路径里头,这显然不合理.至此解释清楚了上述伪代码的思路.
下面给出全排列一题的完整代码:
class Solution {
private List<List<Integer>> res = new LinkedList<>();//结果集
public List<List<Integer>> permute(int[] nums) {
//递归时需维护一个选择列表和已走过的路径,当然列表不一定非要一个对象维护
LinkedList<Integer> track = new LinkedList<>();//路径
boolean[] used = new boolean[nums.length];//和nums一起用以维护选择列表
helper(nums,used,track);
return res;
}
private void helper(int[] nums, boolean[] used, LinkedList<Integer> track) {
//到达底层才会终止
if(track.size()==nums.length){
res.add(new LinkedList(track));
return;
}
//回溯
for(int i=0;i<nums.length;i++){
if(used[i]==true) continue;
//做选择:
track.add(nums[i]);
used[i]=true;
helper(nums,used,track);
//撤销选择:
track.removeLast();
used[i]=false;
}
}
}
Note:
- 选择列表的维护不一定就非要明确给出一个列表的形式,比如上述代码就利用原数组和一个布尔数组达到了同样的效果
- 向结果集中添加一个排列结果的时候,千万不要直接写一个res.add(track),因为回溯的过程会慢慢的把这个track修改成为空,不信你试试
希望对你有所帮助