回溯算法专题(五):去重与剪枝的双重奏——攻克「组合总和 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)。这对我们的递归深度也是一种剪枝条件!

下期见!

相关推荐
TL滕1 小时前
从0开始学算法——第三天(数据结构的多样性)
数据结构·笔记·学习·算法
V1ncent Chen1 小时前
人工智能的基石之一:算法
人工智能·算法
无限进步_1 小时前
深入理解顺序表:从原理到完整实现
c语言·开发语言·数据结构·c++·算法·链表·visual studio
兩尛1 小时前
欢乐周末 (2025B卷
算法
liu****1 小时前
九.操作符详解
c语言·开发语言·数据结构·c++·算法
ALex_zry1 小时前
C语言底层编程与Rust的现代演进:内存管理、系统调用与零成本抽象
c语言·算法·rust
TheLegendMe1 小时前
动态规划Day01
算法·动态规划
666HZ6661 小时前
C语言——交换
c语言·c++·算法
我爱鸢尾花1 小时前
RNN公式推导、案例实现及Python实现
人工智能·python·rnn·深度学习·神经网络·算法