回溯问题(二):组合和问题及回溯问题的横向去重和纵向去重

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

1、问题抽象与代码实现

1)问题抽象

在这里插入图片描述

2)代码实现

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
		/----回溯终止条件1:当前和大于给定值----/
        if (sum > target) {
            return;
        }
        /----回溯终止条件2:当前和等于给定值----/
        if (sum == target) {
            result.push_back(path);
            return;
        }

		/-----回溯逻辑-----/
        for (int i = startIndex; i < candidates.size(); i++) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i); // 不用i+1了,表示可以重复读取当前的数
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

2、剪枝优化

我们可以对给定数组进行排序,排序后,如果回溯某个元素时和大于给定值,则后面的元素由于均大于当前元素,也就不用再回溯了

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum == target) {
            result.push_back(path);
            return;
        }

        // 如果 sum + candidates[i] > target 就终止遍历
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i);
            sum -= candidates[i];
            path.pop_back();

        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        sort(candidates.begin(), candidates.end()); // 需要排序
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

3、横向重复与纵向重复

纵向去重

题目中有以下两点要求:

  • 给定的数组中无重复元素
    这对应树结构中的横向层,给定的数组中无重复元素就是不需要横向去重
  • 数组中的元素可以重复使用
    这对应树结构中的纵向,当前数字使用过了还可以继续使用,因此回溯函数调用时候为
    backtracking(candidates, target, sum, i);
    如果要求数组中的元素不可以重复使用,则需要进行纵向去重,即使用过的数字不能再使用,此时回调函数调用为
    backtracking(candidates, target, sum, i + 1);
    纵向去重的关键在于当前节点调用递归函数时是否包括当前节点。如果包括当前节点,就没有去重;如过不包括当前节点,就纵向去重。

横向去重

当题目中给定的数组有重复元素又要求结果中不能有重复结果时,需要横向去重,如数组为[1,1,3,4],则排列[第一个1,3,4]和[第二个1,3,4]在结果中是重复的。

既然要横向去重,我们就要先对给定数组进行排序,从而根据元素是否与上一元素相等来确定该元素是否重复

我们使用一个与数组等长的vector<bool> used来标志在当前层中,对应的数字是否已经被使用。那么在进行当前路径选择时,当某个数字满足i > 0 && nums[i] == nums[i - 1] && used[i - 1]时, 该数字就是重复的,这条路径就不应该再被选择,具体解释如图:
在这里插入图片描述
如图,used数组表明的是数字在该层之前是否被使用,i > 0 && nums[i] == nums[i - 1] && used[i - 1]的含义是对于index = i的元素,在他之前有一个元素i,且该元素没有在该层之前被使用过;也就是说index = i - 1这个元素会在本层被使用,且在index = i的元素之前被使用,那么我们再使用index = i就是横向重复了,因此在本层for循环中要跳过index = i的元素。而对于首元素,肯定不重复,不用判断,同时i > 0一定要放在条件最前面,放置越界

组合总和 II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。

横向去重流程

  1. 数组排序
  2. 初始化used数组
  3. 当将某个元素添加到路径时,将其used设置为true,表明被使用了
  4. 使用nums[i] == nums[i - 1] && used[i - 1]判定是否同层重复
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
        if (sum == target) {
            result.push_back(path);
            return;
        }

		/---当前层横向循环遍历----/
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {

			/----判定当前元素是否同层重复,如果是,跳过该元素-----/
            if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
                continue;
            }

			/----如果不是,正常递归----/
            sum += candidates[i];
            path.push_back(candidates[i]);
            used[i] = true;   //使用了i元素,更改其used状态
            backtracking(candidates, target, sum, i + 1, used); 
            used[i] = false;  //回溯时候回溯i的used状态
            sum -= candidates[i];
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        path.clear();
        result.clear();
        /-----初始化used数组-----/
        vector<bool> used(candidates.size(), false);
        /-----数组排序----/
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0, used);
        return result;
    }
};

不排序的横向去重

491、给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是 2 
给定数组的长度不会超过15
数组中的整数范围是 [-100,100]
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
结果中不应该包含重复的递增子序列

如上,题目需要横向去重,但我们不能对数组进行排序,否则数据就乱了。这时候我们可以使用set来进行去重

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    void backTracking(vector<int>& nums, int startIndex){
        if(path.size() > 1) res.push_back(path);

		/-----set去重步骤1:在每个回调中(即每层遍历中)新建一个set来记录选择过的路径-----/
        unordered_set<int> numSet;    

        for(int i = startIndex; i < nums.size(); i++){
            if(path.size() != 0 && nums[i] < path.back()) continue;
            /-----set去重步骤2:如果当前路径被set记录过,跳过,横向去重----/
            if(numSet.find(nums[i]) != numSet.end()) continue;    

            /-----set去重步骤3:如果当前路径没有被set记录过,则使用set记录该路径-----/
            /-----注意该添加到set的操作不要回溯,因为这里记录的是曾经选择过该路径-----/
            numSet.insert(nums[i]);    
            path.push_back(nums[i]);
            backTracking(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        res.clear();
        path.clear();
        backTracking(nums, 0);
        return res;
    }
};

两种横向去重性能分析:在可以使用排序去重的时候不要使用set去重,set去重由于set多层压栈会显著增加运行时间和内存占用

  • 主要是因为程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的

  • 如果使用set去重,空间复杂度就变成了O(n^2),因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合

  • 而used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是O(n + n),最终空间复杂度还是O(n)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值