给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。

说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。

示例 1:
输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]

示例 2:
输入: candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]

思路分析:拿到这道题的时候,我首先想起的是前些时候做过的三数之和、四数之和等题型(如果没有做过,请参考我之前的博客)。但是此题与之前的题型稍有不同,之前的题都是确定好了数的个数,而此道题与数的个数无关,主要是要凑齐target。对于这种题型,比较常用的解法是“回溯法”(深度优先搜索)。即从数组中不断拿出元素进行凑,如果不合格就退回到上一步,合格就继续下一步。
初始代码如下:

class Solution {
public:
    set<vector<int> > resultSet;//采取集合的形式方便去重
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        resultSet.clear();//首先对结果集合进行清空
        int candidateSize = candidates.size();
        if (candidateSize == 0){
            return vector<vector<int>>(resultSet.begin(), resultSet.end());//返回结果为vector,需要转换
        }
        sort(candidates.begin(), candidates.end());//进行排序处理,方便选取
        vector<int> tempResult;//中间结果
        searchDSF(candidates, tempResult, target);//进行搜索
        return vector<vector<int>>(resultSet.begin(), resultSet.end());
    }
    //candidates是候选的元素,tempResult表示中间结果,target表示此时还需要凑的数字
    void searchDSF(vector<int>& candidates, vector<int> &tempResult, int target){
        if (target == 0){//如果求和成功
            vector<int> tempVector = tempResult;//1⃣️再次复制一下,需要进行排序处理
            sort(tempVector.begin(), tempVector.end());//将中间结果进行排序,方便去重
            resultSet.insert(tempVector);//放入结果集合中
        }
        else if (target >= candidates[0]){//只能当还有元素可以被选入的情况下进行
            int candidateSize = candidates.size();
            //对候选元素进行穷举
            for (int i = 0; i < candidateSize; ++i){
                if (candidates[i] <= target){//如果这个数小于target,则说明能够使用
                    tempResult.push_back(candidates[i]);//放入中间结果
                    searchDSF(candidates, tempResult, target - candidates[i]);//继续搜寻target -candidates[i]
                    tempResult.pop_back();//搜索之后,需要把之前的删除,这就显示1⃣️操作的重要性了,否则排序后,尾端不一定是前面放进去的那个元素
                }
            }
        }
    }
};

LeetCode 组合总数
代码优化:上面的代码对于重复的结果需要靠set的元素的唯一性,才能进行去重,由于两个vector容器只有当元素值、顺序完全一样才认为是一样的,所以每次得到一个正确的结果都需要排序,才能去重。那我们能不能在源头上就不让它产生重复选项,这样就不需要去重处理?(请思考一下)
举个栗子:
第一种情况:第一次选择了2,第二次选择了3,第三次…
第二种情况:第一次选择了3,第二次选择了3,第三次…
经过排序处理后的vector容器,完全是一样的。这时我们就要想起,在进行深搜之前,对candidates容器进行了排序,那我们能不能对放入的元素采取非递减的规则。这样就可以缩减了去重的过程。

优化后的代码:

class Solution {
public:
    set<vector<int> > resultSet;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        resultSet.clear();//清空
        int candidateSize = candidates.size();
        if (candidateSize == 0){
            return vector<vector<int>>(resultSet.begin(), resultSet.end());
        }
        sort(candidates.begin(), candidates.end());//进行排序处理
        vector<int> tempResult;//中间结果
        searchDSF(candidates, tempResult, target, 0);//进行搜索
        return vector<vector<int>>(resultSet.begin(), resultSet.end());
    }
    //beginIndex用于标记之前使用过的最大的下标
    void searchDSF(vector<int>& candidates, vector<int> &tempResult, int target, int beginIndex){
        if (target == 0){//如果求和成功
            resultSet.insert(tempResult);//放入结果集合中
        }
        else if (target >= candidates[0]){//只能当还有元素可以被选入的情况下进行
            int candidateSize = candidates.size();
            //对候选元素进行穷举
            //beginIndex用于标记之前使用过的最大的下标,从这个下标开始才能保持f,放入中间结果的元素非递减的顺序
            for (int i = beginIndex; i < candidateSize; ++i){
                if (candidates[i] <= target){//如果这个数小于target,则说明能够使用
                    tempResult.push_back(candidates[i]);//放入中间结果
                    searchDSF(candidates, tempResult, target - candidates[i], i);//继续搜寻target -candidates[i]
                    tempResult.pop_back();
                }
            }
        }
    }
};

LeetCode 组合总数
但是貌似还忘记了啥!!!既然此时已重源头上避免的重复中间结果的出现,那么我们现在为啥还需要使用set容器,而不直接使用vector容器进行储存呢?
下面的代码将set容器修改为vector容器

class Solution {
public:
    vector<vector<int> > resultVec;//set容器修改为vector容器
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        resultVec.clear();
        int candidateSize = candidates.size();
        if (candidateSize == 0){
            return resultVec;
        }
        sort(candidates.begin(), candidates.end());//进行排序处理
        vector<int> tempResult;//中间结果
        searchDSF(candidates, tempResult, target, 0);//进行搜索
        return resultVec;
    }
    
    void searchDSF(vector<int>& candidates, vector<int> &tempResult, int target, int beginIndex){
        if (target == 0){//如果求和成功
            resultVec.push_back(tempResult);//放入结果集合中
        }
        else if (target >= candidates[0]){//只能当还有元素可以被选入的情况下进行
            int candidateSize = candidates.size();
            //对候选元素进行穷举
            for (int i = beginIndex; i < candidateSize; ++i){
                if (candidates[i] <= target){//如果这个数小于target,则说明能够使用
                    tempResult.push_back(candidates[i]);//放入中间结果
                    searchDSF(candidates, tempResult, target - candidates[i], i);//继续搜寻target -candidates[i]
                    tempResult.pop_back();
                }
            }
        }
    }
};

LeetCode 组合总数
可能有不少师兄、弟知道“回溯法”(深度优先搜索法)时间复杂度一般都比较大,需要采取一些“剪枝算法”进行辅助,(有时候一个好的剪枝算法可以大大的降低整个算法的时间复杂度),但是我的代码貌似没有见到呀!
请回到searchDSF函数中的

 else if (target >= candidates[0]){//只能当还有元素可以被选入的情况下进行

这句算法就是“剪枝算法”,含义就是,只有当仍需要凑齐的target大于候选容器中最小的元素,才可能继续进行搜索。
但是这句还可以改成

 else if (target >= candidates[beginIndex]){//candidates[beginIndex]表示的上一次之前最大的元素,由于之前从源头上去重的算法采取非递减策略,所以仍需要凑齐的数必须要大于等于使用过的最大的数,才能继续进行凑。

由于领扣中国的测试数据量比较小,我更换剪枝算法还是8ms,看不出效果,但是千万别小看“剪枝算法”的使用在使用“回溯法”时。

相关文章:

  • 2021-12-28
  • 2019-11-03
  • 2019-11-06
  • 2019-11-12
  • 2021-08-20
  • 2020-02-27
  • 2021-09-19
  • 2021-08-22
猜你喜欢
  • 2021-08-13
  • 2021-12-25
  • 2021-04-26
  • 2021-10-08
  • 2022-01-08
  • 2021-06-24
  • 2021-09-16
  • 2021-07-15
相关资源
相似解决方案