刷题笔记9——回溯解决排列/组合/集合

回溯小结

对于排列/组合/集合,无论形式如何变,无非就是穷举所有解,合理的剪枝以降低算法的复杂度,在这类问题中,我们关注的都是树枝上的值!

  • 顺序有关排列数,顺序无关组合数,一共两种类型
  • 排除重复的结果t,就要避免相同的数在同一层出现,可以通过排序且当前元素不等于上一个元素来判断
	if(i!=k && nums[i]==nums[i-1]){
         continue;
     }
  • 元素可以重复的话用swap函数和used数组来控制
  • 最重要的就是backtrack函数中的k,以及每次循环的i是从哪里开始的,决定是否可以重复(i从当前层k开始,说明前边的元素就不能再使用了,是组合数形式,i从0开始,说明元素交换顺序是两种情况,是排列数形式)
  • 适当的使用剪枝函数,可以降低时间复杂度

  • length、length()、size()区别
    • length是数组的属性
    • length() 是字符串的方法
    • size() 是集合Lits的方法
  • List<Integer> t = new ArrayList<Integer>();
    • 左边是一个声明,是一个泛型,List是一个接口定义了列表基本行为
    • ArrayList 是Java集合框架中的一个类
    • 通过 new 关键字创建了一个 ArrayList 对象,可以存储整数类型的元素。
    • 类可以实现(implement)一个或多个接口,从而保证类将实现接口中定义的方法。
    • 接口可以实现多继承
  • List<List<Integer>> res = new ArrayList<List<Integer>>();在嵌套情况下,后边new不需要两个ArrayList
  • list的一些基础操作,add,remove, 固定位置的add
    • list.clear() 清空list中的所有元素
    • list.remove(list.size() - 1) 移除最后一个元素
  • 新学到一种数据结构方式List,track.addLast(i); track.removeLast();
// 右边这部分其实也要写成new LinkedList<List<Integer>>();但是这里用了[钻石运算符](https://www.zhihu.com/question/594493047)
 List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
  • List不能被实例化

51. N 皇后/ 52. N 皇后 II

  • 沉迷于交换法,写出来的代码太长了
class Solution {
    List<List<String>> res = new ArrayList<>();
    public List<List<String>> solveNQueens(int n) {
        int[] a = new int[n];
        for (int j = 0; j < n;a[j] = j,j++);

        List<String> board = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            StringBuilder sb = new StringBuilder();
            for (int j = 0; j < n; j++) {
                sb.append('.');
            }
            board.add(sb.toString());
        }
        permutation(a,0,board);
        return res;
    }

    
    void permutation(int[] a, int k,List<String> board){
        if(k==a.length){
            res.add(new ArrayList<>(board));
            return;
        }
        int n = a.length;
        for(int i= k;i<n;i++){
            if(!isValid(board,k,a[i])){
                continue;
            }
            swap(a,k,i);
            StringBuilder sb = new StringBuilder(board.get(k));
            sb.setCharAt(a[k], 'Q');
            board.set(k,sb.toString());
            permutation(a,k+1,board);
            StringBuilder sc = new StringBuilder(board.get(k));
            sc.setCharAt(a[k], '.');
            board.set(k,sc.toString());
            swap(a,i,k);
        }
    }

    void swap(int a[], int k, int i){
        int temp = a[k];
        a[k] = a[i];
        a[i] = temp;
    }

    boolean isValid(List<String> board,int row,int col){
        int n = board.size();

        // 右上方
        for (int i=row-1,j=col+1;i>=0&&j<n;i--,j++){
            if (board.get(i).charAt(j) == 'Q'){
                return false;
            }
        }

        for (int i=row-1,j=col-1;i>=0&&j>=0;i--,j--){
            if (board.get(i).charAt(j) == 'Q'){
                return false;
            }
        }

        return true;
    }

}

78. 子集

  • 我的答案,是通过一个二叉树,其中左枝是选择,右枝是不选择,遍历这棵树
class Solution {
    List<Integer> t = new ArrayList<Integer>();
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    
    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums,0);
        return res;
    }

    void backtrack(int[] nums,int k){
        if (k==nums.length){
            res.add(new ArrayList<Integer>(t));
            return;
        }

        // 当不选择这个数的时候
        backtrack(nums,k+1);
        // 当选择这个数的时候
        t.add(nums[k]);
        backtrack(nums,k+1);
        t.remove(t.size()-1);
    }
}
  • 这是答案给出的另外一种方法,很巧妙,和全排列同样的代码,唯一的区别是backtrack(nums, i + 1); 的时候不再是k了而是i+1,通过这个方式就可以控制,1在第一轮的时候被选择,在第二轮的时候就直接认为不选择1。
class Solution {

    List<List<Integer>> res = new LinkedList<>();
    // 记录回溯算法的递归路径
    LinkedList<Integer> track = new LinkedList<>();

