39. 组合总和 ●● & 40. 组合总和 II ●● & 216. 组合总和 III ●●

本文深入探讨了组合总和问题的三种不同情况,分别涉及无限制重复选取、元素不可重复选取和特定数量元素的组合。通过排序和回溯策略,解决了在给定数字集中找到目标和的所有可能组合。对于重复组合的去重,通过在回溯过程中使用标记数组实现。此外,还分析了每种情况下的剪枝操作和时间、空间复杂度。
摘要由CSDN通过智能技术生成

39. 组合总和 ●●

描述

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

题解

1. 回溯 + 排序剪枝

特点:
– 组合没有数量要求
– 元素可无限重复选取

因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回。

参数:遍历索引、数组、目标差值
终止条件:差值等于0
单层逻辑:for 循环从 index 开始,遍历数组集合(对数组排序可以减少回溯次数)

在这里插入图片描述

class Solution {
public:

    vector<vector<int>> ans;
    vector<int> path;

    void backtrack(int idx, vector<int>& candidates, int target){
        if(target == 0){
            ans.emplace_back(path);
            return;
        }
        for(int i = idx; i < candidates.size(); ++i){
            if(target - candidates[i] < 0) break;			// 剪枝,不用往下递归
            path.emplace_back(candidates[i]);
            backtrack(i, candidates, target-candidates[i]);	// 递归、隐式回溯,可重复取值,索引为 i
            path.pop_back();								// 回溯
        }
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());			// 排序,用于剪枝
        backtrack(0, candidates, target);
        return ans;
    }
};

40. 组合总和 II ●●

描述

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次

注意:解集不能包含重复的组合

示例

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

题解

1. 回溯 + 去重

此题重点在于重复组合的去重操作,所谓去重,其实就是使用过的元素不能重复选取。

组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过(递归纵向遍历),一个维度是同一树层上使用过(for横向遍历,i > start)

本题中,元素在同一个组合内是可以重复的,但两个组合不能相同。

所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。树层去重的话,需要对数组排序

在这里插入图片描述

  • 排序,使用 vector<bool> &used 数组进行重复使用标记
class Solution {
public:
    vector<vector<int>> ans;
    vector<int> set;

    void backtrack(int index, vector<int> candidates, int difference, vector<bool> &used){
        if(difference == 0){
            ans.emplace_back(set);
            return;
        }
        for(int i = index; i < candidates.size(); ++i){
            // used[i - 1] == true, 说明   **同一树枝**   candidates[i-1]使用过
   			// used[i - 1] == false,说明   **同一树层**   candidates[i-1]使用过
   			// 要对同一树层使用过的元素进行跳过
            if(i > 0 && candidates[i] == candidates[i-1] && used[i-1] == false){
                continue;
            }
            difference -= candidates[i];
            if(difference < 0){
                break;					// 剪枝操作
            }else{
                set.emplace_back(candidates[i]);
                used[i] = true;			// 树枝上使用过
                backtrack(i+1, candidates, difference, used);	// 递归 纵向遍历
                used[i] = false;				// 树层上使用过
                set.pop_back();					// 回溯 
                difference += candidates[i];	// 回溯 
            }     
        }
    }
    
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        vector<bool> used(candidates.size(), false);
        backtrack(0, candidates, target, used);
        return ans;
    }
};
  • 排序,直接在for循环中判断与上一个元素是否重复进行去重判断

注意判断条件i > index 时才是横向遍历的条件,否则可能是递归调用导致的判断。

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> set;

    void backtrack(int index, vector<int> candidates, int difference){
        if(difference == 0){
            ans.emplace_back(set);
            return;
        }
        for(int i = index; i < candidates.size(); ++i){
            if(i > index && candidates[i] == candidates[i-1]){
                continue;                   // 跳过同一层使用过的元素(for 横向遍历)
            }                               // 注意判断条件, i > index 时才是横向遍历的条件,否则可能是递归调用的判断
            
            difference -= candidates[i];
            if(difference < 0){             // 剪枝操作
                break;
            }else{
                set.emplace_back(candidates[i]);
                backtrack(i+1, candidates, difference); // 递归 纵向遍历,索引为 i+1
                set.pop_back();                 // 回溯 
                difference += candidates[i];    // 回溯
            }      
        }
    }
    
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end()); //排序
        backtrack(0, candidates, target);
        return ans;
    }
};

216. 组合总和 III ●●

描述

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

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

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

示例

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

题解

1. 回溯

两处剪枝操作:

  1. 当前组合和比目标值大时;
  2. for 循环遍历的上限值剪枝。
  • 时间复杂度: O ( ( M k ) × k ) O({M \choose k} \times k) O((kM)×k),其中 M 为集合的大小,本题中 M 固定为 9。一共有 ( M k ) M \choose k (kM)(个组合,每次判断需要的时间代价是 O(k)。
  • 空间复杂度:O(M)。临时数组的空间代价是 O(k),递归栈空间的代价是 O(M),故空间复杂度为 O(M+k)=O(M).
class Solution {
public:
    vector<vector<int>> ans;
    vector<int> set;

    void backtrack(int k, int start, int num){      // k为元素个数,start为for遍历起始值,num为与目标差值
        if(num < 0) return;                         // 与目标差值num<0,即和过大,停止递归,剪枝
        if(set.size() == k){                        // k个数时,结束递归
            if(num == 0) ans.emplace_back(set);     // 若组合和满足条件,即差值num=0时,保存数组
            return;
        }
        int m = 9-(k-set.size())+1;			// 对for循环最大值进行剪枝优化		
        for(int i = start; i <= m; ++i){    // 比如size=0时,遍历到7就停止,因为从8开始凑不够三个数        
            set.emplace_back(i);                    
            backtrack(k, i+1, num-i);               // 递归
            set.pop_back();                         // 回溯
        }

    }
    
    vector<vector<int>> combinationSum3(int k, int n) {
        backtrack(k, 1 , n);
        return ans;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值