【回溯Ⅱ】组合问题

第一类组合问题

77.组合

77.组合

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数组合
你可以按 任何顺序 返回答案。
在这里插入图片描述

这个题目之前的博客已经解答过了,里面还有字典序/二进制序列法求解代码。这里再说一下回溯法。遍历[1~n]里面的每个数,每次都有两种两种情况:选or不选;递归结束的条件是已经有k个数字了or数组遍历完了,其实还有一个结束条件:当前遍历的数字中选择的有m个,还剩下p个,然而m+p < k了,继续遍历下去就算全部选择也不可能满足“k个数”这个条件。Java代码如下:

class Solution {
    
    
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    List<Integer> temp = new ArrayList<Integer>();

    public List<List<Integer>> combine(int n, int k) {
    
    
        dfs(1, n, k);
        return ans;
    }
    public void dfs(int cur, int n , int k){
    
    
        if(temp.size() + (n - cur + 1) < k)
            return ;
        if(temp.size() == k){
    
    
            ans.add(new ArrayList<Integer>(temp));
            return ;
        }

        temp.add(cur);
        dfs(cur + 1, n, k);
        temp.remove(temp.size() - 1);
        dfs(cur + 1, n, k);
    }
}

216.组合问题Ⅲ

216. 组合问题Ⅲ

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
在这里插入图片描述

这和77的区别是,这个题目不仅要找k个数,还要求和为n。因此在上一个题的基础上,加上一个判断过程【和是否为n】即可:

class Solution {
    
    
    List<Integer> temp = new ArrayList<Integer>();
    List<List<Integer>> ans = new ArrayList<List<Integer>>();

    public List<List<Integer>> combinationSum3(int k, int n) {
    
    
        dfs(1, k, n);
        return ans;
    }

    public void dfs(int cur, int k, int sum) {
    
    
        if (temp.size() + (9 - cur + 1) < k) {
    
    
            return;
        }
        if (temp.size() == k) {
    
    
            int tempSum = 0;
            for (int num : temp) {
    
    
                tempSum += num;
            }
            if (tempSum == sum) {
    
    
                ans.add(new ArrayList<Integer>(temp));   
            }
            return;
        }
        temp.add(cur);
        dfs(cur + 1, k, sum);
        temp.remove(temp.size() - 1);
        dfs(cur + 1, k, sum);
    }
}

第二类组合问题

下面的几个题,可以认为是一个系列的~

39. 组合总和

39. 组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个数字可以无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
在这里插入图片描述

递归法一:组合位置填空

这个递归思路,是每次递归都要给这个位置选择一个数,这个数是candidates数组中的——即每次递归,都遍历取依次candidates中的数。不过取的数字需要满足一些条件。

  • 我们肯定想先从小的数字开始选,毕竟题目给的答案也是从小的数字开始的。所以先对candidates排序;
  • 每次递归都找一个数,但是这个数num需要小于等于target;【num都比target大了还怎么满足加上去和为target是吧】
  • 根据这个的思路,第一个例子candidates =[2,3,6,7],第一次递归选2,第二次递归如果选3,第三次递归可以选2;那么和第一次递归选2,第二次递归选2,第三次递归选3就重复了;这里需要添加一个条件以去除重复:⭐我们的答案组合中的数字是从小到大的,因此我们每次递归选的数组 num 必须大于等于前一轮选的数字 ,假设为min,即num≥min;因为candidate是已经排序的,那么每一轮开始寻找的初始索引,应该是从上一轮选择的数的索引开始的;
  • 递归结束条件就是传入的target为0;以及遍历完数组,待选择的数为空;
    完整Java代码如下:【两种都可以,一个是直接传min;一个是传idx】