    // 主函数
    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums, 0);
        return res;
    }

    // 回溯算法核心函数,遍历子集问题的回溯树
    void backtrack(int[] nums, int start) {

        // 前序位置,每个节点的值都是一个子集
        res.add(new LinkedList<>(track));
        
        // 回溯算法标准框架
        for (int i = start; i < nums.length; i++) {
            // 做选择
            track.addLast(nums[i]);
            // 通过 start 参数控制树枝的遍历,避免产生重复的子集
            backtrack(nums, i + 1);
            // 撤销选择
            track.removeLast();
        }
    }
}

做完这俩题好像懂了为什么要分组合/子集树 排列数两种类型了

以拍照为例,子集树是确定谁来合影,排列数是确定合影时的站位顺序

  • 子集:元素的位置不重要,所以只需要对当前位置元素的选或者不选遍历即可,顺序无关
    请添加图片描述
  • 排列数:不重复情况下,在每个位置k上全部元素都要遍历一遍,顺序有关
    请添加图片描述

77. 组合

class Solution {
    List<Integer> t = new ArrayList<Integer>();
    List<List<Integer>> res = new ArrayList<List<Integer>>();

    public List<List<Integer>> combine(int n, int k) {
        backtrack(n,1,k);
        return res;
    }

    void backtrack(int n, int m, int k){
        if(t.size()==k){
            res.add(new ArrayList<Integer>(t));
            return;
        }

        for (int i = m; i<=n; i++){
            t.add(i);
            backtrack(n, i+1, k);
            t.remove(t.size()-1);
        }
    }
}

90. 子集 II

  • 带重复元素,要不就是先排序然后判断相邻的元素是否相同
  • 或者每次循环的时候判断这个值是否出现过
class Solution {
    List<Integer> t = new ArrayList<Integer>();
    List<List<Integer>> res = new ArrayList<List<Integer>>();

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        backtrack(nums,0);
        return res;
    }
    void backtrack(int[] nums,int k){

        res.add(new ArrayList<Integer>(t));
        for(int i=k; i<nums.length;i++){
            if(i!= k && nums[i] == nums[i-1]){
                continue;
            }
            t.add(nums[i]);
            backtrack(nums,i+1);
            t.remove(t.size()-1);
        }
    }
}

40. 组合总和 II

  • 第一次运行的时候超出了时间限制,我以为是Arrays.sort函数复杂度高,没想到原来是需要剪枝!!!回溯结束会自己停止,但是如果超时那么就需要考虑某一枝是否值得遍历
class Solution {
    List<Integer> t = new ArrayList<Integer>();
    List<List<Integer>> res = new ArrayList<List<Integer>>();

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        backtrack(candidates,0,target);
        return res;

    }
    void backtrack(int[] nums,int k,int target){
        int sum = t.stream().mapToInt(Integer::intValue).sum();
        if(sum == target){
            res.add(new ArrayList<Integer>(t));
            return;
        }

        if(sum>target){
            return;
        }

        for(int i=k; i<nums.length;i++){
            if(i!=k && nums[i]==nums[i-1]){
                continue;
            }
            t.add(nums[i]);
            backtrack(nums,i+1,target);
            t.remove(t.size()-1);
        }
    }
}

47. 全排列 II

  • 最开始用swap方法做了半天,但是发现swap和sort方法相冲,所以换成了used来记录放置元素重复使用
  • 还有一个点是调试才懂的,就是要求 nums[i]==nums[i-1] 的时候直接continue,这里其实需要规定此时它们不在同一评估层次,如果在同一层就需要continue,如果在不同层,类似于112的情况,此时尽管第二个1与前边重复,但是还需要遍历到
class Solution {
    List<Integer> t = new ArrayList<Integer>();
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    boolean[] used;

    // swap会把之前的排序打乱,所以不能用,只能用used
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        used = new boolean[nums.length];
        backtrack(nums,0);
        return res;
    }

    void backtrack(int[] nums,int k){
        if(k==nums.length){
            res.add(new ArrayList<Integer>(t));
            return;
        }

        for(int i= 0;i<nums.length;i++){
            if(used[i]) continue;
            if(i!=0 && nums[i]==nums[i-1] && used[i-1]!=true){
                continue;
            }

            used[i] = true;
            t.add(nums[i]);
            backtrack(nums,k+1);
            t.remove(t.size()-1);
            used[i] = false;
        }
    }
}

39. 组合总和

  • 思路很简单
class Solution {
    List<Integer> t = new ArrayList<Integer>();
    List<List<Integer>> res = new ArrayList<List<Integer>>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backtrack(candidates,0,target);
        return res;
    }

    void backtrack(int[] candidates,int k,int ret){
        if(k==candidates.length){
            if(ret == 0){
                res.add(new ArrayList<Integer>(t));
                return;
            }
            return;
        }

        int num = ret/candidates[k];
        for (int i=0;i<=num;i++){
            for( int j=0;j<i;j++){
                t.add(candidates[k]);
            }
            backtrack(candidates,k+1,ret-i*candidates[k]);
            for( int j=0;j<i;j++){
                t.remove(t.size()-1);
            }
        }
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值