今日算法(组合总和)(回溯问题)

题目描述

给定一个无重复元素 的整数数组 candidates 和一个目标整数 target,找出 candidates 中可以使数字和为目标数 target 的所有不同组合,并以列表形式返回。

关键规则:

  • candidates 中的同一个数字可以无限制重复被选取。
  • 如果至少一个数字的被选数量不同,则两种组合是不同的。
  • 保证和为 target 的不同组合数少于 150 个。

示例 1:

  • 输入:candidates = [2,3,6,7], target = 7
  • 输出:[[2,2,3],[7]]
  • 解释:2+2+3=7,7=7,仅这两种组合。

示例 2:

  • 输入:candidates = [2,3,5], target = 8
  • 输出:[[2,2,2,2],[2,3,3],[3,5]]

解法:回溯法(深度优先搜索)

核心思路

这道题本质上是带条件的组合问题,可以用回溯法解决:

  1. 路径记录 :用一个列表 path 保存当前正在构建的组合。
  2. 和的跟踪 :用一个变量 sum 记录当前路径的和。
  3. 终止条件 :
    • 当 sum == target:将当前 path 加入结果集。
    • 当 sum > target:直接返回(剪枝)。
  4. 可重复选取:为了避免重复组合,搜索时只能从当前元素开始向后遍历(不能回头),这样既保证了元素可重复使用,又不会出现排列重复的情况。

代码实现(C++)

复制代码
class Solution {
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>> res;
        vector<int> path;
        backtrack(candidates, target, 0, 0, path, res);
        return res;
    }

private:
    /**
     * @param candidates 候选数组
     * @param target 目标和
     * @param start 本次搜索的起始位置(避免重复组合)
     * @param sum 当前路径的和
     * @param path 当前路径
     * @param res 结果集
     */
    void backtrack(const vector<int>& candidates, int target, int start, int sum, 
                   vector<int>& path, vector<vector<int>>& res) {
        // 终止条件1:和等于目标,记录结果
        if (sum == target) {
            res.push_back(path);
            return;
        }
        // 终止条件2:和超过目标,剪枝
        if (sum > target) {
            return;
        }

        // 从start开始遍历,避免重复组合
        for (int i = start; i < candidates.size(); ++i) {
            // 选择当前元素
            path.push_back(candidates[i]);
            sum += candidates[i];

            // 递归搜索:下一层仍从i开始(可重复选取)
            backtrack(candidates, target, i, sum, path, res);

            // 回溯:撤销选择
            sum -= candidates[i];
            path.pop_back();
        }
    }
};

优化版(排序 + 剪枝)

先对数组排序,在循环中提前终止不可能的分支,提高效率:

复制代码
class Solution {
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>> res;
        vector<int> path;
        sort(candidates.begin(), candidates.end()); // 排序
        backtrack(candidates, target, 0, 0, path, res);
        return res;
    }

private:
    void backtrack(const vector<int>& candidates, int target, int start, int sum, 
                   vector<int>& path, vector<vector<int>>& res) {
        if (sum == target) {
            res.push_back(path);
            return;
        }

        for (int i = start; i < candidates.size(); ++i) {
            if (sum + candidates[i] > target) break; // 剪枝:后面的元素更大,直接终止

            path.push_back(candidates[i]);
            backtrack(candidates, target, i, sum + candidates[i], path, res);
            path.pop_back();
        }
    }
};

详细步骤解析(以 candidates = [2,3,6,7], target = 7 为例)

1. 初始状态

  • path = [],sum = 0,start = 0

2. 第一次递归(i=0,元素 2)

  • 选择 2:path = [2],sum = 2
  • 递归调用 backtrack,start = 0(可继续选 2)
第二次递归(i=0,元素 2)
  • 选择 2:path = [2,2],sum = 4
  • 递归调用 backtrack,start = 0
第三次递归(i=0,元素 2)
  • 选择 2:path = [2,2,2],sum = 6
  • 递归调用 backtrack,start = 0
    • 第四次递归:选 2 → sum=8>7,剪枝返回
    • 回溯,弹出 2,sum=6
    • i=1,选 3 → sum=6+3=9>7,剪枝返回
    • ... 后续元素均大于剩余和,返回
  • 回溯,弹出 2,sum=4
  • i=1,选 3 → sum=4+3=7,path=[2,2,3],加入结果集
回溯过程
  • 弹出 3,sum=4 → 继续遍历,后续元素过大,返回
  • 弹出 2,sum=2 → i=1,选 3 → sum=5,递归后无法凑够 7,返回
  • i=2,选 6 → sum=8>7,剪枝返回
  • i=3,选 7 → sum=9>7,剪枝返回

3. 第一次递归(i=1,元素 3)

  • 选 3:sum=3,后续无法凑够 7,返回
  • i=2,选 6:sum=6,后续无法凑够 7,返回
  • i=3,选 7:sum=7,path=[7],加入结果集

最终结果

[[2,2,3],[7]]


关键知识点解析

1. 为什么 start 参数能避免重复组合?

  • 如果每次都从 0 开始遍历,会出现 [2,3] 和 [3,2] 这样的重复组合。
  • 从 start 开始遍历,保证了组合的元素顺序与原数组一致,不会出现排列重复。
  • 同时,递归时仍从 i 开始,允许元素重复选取。

2. 回溯的本质:选择 - 递归 - 撤销选择

  • 选择 :将当前元素加入 path,更新 sum。
  • 递归:进入下一层搜索。
  • 撤销选择 :弹出当前元素,恢复 sum,尝试下一个元素。

3. 剪枝优化的原理

  • 对数组排序后,如果当前元素 candidates[i] 已经大于 target - sum,后面的元素会更大,无需继续遍历,直接 break。
  • 大幅减少无效递归调用,提升效率。

复杂度分析

  • 时间复杂度:\(O(n \times 2^n)\),其中 n 是数组长度。每个元素都有选或不选两种可能,最坏情况下需要遍历所有组合。
  • 空间复杂度:\(O(target / min(candidates))\),递归栈深度最多为目标和除以最小元素,即最长组合的长度。

常见误区与注意事项

  1. 忘记设置 start 参数 :会导致重复组合,如 [2,3] 和 [3,2]。
  2. 递归时 start 设为 i+1:会变成 "元素不可重复选取" 的组合问题,不符合题意。
  3. 未进行剪枝优化 :对于较大的 target 和数组,会超时。

总结

这道题是回溯算法的经典入门题,核心是理解:

  1. 如何用 start 参数控制组合顺序,避免重复。
  2. 如何利用排序 + 剪枝优化搜索效率。
  3. 回溯的 "选择 - 递归 - 撤销选择" 模板。
相关推荐
想吃火锅10052 分钟前
【leetcode】189.轮转数组js
开发语言·javascript·leetcode
大侠归来9 分钟前
cJSON 源码解析:parse_string 函数深入剖析
linux·网络·算法
余额瞒着我当琳1 小时前
C++ map 底层与AVL 树
开发语言·c++·算法
释厄6231 小时前
物理发展史·量子力学=节点2·牛顿力学=节点3·宇宙的语言算法AI
人工智能·算法·microsoft
CQU_JIAKE1 小时前
9.30【A】
算法
INGNIGHT2 小时前
1897 · 会议室 3(扫描线)
数据结构·算法
All for pursuit.2 小时前
【贪心-2】253.会议室 II
数据结构·c++·算法·leetcode
垆边人似月.2 小时前
华为机考题(一):质数因子
算法·华为
迷途之人不知返2 小时前
算法系列5:位运算
算法
牢姐与蒯2 小时前
c++加餐之位图
c++·算法·哈希