回溯法去重需要先排序

文章介绍了在处理包含重复元素的集合问题时,如何通过排序结合两种方法(一种是排序后逐个检查,另一种是使用unordered_set记录已出现元素)实现树层上的去重。特别强调了排序的重要性,尤其是在不能对输入排序的情况下,如LeetCode的491题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对于集合、组合、排列等经典回溯问题,当所提供元素出现重复时,往往需要去重。

以集合问题为例,根据代码随想录的题解,一般有两种办法进行树层上的去重。

方法一:先排序,从startindex的第二个元素开始,如果和前一个元素相等,则跳过该元素。这里排序是很自然的想法,因为要将相同的元素放在一起,才能在遍历时轻松找到重复元素。代码如下(copy from 代码随想录):

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex) {
        result.push_back(path);
        for (int i = startIndex; i < nums.size(); i++) {
            // 而我们要对同一树层使用过的元素进行跳过
            if (i > startIndex && nums[i] == nums[i - 1] ) { // 注意这里使用i > startIndex
                continue;
            }
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }

public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(), nums.end()); // 去重需要排序
        backtracking(nums, 0);
        return result;
    }
};

方法二:先排序,使用unordered_set记录当前层出现的元素,后续再出现时直接跳过,从而完成树层上的去重。代码如下(copy from 代码随想录):

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex) {
        result.push_back(path);
        unordered_set<int> uset;
        for (int i = startIndex; i < nums.size(); i++) {
            if (uset.find(nums[i]) != uset.end()) {
                continue;
            }
            // 记录当前层出现的元素进行去重,注意不要传到下一层去
            // 下一层会要一个新的uset记录出现过的元素
            uset.insert(nums[i]);
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }

public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(), nums.end()); // 去重需要排序
        backtracking(nums, 0);
        return result;
    }
};

对于方法二,既然已经利用了额外空间记录出现过的元素,那么就可以很容易进行树层上的去重,为什么还要进行排序呢。这是因为去重时需要连续跳过全部重复元素。

举个例子,对[4,4,4,1,4]获取全部子集并去重。如果不排序,那么对于树的第二层(第一层是空集),选取第一个4完成回溯后,直接来到1的位置,而1的后面还有元素4,则会出现[1,4]这个集合,而在之前的回溯过程中,已经出现了[4,1]这个集合了,也就是出现了重复。实际上,不排序得到的结果还有[4,1,4]和[4,4,1]重复,以及[4,4,1,4]和[4,4,4,1]重复。可见原序列中的最后一个4对结果造成了影响,因此需要连续跳过全部重复元素。

当我们排序之后,就变成了对[4,4,4,4,1](或者[1,4,4,4,4])获取全部子集并去重。使用额外的空间记录该层出现过的元素,便能连续跳过全部重复元素,从而完成去重。

而对于另外一道题,491. 非递减子序列 - 力扣(LeetCode),也是要求对结果进行去重,然而不可能对输入进行排序,那要怎么办呢。其实因为输出的子集本身就要求是有序的,那么1后面的4必然会被跳过,那么输出自然就不会同时有[1,4]和[4,1]这种顺序相反的结果了。因此只需要使用额外的集合去记录某层出现过的元素,便能完成去重。

回溯法是一种解决组合问题的有效方法。组合总和等于0的问题可以通过回溯法来解决。下面是基本的回溯法求解过程: 1. 定义一个数组用来存储符合条件的组合,以及一个变量用来记录当前组合的和。 2. 对原始数组进行排序,以便于后续的剪枝。 3. 从原始数组的第一个元素开始,依次枚举所有的元素。 4. 对于每个元素,如果当前组合的和加上该元素小于等于0,则将该元素加入到组合中,并更新当前组合的和。 5. 如果当前组合的和等于0,则将该组合加入到结果数组中。 6. 对于每个元素,如果当前组合的和加上该元素大于0,则不再继续向下搜索,直接返回上一层回溯。 7. 如果当前组合的和加上该元素等于0,则将该元素从组合中删除,并更新当前组合的和。 8. 复步骤4-7,直到枚举完所有元素。 9. 返回结果数组。 下面是Python的代码实现: ```python def combinationSum(nums): nums.sort() # 排序 res = [] def backtrack(path, target, start): if target == 0: # 找到符合条件的组合 res.append(path) return for i in range(start, len(nums)): if nums[i] > target: break # 剪枝 if i > start and nums[i] == nums[i-1]: continue # backtrack(path+[nums[i]], target-nums[i], i+1) backtrack([], 0, 0) return res ``` 该算法的时间复杂度为O(2^n),其中n为数组的长度。由于需要遍历所有可能的组合,因此时间复杂度比较高。同时,由于需要存储所有符合条件的组合,空间复杂度也比较高。因此,在实际应用中需要考虑性能问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值