回溯算法专题(五):去重与剪枝的双重奏——攻克「组合总和 II」

哈喽各位,我是前端小L。

欢迎来到我们的回溯算法专题第五篇!今天我们要解决的,是组合问题中约束最复杂的一种情况。

我们要在一个乱序、有重复的数组中,找出和为 target 的组合,且不能有重复解。

这道题就像是在告诉我们:"既要(数字凑够),又要(不能重复用),还要(解集不重复)"。

面对这种"既要又要还要"的需求,我们的回溯模板需要开启"完全体"模式:排序 + 树层去重 + 剪枝。

力扣 40. 组合总和 II

https://leetcode.cn/problems/combination-sum-ii/

题目分析:

  • 输入 :数组 candidates(有重复),目标 target

  • 目标 :找出所有和为 target 的组合。

  • 约束

    1. 每个数字在每个组合中只能使用一次

    2. 解集不能包含重复的组合。

例子:

candidates = [10,1,2,7,6,1,5], target = 8

  • 排序后:[1, 1, 2, 5, 6, 7, 10]

  • 组合 [1, 7] (第一个1)

  • 组合 [1, 7] (第二个1) -> 重复!必须去掉!

  • 组合 [1, 2, 5]

  • 组合 [2, 6]

  • 组合 [1, 1, 6]

核心策略:排序 + "树层去重"

这道题的去重逻辑,和 LC 90 子集 II 完全一致!

  1. 必须排序

为了让重复的元素聚在一起,方便我们判断,第一步必须是 sort。

2. 树层去重 vs 树枝去重

  • 树枝去重(纵向):同一个分支深处,能不能选重复的元素?

    • 题目说"每个数字只能用一次",指的是同一个索引 的数字只能用一次。但如果数组里有两个 1(索引不同),我们当然可以在一个组合里同时选这两个 1(比如 [1, 1, 6])。这是允许的。
  • 树层去重(横向):同一个父节点下,能不能选值相同的兄弟?

    • 假如第一层选了第一个 1,我们会递归搜索出所有以 1 开头的组合(包括 [1, 7])。

    • 回溯后,同层循环到了第二个 1。如果我们再选它,必然会生成重复的 [1, 7]。这是禁止的。

通用去重模板:

C++

复制代码
if (i > startIndex && candidates[i] == candidates[i-1]) {
    continue;
}
  • i > startIndex:确保是"同层后续元素"(横向),而不是"递归下一层元素"(纵向)。
  1. 递归参数

因为每个数字只能用一次,所以递归时传入 i + 1(指向下一个数),而不是 i。

代码实现 (C++)

C++

复制代码
#include <vector>
#include <algorithm> // for sort

using namespace std;

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    int currentSum = 0;

    void backtrack(vector<int>& candidates, int target, int startIndex) {
        // 1. 结束条件 (成功)
        if (currentSum == target) {
            res.push_back(path);
            return;
        }

        // 2. 遍历选择列表
        for (int i = startIndex; i < candidates.size(); ++i) {
            // --- 核心剪枝 1:数值剪枝 ---
            // 因为数组已排序,如果加上当前数这就超了,后面的肯定更超
            // 直接 break,大幅提升效率
            if (currentSum + candidates[i] > target) {
                break; 
            }

            // --- 核心剪枝 2:树层去重 ---
            // 如果当前元素和前一个元素相同,且前一个元素没被选(i > startIndex)
            // 说明是同层重复,跳过
            if (i > startIndex && candidates[i] == candidates[i-1]) {
                continue;
            }

            // 做选择
            path.push_back(candidates[i]);
            currentSum += candidates[i];

            // 递归
            // 关键:传入 i + 1,因为当前元素不可复用
            backtrack(candidates, target, i + 1);

            // 撤销选择
            currentSum -= candidates[i];
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        res.clear();
        path.clear();
        currentSum = 0;
        
        // 0. 必须排序!去重和剪枝的基础
        sort(candidates.begin(), candidates.end());
        
        backtrack(candidates, target, 0);
        
        return res;
    }
};

深度对比:组合问题的"三剑客"

到现在,我们已经彻底拿下了 LeetCode 上最经典的三道"组合"题。让我们最后一次横向对比,把它们刻在脑子里:

题目 LC 39 组合总和 LC 40 组合总和 II LC 216 组合总和 III (下期预告)
输入数组 无重复 有重复 1-9 (无重复)
元素复用 无限复用 不可复用 不可复用
目标 和为 target 和为 target 和为 n,且个数为 k
排序 需要 (为了剪枝) 必须 (为了去重+剪枝) 不需要 (天然有序)
递归参数 i i + 1 i + 1
去重逻辑 i > start && nums[i]==nums[i-1]

LC 40 (本题) 是最能体现回溯算法"控制力"的一题。它要求我们像外科医生一样,精准地剔除重复的枝叶,同时保留合法的解。

下一篇,我们将快速解决 LC 216. 组合总和 III 。这道题增加了一个新的约束------"个数限制" (k)。这对我们的递归深度也是一种剪枝条件!

下期见!

相关推荐
池央4 分钟前
贪心-最长递增子序列
算法·贪心算法
We་ct9 分钟前
LeetCode 383. 赎金信:解题思路+代码解析+优化实战
前端·算法·leetcode·typescript
不懒不懒20 分钟前
【逻辑回归从原理到实战:正则化、参数调优与过拟合处理】
人工智能·算法·机器学习
一只大袋鼠21 分钟前
分布式 ID 生成:雪花算法原理、实现与 MyBatis-Plus 实战
分布式·算法·mybatis
tobias.b24 分钟前
408真题解析-2010-27-操作系统-同步互斥/Peterson算法
算法·计算机考研·408真题解析
寄存器漫游者33 分钟前
数据结构 二叉树核心概念与特性
数据结构·算法
m0_7066532336 分钟前
跨语言调用C++接口
开发语言·c++·算法
皮皮哎哟38 分钟前
数据结构:从队列到二叉树基础解析
c语言·数据结构·算法·二叉树·队列
一匹电信狗1 小时前
【高阶数据结构】并查集
c语言·数据结构·c++·算法·leetcode·排序算法·visual studio
愚者游世1 小时前
list Initialization各版本异同
开发语言·c++·学习·程序人生·算法