力扣解题思路:组合与全排列问题 纠错记录

排列组合题型中遇到的问题


思路:之前做过很多排列组合的题目比如力扣46题,求的是无重复数组的全排列,这个比较简单,直接DFS即可,DFS函数的出口就是结果子集中的元素个数刚好是数组总元素的个数。
而第47题就是46的升级版,就是求的是有重复元素数组的全排列,这里就需要考虑到组合会产生重复的子集,于是我用的hashSet来解决这个重复的问题,感觉笨笨的,递归出口是这样写的:

if(prefix.size()!=0 && candidate.size()==0){
    StringBuilder sb = new StringBuilder();
    for(int c:prefix){
        sb.append(c);
    }
 if(!set.contains(sb.toString())){
        res.add(prefix);
        set.add(sb.toString());
    }
}

有点傻,我将list转成了String来判断是否重复。。。后来我想了一下,既然要避免重复,那么在list添加元素的时候就应该及时的剪枝,于是我换了一下思路,首先我对数组排序并定义一个数组记录已访问的点,这个数组也作为DFS中的参数之一:

Arrays.sort(nums);
boolean[] used = new boolean[len];

接下来非常重要的一步剪枝:

if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue;
 }

其中,i > 0 是为了保证 nums[i - 1] 有意义,nums[i] == nums[i - 1]表示此次选择的节点和上次选择的一样,即可能出现重复,写 !used[i - 1] 是因为 nums[i - 1] 在深度优先遍历的过程中刚刚被撤销选择,如果没有 !used[i - 1]可能会错误的剪掉在正常回溯(无重复)过程中的有效的枝节。我个人可能更偏向于直接用hashMap去重,因为更简单,但实际上,剪枝来得更快且在实际中很高效。
接下来就是77题组合,唯一的区别是不是全排列,而是只选出部分排列,这个也很简单,和46类似,唯一的区别是DFS递归的出口并不是结果子集中的元素个数刚好是数组总元素的个数,而是等于题目给定的个数K即可。

总结一下,这类排列组合的题目有两个关键点,一个是递归的出口一定要根据题意来选择,一个就是在将满足条件的结果子集加入到结果集时一定要重新创建一个list(深拷贝)!!!虽然我知道但是经常犯错,写在这里警醒一下自己╮(╯▽╰)╭

接下来上例题~

39. 组合总和


思路:题目:

given candidate set [2, 3, 6, 7] and target 7,
A solution set is:
[[7],[2, 2, 3]]

题目挺简单的,也是基本的DFS方法,对于DFS函数,先找到其递归出口:

if(candidates.length == 0) return;
if(target<num) return;
if(target == num){
    res.add(new ArrayList<>(list));
    return;
}

写得不是很简洁,但是这样就能更好的看出什么情况退出递归函数。
接下来就可以遍历candidates啦:

List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
    Arrays.sort(candidates);
    dfs(candidates,0,target,new ArrayList<>());
    return res;
}
public void dfs(int[] candidates,int num,int target,List<Integer> list){
    if(candidates.length == 0) return;
    if(target<num) return;
    if(target == num){
        res.add(new ArrayList<>(list));
        return;
    } 
    for(int i=0;i<candidates.length&&target>=(num+candidates[i]);i++){
        list.add(candidates[i]);
        dfs(candidates,num+candidates[i],target,list);
        list.remove(list.size()-1);
    }
}

一顿操作猛如虎,一看运行结果就出现了问题。。。我把这个做成了全排列了,比如我把【2,2,3】和【2,3,2】当成两个答案了,所以出错了,因为在每次遍历candidates时我都把之前遍历过的和正在遍历的节点都重复遍历了一遍,而我们仅仅只需要从当前遍历的节点开始向后遍历即可(因为元素可以重复,所以此时当前遍历的节点在下一次遍历中可能还会使用到),我们只需要给DFS函数再加一个参数即可:

public void dfs(int[] candidates,int num,int target,List<Integer> list,int start){
    if(candidates.length == 0) return;
    if(target<num) return;
    if(target == num){
        res.add(new ArrayList<>(list));
        return;
    } 
    for(int i=start;i<candidates.length&&target>=(num+candidates[i]);i++){
        list.add(candidates[i]);
        dfs(candidates,num+candidates[i],target,list,i);
        list.remove(list.size()-1);
    }
}