class Solution {
    
    
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    List<Integer> temp = new ArrayList<Integer>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
    
    
        Arrays.sort(candidates);
        dfs(candidates, target,0);
        return ans;
    }

    public void dfs(int[] candidates, int target,int idx){
    
    
        if(target == 0 ){
    
    
            ans.add(new ArrayList<Integer>(temp));
            return;
        }
        //candidates数组中已经没有数字可选以满足条件
        if(idx == candidates.length)
            return;

        for(int i = idx ; i < candidates.length && candidates[i] <= target; i++){
    
    
            temp.add(candidates[i]);
            dfs(candidates, target - candidates[i], i);
            temp.remove(temp.size() - 1);
        }         
    }
}
class Solution {
    
    
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    List<Integer> temp = new ArrayList<Integer>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
    
    
        Arrays.sort(candidates); //先排序,升序
        dfs(candidates, target,candidates[0]); //初始传入的min即为排序后candidates中的第一个元素
        return ans;
    }

    public void dfs(int[] candidates, int target,int min){
    
    
        if(target == 0 ){
    
    
            ans.add(new ArrayList<Integer>(temp));
            return;
        }
        //candidates数组中已经没有数字可选以满足条件
        if(target < min)
            return;

        for(int num : candidates){
    
     //遍历candidate
            if(num >= min && num <= target){
    
     //选择满足条件的数字
                temp.add(num);
                dfs(candidates,target - num, num); // 更新target为target-num
                temp.remove(temp.size()-1);
            }
        }             
    }
}

递归法二:遍历数组

第二个思路是去遍历数组,那么每次针对数组中的数字有两种情况:选or不选。
每一轮递归的目标是选择一个数num,使得这个数num与当前还差的target相等。递归结束条件①target=0,即选了这些数以后,与目标值还差0;即选的这些数和已经为target了!②遍历完了这个数组,因此需要一个参数记录遍历到了数组的位置/下标,idx。这个情况不需要先对candidates排序。
完整Java代码如下:


class Solution {
    
    
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    List<Integer> temp = new ArrayList<Integer>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
    
    
        dfs(candidates, target,0); //初始idx为0
        return ans;
    }

    public void dfs(int[] candidates, int target,int idx){
    
    
        if(target == 0 ){
    
    
            ans.add(new ArrayList<Integer>(temp));
            return;
        }
        //已经遍历完数组
        if(idx == candidates.length)
            return;
        dfs(candidates,target,idx + 1); // 不选择这个数,跳过当前下标idx,下次从idx+1开始选
        //如果当前num比target小,选择
        if(target - candidates[idx] >=0){
    
    
            temp.add(candidates[idx]);
             //注:因为一个数可以选择多次,因此选择这个数后,idx不变化
            dfs(candidates, target - candidates[idx], idx);
            temp.remove(temp.size()-1);
        }
    }
}

40. 组合总和 II

40. 组合总和 II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target组合
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
在这里插入图片描述

递归法一:组合位置填空

这个题目和上面的区别在两点:

  • 上一题candidates中的数字没有重复;这个题目candidates是有重复的;
  • 上一题candidates中的数字可以无限次使用;这个题目candidates的数量是有限的;

组合位置填空法的遍历思路,还是每次遍历都要找一个数【在candidates中找一个】,但是这里需要增加一点以去除重复组合:比如candidates = [2,5,2,1,2]这个例子,先对candidates排序→candidates = [1,2,2,2,5];第一轮选择了1,第二轮选择[2,2,2,5]中的任意一个,那么第二个位置选2的就有3种情况【因为有3个2】,这就引起重复了。 去除重复:candidates已经排序,相等元素是相邻的,如果candidates[i] == candidates[i-1],说明这个candidates[i]不能再选了,但是i+1的情况知道,因此continue,后面的数字继续。
完整Java代码如下:

class Solution {
    
    
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    List<Integer> temp = new ArrayList<Integer>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    
    
        Arrays.sort(candidates);
        dfs(candidates,target,0);
        return ans;
    }

    public void dfs(int[] candidates, int target, int idx){
    
    
        if(target == 0){
    
     //找到满足要求的组合,递归结束
            ans.add(new ArrayList<Integer>(temp));
            return;
        }
        if(idx == candidates.length) //遍历完数组,递归结束
            return; 
        // 遍历idx~ candidates.length-1的数,没有重复的就加入
        for(int i = idx; i < candidates.length && target >= candidates[i]; i++){
    
    
            if( i > idx && candidates[i] == candidates[i-1]) //重复了,跳过该数
                continue;
            temp.add(candidates[i]);
            dfs(candidates, target - candidates[i], i+1);
            temp.remove(temp.size()-1);
        }    
    }
}

递归法二:遍历数组

