回溯法生成子集需在递归前保存当前路径,每层决定选或不选当前元素;含重复元素时须先排序并跳过同一层重复分支;位运算适用于小规模枚举;注意返回值设计与边界验证。

用 std::vector + 回溯递归生成所有子集
子集问题本质是枚举所有可能的选/不选组合,C++ 中最直接的方式是回溯:每层决定是否把当前元素加入临时路径,递归处理后续元素。关键不是“剪枝”,而是保证每个状态都被访问且不重复。
常见错误是把 push_back 和 pop_back 放错位置,或在递归前漏掉记录当前路径(子集包含空集,必须在进入递归前就 result.push_back(path))。
- 每次递归调用前先保存当前
path,对应一个合法子集 - 先选当前元素(
path.push_back(nums[i])),递归下一层;再撤销(path.pop_back()),继续尝试不选 - 递归终止条件其实可省略——当
i == nums.size()时自然结束,无需显式return
void backtrack(const vector<int>& nums, int i, vector<int>& path, vector<vector<int>>& result) {
result.push_back(path); // 当前路径就是一个子集
for (int j = i; j < nums.size(); ++j) {
path.push_back(nums[j]);
backtrack(nums, j + 1, path, result);
path.pop_back();
}
}处理含重复元素的子集(如 [1,2,2])
原始回溯会生成重复子集,比如两个 2 交换顺序产生相同结果。解决方法不是用 set 去重(性能差、破坏顺序),而是在搜索树层面跳过等值的重复分支。
前提:先对输入排序。然后在循环内加判断:if (j > i && nums[j] == nums[j-1]) continue;。这个条件确保「同一层」中,相同数值只由第一个出现的位置展开分支。
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
-
j > i是关键:它区分了“同一层”和“不同层”。j == i是本层首次取该值,允许;后续等值的j就跳过 - 不能写成
nums[j] == nums[i]——那会错误地砍掉深层合法分支 - 排序是必要预处理,否则相等元素不相邻,跳过逻辑失效
用位运算枚举子集(仅适用于小规模 nums.size() <= 20)
长度为 n 的数组有 2^n 个子集,正好对应 0 到 (1 << n) - 1 的二进制表示。第 k 位为 1 表示选第 k 个元素。
优点是代码极简、无递归栈开销;缺点是无法剪枝,且当 n > 20 时 1 << n 可能溢出或耗时爆炸。
- 外层循环
for (int mask = 0; mask < (1 << n); ++mask) - 内层用
if (mask & (1 << i))判断第i位是否为1 - 注意
1 << n是int运算,若n >= 31必须用1LL << n
返回值设计与内存注意事项
返回 vector<vector<int>> 是标准做法,但要注意:如果只是需要遍历而非全部存储,应改用回调函数(function<void(const vector<int>&)>)避免中间结果堆积。尤其当输入较大但只需验证是否存在某个子集时,全量生成就是浪费。
- 不要在递归中频繁
vector<int>(path)构造新对象传参,优先用引用 + 回溯恢复 - 若需去重子集且原数组无序,
sort + unique成本高于一开始就排序 + 跳重 - 编译器对
vector移动语义优化较好,但返回前仍建议用std::move(result)(尤其 C++11 以上)
真正麻烦的往往不是算法逻辑,而是边界——比如空输入、单元素、全重复数组,这些情况要手动跑一遍验证回溯的 result 是否包含空集、是否少解或多解。

















