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

题目描述

给定一个无重复元素 的整数数组 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 = 0start = 0

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

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

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

  • 3sum=3,后续无法凑够 7,返回
  • i=2,选 6sum=6,后续无法凑够 7,返回
  • i=3,选 7sum=7path=[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. 回溯的 "选择 - 递归 - 撤销选择" 模板。
相关推荐
EllinY1 小时前
CF2217E Definitely Larger 题解
c++·笔记·算法·构造
玖釉-4 小时前
下一个排列:从字典序到原地算法的完整推导
数据结构·c++·windows·算法
IronMurphy4 小时前
【算法五十】62. 不同路径
算法
影寂ldy5 小时前
C#一维数组
算法
过期动态5 小时前
【LeetCode 热题 100】移动零
java·数据结构·算法·leetcode·职场和发展·rabbitmq
计算机安禾6 小时前
【算法分析与设计】第10篇:下界理论与NP完全性初步
大数据·人工智能·算法
水木流年追梦7 小时前
大模型入门-大模型分布式训练2
开发语言·分布式·python·算法·正则表达式·prompt
sali-tec7 小时前
C# 基于OpenCv的视觉工作流-章78-KRT测量
图像处理·人工智能·数码相机·opencv·算法·计算机视觉
菜菜的顾清寒7 小时前
力扣HOT100(32)二叉树的中序遍历
数据结构·算法·leetcode