DFS-Backtracking回溯-经典排序组合

Backtracking有很多很多不同场景的问题。Backtracking其实就是DFS + 剪枝。就几乎是枚举,然后either发现满足条件的branch,or当前branch已经不可能符合条件了剪枝放弃

写一下经典排序组合问题吧。

题目简介candidates
是否有重复元素
元素是否允许
重复使用
有start
39. Combination Sum求和为k的组合元素distinct允许多次使用
40. Combination Sum II求和为k的组合元素可以有重复有几个限几个
78. Subsets求所有子集元素distinct有啥用啥
90. Subsets II求所有子集元素可以有重复有几个限几个
46. Permutations求所有排列元素distinct- - (全用)
47. Permutations II求所有排列元素可以有重复- - (全用)
77. Combinations(类78)所有sizeK子集元素distinct用k个
  • 组合:需要start,来限制当前层只能从一个范围内挑选(而不是全集),因为有些已经使用过了。组合问题是从“当前resource是否使用”的角度考虑的,resource1,可以用也可以不用,决定之后,resource2,可以用也可以不用,……等到当前层的时候,比如resourceM,那M之前的都已经考虑过了(并且已经做出决定了),于是当前层就只考虑从M到最后这部分resource即可了(即从start开始)。
  • 排序:不需要start。因为排序问题是从“当前position有哪些合法candidate”的角度考虑的。而candidate(即resource)是按照resource排序的,组合问题从“当前resource是否使用”角度考虑,肯定是按顺序的,于是可以用start来标记,而这里只能用一整个数组used[] 来记录哪些resource已经使用过了。

39. Combination Sum

Input: candidates = [2,3,6,7], target = 7,
A solution set is:
[
[7],
[2,2,3]
] candidates中元素可以用无数次

这个题candidates = [2,3,6,7]里的元素允许无限次使用,若求所有组合,则是无限多个的。好在还有个和target=7的限制。

因为candidates元素允许无限次使用,所以往下一级传start的时候,不是传i+1,而是传i,下一级仍然处理当前位。那岂不是在这一位一直不往下一位跳吗?对就是这样,本来2,2,2,……就是其中一个branch,直到总和超过target,不符合要求了,被剪枝。

关于最开头candidates是不是需要先排序这个问题,如果candidates元素distinct,那就不需要排序。排序都存在于“candidates可能有重复元素”的情况,要保证值相同的两个元素挨着。元素本身的值大小关系,我们不关心。

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> lst = new ArrayList<>();
        genCombination(lst, new ArrayList<Integer>(), candidates, target, 0);
        return lst;
    }
    
    private void genCombination(List<List<Integer>> lst, List<Integer> path, int[] candidates, int remain, int start) {
        if (remain == 0) {//good terminal
            lst.add(new ArrayList<>(path));//注意:new一个新的ArrayList
            return;
        } else if (remain < 0) {//bad terminal
            return;
        }
        for (int i = start; i < candidates.length; i++) {
            path.add(candidates[i]);
            genCombination(lst, path, candidates, remain-candidates[i], i);
            path.remove(path.size()-1);
        }
    }
}

40. Combination Sum II

Input: candidates = [10,1,2,7,6,1,5], target = 8,
A solution set is:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
] 区别:candidates中每个元素只能用一次

  • 39: candidates元素允许无限次使用,往下一级传start的时候传i。
  • 40: candidates元素只允许使用一次,往下一级传start的时候传i+1。

这里candidates可能有重复元素,于是需要在backtracking之前先排序[1,1,2,5,6,7,10],保证值相同的元素挨着。挨着之后,我们就要想“重复元素是否需要跳过”。注意不是所有值相同的元素都跳过,[1,1,2,…]这里在处理第二个1的时候,不能跳过,否则 [1, 1, 6]就漏掉了。

  • 这里说的“跳过重复”是指除了当前start所在位以外,后面的数里如果存在重复,比如[1,1,2,5,6,6,7,10],第二个6就跳过不处理了(同层横向–跳过)。
  • 而“上一层的1和本层的1”这种不跳过(上下层纵向–不跳过)。

i > start && candidates[i] == candidates[i-1] 来定义跳过条件,把i = = start,即本层第一个排除出去(不跳过)。

class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        List<List<Integer>> lst = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        getCombination(lst, path, candidates, target, 0);
        return lst;
    }
    
    private void getCombination(List<List<Integer>> lst, List<Integer> path, int[] candidates, int remain, int start) {
        if (remain == 0) {//good terminal
            lst.add(new ArrayList<>(path));
            return;
        } else if (remain < 0) {//bad terminal
            return;
        }
        for (int i = start; i < candidates.length; i++) {
            if (i > start && candidates[i] == candidates[i-1]) {continue;}
            path.add(candidates[i]);
            getCombination(lst, path, candidates, remain-candidates[i], i+1);
            path.remove(path.size()-1);
        }
    }
}

78. Subsets

Input: nums = [1,2,3], Input元素distinct
Output:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]

