【回溯第四篇】用回溯代替暴力破解解决排序问题:用n次递归代替n次for循环(本质)

208 阅读4分钟

文章目录

46.全排列(入门,大排列中不包含重复数字)

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

为什么 树 这么重要,因为暴力破解的多层for循环都可以用递归树来解决。

数学特性(题目隐患条件):全排列就是每个元素都要用到,所有只要取 叶子节点 ,不需要分支节点。

这里和组合问题、切割问题和子集问题最大l两个不同:

  1. for循环不是从startIndex开始,而是从0开始(输入给定一个数组就从0开始,输入给定一个n就从1开始)
  2. 递归函数的参数里面也不需要startIndex
  3. 需要used数组记录path里都放了哪些元素了

因为组合问题、切割问题、子集问题就用这两个来实现输出结果不同的,现在不需要了,就两个都不需要了
组合问题:题目给定明确条件,输出的子组合不能重复;
切割问题:
子集问题:题目给定明确条件,输出的子集合不能重复;集合的数学特性,一个大集合选子集合的时候,元素不能重复。

class Solution {
    List<List<Integer>> result=new ArrayList<>();
    List<Integer> path=new ArrayList<>();
    public void backtracking(int[] nums,boolean[] used){
        if (path.size() == nums.length){
            result.add(new ArrayList<>(path));
            return;  // 得到一个全排列就好了,不用再深入了,就可以
        }
        for (int i=0;i<nums.length;i++){
            if (true == used[i])  continue;   // 如果用过了
            path.add(nums[i]);
            used[i]=true;
            backtracking(nums,used);
            path.remove(path.size()-1);   // 算法题不用考虑多线程什么的,这种两句一段的代码顺序是可以随便写的
            used[i]=false;
        }
    }
    public List<List<Integer>> permute(int[] nums) {
        boolean[] used=new boolean[nums.length];
        for (int i=0;i<used.length;i++)   // used中每一个元素就是代表nums中的元素是否使用过
            used[i]=false;
        backtracking(nums,used);
        return result;
    }
}

47.全排列 II(升级,大排列中包含重复数字)

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

这道题目和回溯算法:排列问题!的区别在于「给定一个可包含重复数字的序列」,要返回「所有不重复的全排列」。

这里又涉及到去重了。

在回溯算法:求组合总和(三) 、回溯算法:求子集问题(二)我们分别详细讲解了组合问题和子集问题如何去重。

那么排列问题其实也是一样的套路。

核心:对于结果,元素在同一个子组合内是可以重复的,怎么重复都没事,但两子个组合不能相同
大组合中每个元素只能使用一次(非组合的数学特性,题目明确给定条件),且结果中不能包含相同子组合(题目明确给定输出结果要求,子组合中元素个数是无序的):大组合排好顺序之后,相同元素同一个树层中只能使用一次,出现两次,得到的子组合就有重复的了;
大集合中每一个元素只能使用一次(集合的数学特性,因为是集合,所以这样,题目隐含条件),结果中不能包含相同子集合(题目明确给定输出结果要求,子集合中元素个数是无序的):大集合排好顺序之后,相同元素只能使用一次,出现两次,得到的子集合就有重复的了。

递增子序列
大序列中每个元素只能使用一次(序列的数学特性,因为是序列,所以这样,题目隐含条件),结果中不能包含相同子序列(题目明确给定输出结果要求,子序列中元素个数要求是递增的,所以要剪枝if):大序列给定后(不能排序),同一树层相同元素只能使用一次,出现两次,得到的子序列就有重复的了。还有,子序列子序列中元素个数要求是递增的,所以要剪枝if。所以两个剪枝

「一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果」。

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();

    public void backtracking(int[] nums, boolean[] used) {
        if (path.size() == nums.length) {
            result.add(new ArrayList<>(path));
            return;  // 得到一个全排列就好了,不用再深入了,就可以
        }
        for (int i = 0; i < nums.length; i++) {
            if (i > 0 && nums[i - 1] == nums[i] && false == used[i - 1]) continue;   // 如果用过了
            if (used[i] == false) {
                path.add(nums[i]);
                used[i] = true;
                backtracking(nums, used);
                path.remove(path.size() - 1);   // 算法题不用考虑多线程什么的,这种两句一段的代码顺序是可以随便写的
                used[i] = false;
            }
        }
    }

    public List<List<Integer>> permuteUnique(int[] nums) {
        boolean[] used = new boolean[nums.length];
        for (int i = 0; i < used.length; i++)   // used中每一个元素就是代表nums中的元素是否使用过
            used[i] = false;
        Arrays.sort(nums);  // 包含重复元素,要求全排列,这里就必须排序 
        backtracking(nums, used);
        return result;
    }
}

相对于上面的,就是加上排序 和 if (used[i] == false) 判断

为什么需要 if (used[i] == false) 判断,如果没有的话,