力扣HOT100 回溯 组合总和

一、先抓准题目核心特点

先明确三个关键条件,决定了我们的解法方向:

  1. 数组元素无重复:不用处理 "相同元素选多次导致重复组合" 的问题
  2. 每个数字可以无限次选取:选完一个数之后,还能继续选它自己
  3. 组合不重复[2,3][3,2] 算同一种组合,必须去重

这道题的本质是带无限选取的组合枚举,属于典型的「回溯算法」题。

二、核心难点:怎么保证组合不重复?

这是这道题最关键的问题:如果随便选,很容易出现 [2,3][3,2] 这种顺序不同、但元素完全一样的重复组合。

通用去重思想:强制组合按数组顺序递增 我们规定:选数字的时候,只能从左往右选,不能回头选左边的数字 。 比如数组是 [2,3,6,7]

  • 选了 2 之后,下一个可以继续选 2,或者选 3、6、7
  • 但选了 3 之后,下一个不能回头选 2 这样组合里的数字永远是按数组顺序出现的,天然就不会有顺序颠倒的重复组合。

方法一:「选或不选」决策法(最易理解)

1. 思路原理

对数组里的每一个数字,我们只做两种决策:

  • 不选它:跳过这个数字,去考虑下一个数字
  • 选它 :把它加入当前路径,因为可以重复选,所以下一层依然考虑这个数字(不往后跳)

对应完全背包的思路:每个物品可以选无限次,决策 "拿还是不拿"。

2. 递归三要素

  • 递归参数i(当前考虑到第 i 个数字)、left(距离 target 还差多少)
  • 递归边界
    1. left == 0:凑出目标和了,把当前路径加入答案,返回
    2. i == 数组长度left < 0:所有数字考虑完了,或者凑超了,直接返回
  • 递归决策
    1. 不选当前数:递归 dfs(i+1, left)
    2. 选当前数:把数字加入路径 → 递归 dfs(i, left - candidates[i])(i 不变,下次还能选) → 递归回来后弹出数字(恢复现场)

3. 手算走一遍(示例 1:candidates=2,3, target=5)

plaintext

复制代码
dfs(i=0, left=5)  // 考虑数字2
├─ 不选2 → dfs(i=1, left=5)  // 考虑数字3
│   ├─ 不选3 → i=2越界,返回
│   └─ 选3 → left=2,继续dfs(i=1, left=2),3>2,最后返回
└─ 选2 → path=[2], left=3,dfs(i=0, left=3)
    ├─ 不选2 → dfs(i=1, left=3)
    │   └─ 选3 → left=0,path=[2,3],加入答案
    └─ 选2 → path=[2,2], left=1,继续递归后返回

最终只得到 [2,3],没有重复组合。

cpp 复制代码
class Solution {
    vector<vector<int>> ans;
    vector<int> path;
    vector<int> candidates;

    void dfs(int i,int left){ //i代表当前数的索引
        //left 表示和target的距离
        //如果left为0 就是目标值了 直接加入答案
        if(left == 0){
            ans.push_back(path);
            return;
        }
        //边界2 如果数字用完了 或 凑超了,返回
        if(i == candidates.size() || left<0){
            return;
        }

        //决策1 不选择当前的数 去下一个
        dfs(i+1,left);
        //决策2 选当前的数字
        path.push_back(candidates[i]);
        dfs(i,left-candidates[i]);//i不变 因为是选当前的数 然后距离变小了
        path.pop_back();//恢复现场
    }

public:


    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        this->candidates = candidates;
        dfs(0,target);
        return ans;
    
    }
};