39,40 combination sum是以“和达到target”作为一个branch的成功条件。78,90 subset是没有鉴别标准,要求所有的branch。这种情况就必须要求“不能允许无限次使用元素”,否则就没完了。

78元素distinct,90允许有重复元素,但都是某个值有几个就最多使用几个。所以从“当前resouce是否使用”的角度考虑,这个用过了就不能再用了,往下一层传传i+1。

78元素distinct,就不存在排序的需要,也没有跳不跳过的问题(只有有重复元素且排序过的才需要考虑跳过)。

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> list = new ArrayList<>();
        getSubset(list, new ArrayList<Integer>(), nums, 0);
        return list;
    }
    
    private void getSubset(List<List<Integer>> list, List<Integer> path, int[] nums, int start) {
        list.add(new ArrayList<>(path));//无论如何都把path添加进list
        for (int i = start; i < nums.length; i++) {
            path.add(nums[i]);
            getSubset(list, path, nums, i+1);
            path.remove(path.size()-1);
        }
    }
}

90. Subsets II

Input: [1,2,2] 不同:Input可能有重复元素
Output:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]

几乎和78一样,除了78元素distinct,90允许有重复元素。需要排序,且需要考虑跳过的问题。跳过的方法和40. Combination Sum II一样。

class Solution {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> lst = new ArrayList<>();
        Arrays.sort(nums);
        getSubset(lst, new ArrayList<Integer>(), nums, 0);
        return lst;
    }
    
    private void getSubset(List<List<Integer>> lst, List<Integer> path, int[] nums, int start) {
        lst.add(new ArrayList<>(path));
        for (int i = start; i < nums.length; i++) {
            if (i > start && nums[i] == nums[i-1]) {continue;}
            path.add(nums[i]);
            getSubset(lst, path, nums, i+1);
            path.remove(path.size()-1);
        }
    }
}

46. Permutations

Input: [1,2,3],数组元素distinct
Output:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

上面四个都是组合,这个是排列,因为所有元素都要用,取用元素和input的顺序不一定相同,所以无法用start来限制可选范围。

那怎么知道哪些元素已经用过了呢?path.contains(nums[i]) 作为“检查是否已使用过”条件。因为各个元素值不同,所以直接在ArrayList里检查是否已经出现过就可以了。

此处注意:“检查是否已使用过”条件和“跳过”条件不同。参见下面47的解释。

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> ret = new ArrayList<>();
        backtracking(ret, new ArrayList<>(), nums);
        return ret;
    }
    
    private void backtracking(List<List<Integer>> list, List<Integer> path, int[] nums) {
        if (path.size() == nums.length) {
            list.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (path.contains(nums[i])) {continue;}
            path.add(nums[i]);
            backtracking(list, path, nums);
            path.remove(path.size()-1);
        }
    }
}

47. Permutations II

Input: [1,1,2] 区别:input数组可能有重复的
Output:
[
[1,1,2],
[1,2,1],
[2,1,1]
]

  • 46: “检查是否已使用过”条件:path.contains(nums[i])。因为各个元素值不同,所以直接在ArrayList里检查是否已经出现过就可以了。
  • 47: “检查是否已使用过”条件:used[i] == true。因为有可能有值相同的元素,所以需要单独一个数组used[]来记录每个元素是否已经使用过。

检查是否已使用过”条件和“跳过”条件不同。
47还有一个“跳过”条件:和40. Combination Sum II,90. Subsets II类似。
i > 0 && nums[i] == nums[i-1] && !used[i-1]
和40,90的区别在于:

  • 40,90: i = = start 不跳过,即每层的第一个元素如果和上一层元素相同,则不跳过。[1,1,2,5,6,7,10],即处理[1,1]这里上下级调用的时候,第二个1不跳过。
  • 47: used[i-1]= = true 不跳过,即相同的那两个元素中的former那个元素已经用过,则latter那个不跳过

其实这两种情况本质上是一样的。都是:former若还没用,则latter跳过(因为everything都在former处处理),former已经用了,那latter不能跳过,[1]和[1,1]都需要考虑。(40,90因为顺序处理,所以i= =start就是“former已经用了,那latter不能跳过”的情况)。

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> lst = new ArrayList<>();
        Arrays.sort(nums);
        boolean[] used = new boolean[nums.length];
        getPermutation(lst, new ArrayList<Integer>(), nums, used);
        return lst;
    }
    
    private void getPermutation(List<List<Integer>> lst, List<Integer> path, int[] nums, boolean[] used) {
        if (path.size() == nums.length) {
            lst.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) {continue;}//跳过条件
            if (used[i] == true) {continue;}//检查是否已使用过
            path.add(nums[i]);
            used[i] = true;
            getPermutation(lst, path, nums, used);
            used[i] = false;
            path.remove(path.size()-1);
        }
    }
}

77. Combinations

Input: n = 4, k = 2
Output:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

和78. Subsets几乎一样,除了这里只有长度为2的path才认为是成功的(才添加进result list里)。这里不贴代码啦。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值