上面一题,在遍历数组的递归过程中,选择一个数字后,由于这个数字可以重复使用,因此idx不变化。 那么是否这个题目,针对一个数有选or不选的情况;不管选or不选,idx+1,选下一个,是否正确?
问题就出在这个题目的candidates是重复的,还是candidates = [1,2,2,2,5],target = 5这个例子,三个2,不选第一个2+选第二个2,选第一个2+不选第二个2,实际是一样的。

❌ 常规思路:有重复

用上面的常规思路,写的代码:

class Solution {
    
    
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    List<Integer> temp = new ArrayList<Integer>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    
    
        Arrays.sort(candidates);
        dfs(candidates,target,0);
        return ans;
    }
  public void dfs(int[] candidates, int target, int idx){
    
    
        if(target == 0){
    
    
            ans.add(new ArrayList<Integer>(temp));
            return;
        }
        if(idx == candidates.length)
            return;
        if(target >= candidates[idx]){
    
    
            temp.add(candidates[idx]);
            dfs(candidates, target - candidates[idx],idx+1);
            temp.remove(temp.size()-1);
        } 
        dfs(candidates,target,idx+1);
    }
}

结果如下,有重复:

在这里插入图片描述
这个test case的结果中出现了两次[1,2,5]这是因为candidates中有两个1,组合第一位就有两种选择;同样[1,7]也重复。 但是[1,1,6]不重复,是两个1都用上了!
在这里插入图片描述
这个test case的结果中出现了三次[1,2,2]是因为candidates中有3个2,结果中的两个2,可以是(第一个2,第二个2)、(第一个2,第三个2)、(第二个2,第三个2)。

❗hash表解决冲突

这个解决冲突的思路,就是先把candidates去重。统计candidates中的数及出现的次数,存在freq中。那么freq中的每个数有选or不选,两种情况;针对选的情况,又有选几次的问题:min(target/num, count),即现在的target最多能选几个num、这个num在candidates中有一个,二者的最小值。 完整java代码如下:

class Solution {
    
    
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    List<Integer> temp = new ArrayList<Integer>();
    List<int[]> freq = new ArrayList<int[]>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    
    
        Arrays.sort(candidates);
        // 先统计candidates中的数及出现的频率
        for(int num : candidates){
    
    
            int size = freq.size();
            if(freq.isEmpty() || num != freq.get(size-1)[0])
                freq.add(new int[]{
    
    num,1});
            else
                freq.get(size-1)[1]++;
         }        
        dfs(target,0);
        return ans;
    }
  public void dfs(int target, int idx){
    
    
        if(target == 0){
    
    
            ans.add(new ArrayList<Integer>(temp));
            return;
        }
        // 已经遍历完freq中的数 或者 当前freq中的数已经大于targetl直接剪枝
        if(idx == freq.size() || target < freq.get(idx)[0])
            return;
    
        // 依次选择1~ target/num个num这个数
        int num = freq.get(idx)[0];
        int most = Math.min(target/num, freq.get(idx)[1]);
        for(int i = 1; i <= most; i++){
    
    
            temp.add(num);
            dfs(target - i*num, idx + 1);
        } 
        // 选择后要移除掉
        for(int i = 1; i <= most; i++)
            temp.remove(temp.size()-1);
         
        // 这个数字一个都不选!
        dfs(target, idx + 1);
    }
}

377. 组合总和 Ⅳ 【动态规划】

377. 组合总和 Ⅳ

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数
题目数据保证答案符合 32 位整数范围。
在这里插入图片描述

这个题目虽然是一系列的变化题目…但是需要用动态规划求解。对于数字k,遍历nums数组中比k小的其他元素j,dp[k] += dp[k-j]。 其中dp[0] = 1, 表示k == num的情况,只有这一种组合。那么完整版Java代码如下:

class Solution {
    
    
    public int combinationSum4(int[] nums, int target) {
    
    
        int n = nums.length;
        int[] count = new int[target+1];
        count[0] = 1;
        Arrays.sort(nums);
        for(int i = 1; i <= target; i++){
    
     //从小到大依次计算i的组合情况
            for(int j = 0; j < n && nums[j] <= i; j++) // 遍历nums数组中的元素,加入组合
                count[i] += count[i - nums[j]];
        }
        return count[target];
    }
}

猜你喜欢

转载自blog.csdn.net/massive_jiang/article/details/141066436