回溯加递归
:每次选择都是从之前选择好的数据中排除,再次进行选择,因此需要一个数组标记,已经选择了哪些数据,若之前选过,则剪枝跳过
;使用编程
的方法得到全排列,就是在这样的一个树形结构
中进行编程,具体来说,就是执行一次深度优先遍历
,从树的根结点到叶子结点形成的路径就是一个全排列。
package BDyNamicProgramming;
import java.util.ArrayList;
import java.util.List;
/**
* @Author Zhou jian
* @Date 2020 ${month} 2020/4/25 0025 15:08
*/
public class Problem46 {
/**
* https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
* 使用编程的方法得到全排列,就是在这样的一个树形结构中进行编程
* 具体来说:就是执行一次深度优先遍历,从树的根节点到叶子节点形成的路径是一个全排列
*
* 每一个节点都表示了全排列问题求解的不同阶段,这些阶段通过变量的不同的值体现
* 这些变量的不同的值,也称为状态
* 使用深度优先遍历有回头的过程,在回头以后,状态变量需要设置成和先前一样
* 因此在回到上一层节点的过程中,需要撤销上一次的选择,这个操作也称为状态重置哦
* 深度优先遍历,可以直接借助系统栈空间,为我怕你们保存所需要的状态变量,在编码中只需要注意遍历到相应的节点是‘
* 状态变量的值是正确的,具体的做法是:往下走一次的时候,path变量在尾部追加,而往回走的时候,需要撤销上一次左的选择,也就是在尾部操作,因此
*
回溯算法会大量应用“剪值”技巧以达到加快速度,有些时候,需要做一些预处理工作(例如排序)才能达到剪值的目的
预处理工作虽然也耗时,但一般而且能够剪值节约时间更多
我做题的时候,第 1 步都是先画图,画图是非常重要的,只有画图才能帮助我们想清楚递归结构,想清楚如何剪枝。就拿题目中的示例,想一想人手动操作是怎么做的,一般这样下来,这棵递归树都不难画出。
即在画图的过程中思考清楚:
1、分支如何产生;
2、题目需要的解在哪里?是在叶子结点、还是在非叶子结点、还是在从跟结点到叶子结点的路径?
3、哪些搜索是会产生不需要的解的?例如:产生重复是什么原因,如果在浅层就知道这个分支不能产生需要的结果,应该提前剪枝,剪枝的条件是什么,代码怎么写?
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
*/
/**
*
* 1、首先这颗树除了根节点和叶子节点以外,每个节点左的事情其实一样,即在已经选择了一些数据的前提,
* 我们需要在剩下还没有选择的数中按照顺序依次选择一个数
*
* 2、递归的终止条件,数已经选购了,因此我们需要一个变量来表示当前递归到第几层,我们把这个变量叫做depth
*
* 3、这些节点实际上表示了搜索(查找)全排列问题的不同阶段,为了区分这些不同阶段,我们就需要一些变量来记录为了
* 得到一个圈排列,程序进行到哪一步了,在这里我们需要设置两个变量:
*
* ··(1)已经选好了哪些树,到叶子节点时候,这些已经选择数就构成了一个圈排列
* (2) 一个布尔数组used,初始化的时候都为fasle,表示这些树都还没有被选择,当我们选定一个数的时候,就将这个
* 数组的相应位置设置为true,这样在考虑下一个位置的时候,就能够以O(1)的时间复杂度判断这个数是否已经被选择过
* 这是一种以“空间换时间的”思想
*
* 我们把这两个变量称为“状态变量”,他们表示在我们求解一个问题的时候所处的阶段
*
* 4、在非叶子结点处,产生不同的分支,这一操作的语义是:在还未选择的数中一次选择一个元素作为下一个位置的元素,这显然通过一个循环实现
*
* 5、另外,因为是执行深度优先遍历,从较深层次的节点返回到较浅层次的接地那的时候,需要左状态充值,即回到过去
*
* @param nums
* @return
*/
public List<List<Integer>> permute(int[] nums){
List<List<Integer>> rs = new ArrayList<>();
List<Integer> path = new ArrayList<>();
//标记之前递归的过程中 数组的哪些位置已经使用过
boolean[] used = new boolean[nums.length];
fun(nums,path,rs,used,0);
return rs;
}
public void fun(int[] nums,List<Integer> path,List<List<Integer>> rs,boolean[] used,int depth){
if(depth==nums.length){
rs.add(new ArrayList<>(path));
}
for(int i=0;i<nums.length;i++){
//当前nums[i]在前面被使用过则进行剪枝
if(used[i]){
continue;
}
path.add(nums[i]);
used[i]=true;
fun(nums,path,rs,used,depth+1);
//注意这里是状态重置,是从深层次节点回到浅层次节点的过程,代码在形式上和之前是对成发的
used[i]=false;
path.remove((Object)nums[i]);
}
}
}