这样就没问题啦~

但是其实不需要排序,因为没有任何在重复性上的要求!!只要注意start是从当前i开始,并且一定包含i (因为可重复,且题目所给数组是不重复的),所以可以这样:

    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        dfs(candidates,0,target,new ArrayList<>(),0);
        return res;
    }
    public void dfs(int[] candidates,int num,int target,List<Integer> list,int start){
        if(target<num) return;
        if(target == num){
            res.add(new ArrayList<>(list));
            return;
        } 
        //if(start == candidates.length) return;//可有可无
        for(int i=start;i<candidates.length;i++){//&&target>=(num+candidates[i])可有可无
            list.add(candidates[i]);
            dfs(candidates,num+candidates[i],target,list,i);
            list.remove(list.size()-1);
        }
    }

再来一个剪枝的练练手(●’◡’●)

40. 组合总和 II


思路:题目:

For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8,
A solution set is:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]

乍一看和上一题基本上一摸一样!只是每个数字只能用一次了,那这个好说,直接把上一个DFS函数中递归的起点从i改到i+1不就行了:

public void dfs(int[] candidates,int num,int target,List<Integer> list,int start){
    if(candidates.length == 0) return;
    if(target<num) return;
    if(target == num){
        res.add(new ArrayList<>(list));
        return;
    } 
    for(int i=start;i<candidates.length&&target>=(num+candidates[i]);i++){
        list.add(candidates[i]);
        dfs(candidates,num+candidates[i],target,list,i+1);
        list.remove(list.size()-1);
    }
}

看似也没有问题,一运行又出现了重复的子集,这次的重复就是出现了两个完全一样的子集了,很明显是因为数组元素重复导致的,自然可以想到之前讲的排列组合用的剪枝的方法,改法是一样的,在for循环中增加一个判断,DFS函数的参数中加一个visited数组即可:

List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    Arrays.sort(candidates);
    boolean[] visited = new boolean[candidates.length];
    dfs(candidates,0,target,new ArrayList<>(),0,visited);
    return res;
}
public void dfs(int[] candidates,int num,int target,List<Integer> list,int start,boolean[] visited){
    if(candidates.length == 0) return;
    if(target<num) return;
    if(target == num){
        res.add(new ArrayList<>(list));
        return;
    } 
    for(int i=start;i<candidates.length&&target>=(num+candidates[i]);i++){
        if (i > 0 && candidates[i] == candidates[i - 1] && !visited[i - 1]) {
            continue;
         }
         visited[i] = true;
        list.add(candidates[i]);
        dfs(candidates,num+candidates[i],target,list,i+1,visited);
        list.remove(list.size()-1);
        visited[i] = false;
    }
}

是不是很简单,都是固定的套路,理解就行啦o( ̄▽ ̄)ブ

377. 组合总和 Ⅳ

思路:在这里插入图片描述
题目比较简单,但是用普通的递归会超时:

public int combinationSum4(int[] nums, int target) {
    if (target == 0) {
        return 1;
    }
    int res = 0;
    for (int num : nums) {
        if (target >= num) {
            res += combinationSum4(nums, target - num);
        }
    }
    return res;
}

因为回溯过程中会出现很多已经回溯过的target,因此可以采用记忆化回溯:

private int[] memo;
public int combinationSum4(int[] nums, int target) {
    memo = new int[target + 1];
    Arrays.fill(memo, -1);
    memo[0] = 1;//终点
    return search(nums, target);
}
private int search(int[] nums, int target) {
    if (memo[target] != -1) {
        return memo[target];
    }
    int res = 0;
    for (int num : nums) {
        if (target >= num) {
            res += search(nums, target - num);
        }
    }
    memo[target] = res;
    return res;
}

这个memo实际相当于动态规划中的dp数组,因此我们也可以采用动态规划(背包问题),一定要注意初始化:

public int combinationSum4(int[] nums, int target) {
    int[] dp = new int[target+1];
    dp[0] = 1;
    for(int i=1;i<=target;i++){
        for(int j=0;j<nums.length;j++){
            if(i>=nums[j]){
                dp[i] += dp[i-nums[j]];
            }
        }
    }
    return dp[target];
}

这里有个问题,以前完全背包问题target不是放在循环里面嘛,为什么这里target只能放在外面呢?
在这里插入图片描述
有个说法是这样:

如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值