一、题目深度解析与组合约束条件
题目描述
找出所有相加之和为n
的k
个数的组合,且满足以下条件:
- 每个数只能使用一次(范围为1到9)
- 所有数字均为唯一的正整数
- 组合中的数字按升序排列
例如,当k=3
,n=9
时,正确组合为[[1,2,6], [1,3,5], [2,3,4]]
。题目要求返回所有可能的有效组合,且组合不能重复。
核心约束条件分析
与普通组合问题相比,本题增加了两个关键约束:
- 和约束:组合中所有元素的和必须等于
n
- 长度约束:组合的元素个数必须等于
k
这两个约束条件共同决定了回溯过程中的剪枝策略和终止条件,需要在回溯过程中动态维护当前组合的元素和与元素数量。
二、回溯解法的核心实现与逻辑框架
完整回溯代码实现
class Solution {
List<Integer> temp = new LinkedList<>(); // 存储当前组合
List<List<Integer>> res = new ArrayList<>(); // 存储所有结果组合
int sum = 0; // 记录当前组合的元素和
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(k, n, 1, sum); // 从1开始回溯
return res;
}
public void backtracking(int k, int n, int start, int sum) {
// 剪枝条件:和超过n或组合长度超过k
if (sum > n || temp.size() > k) {
return;
}
// 终止条件:和等于n且组合长度等于k
if (sum == n && temp.size() == k) {
res.add(new ArrayList<>(temp)); // 保存当前组合的副本
return;
}
// 核心循环:动态计算循环上界,优化搜索空间
for (int i = start; i <= 9 - (k - temp.size()) + 1; i++) {
sum += i; // 累加当前元素值
temp.add(i); // 选择当前元素
backtracking(k, n, i + 1, sum); // 递归处理下一个元素
sum -= i; // 回溯:撤销元素和累加
temp.removeLast(); // 回溯:移除当前元素
}
return;
}
}
核心设计解析:
-
状态变量维护:
temp
:存储当前正在构建的组合,使用LinkedList
支持高效尾部操作sum
:记录当前组合的元素和,用于快速判断和约束res
:存储所有符合条件的组合
-
剪枝与终止条件:
sum > n
:若当前和已超过目标值,直接剪枝temp.size() > k
:若组合长度已超过k,直接剪枝sum == n && temp.size() == k
:同时满足和约束与长度约束时,保存结果
-
循环边界优化:
i <= 9 - (k - temp.size()) + 1
:动态计算循环上界,确保剩余元素足够选满k个- 例如,当
k=3
且已选1个元素时,剩余需选2个元素,当前可选最大数为9 - 2 + 1 = 8
三、核心问题解析:多条件约束下的回溯逻辑
1. 双重约束条件的处理
和约束的维护:
sum += i; // 选择元素时累加和
backtracking(..., sum); // 递归传递当前和
sum -= i; // 回溯时撤销累加
通过在递归前后动态调整sum
值,确保每次递归调用时都能正确传递当前组合的元素和。
长度约束的维护:
temp.size() > k // 剪枝条件:长度超过k
temp.size() == k // 终止条件:长度等于k
利用temp
列表的长度作为判断依据,结合和约束共同决定递归路径的选择与终止。
2. 循环边界的数学推导
与普通组合问题类似,本题循环上界需满足:
- 设当前已选
t
个元素,还需选m = k - t
个元素 - 当前可选最大数
i
需满足:i + m - 1 <= 9
(因最大数为9) - 推导得:
i <= 9 - m + 1 = 9 - (k - t) + 1
示例说明:
当k=3
,已选1个元素时:
m = 3 - 1 = 2
- 循环上界:
9 - 2 + 1 = 8
- 即当前可选范围为1到8,确保后续能选够2个元素
四、回溯流程深度模拟:以k=3,n=9为例
递归调用树(部分展开):
backtracking(3,9,1,0)
├─ i=1: sum=1, temp=[1]
│ └─ backtracking(3,9,2,1)
│ ├─ i=2: sum=3, temp=[1,2]
│ │ └─ backtracking(3,9,3,3)
│ │ ├─ i=3: sum=6, temp=[1,2,3] → 剪枝(和=6,继续递归)
│ │ ├─ i=4: sum=7, temp=[1,2,4] → 剪枝
│ │ ├─ i=5: sum=8, temp=[1,2,5] → 剪枝
│ │ └─ i=6: sum=9, temp=[1,2,6] → 加入res
│ ├─ i=3: sum=4, temp=[1,3]
│ │ └─ backtracking(3,9,4,4)
│ │ └─ i=5: sum=9, temp=[1,3,5] → 加入res
│ └─ i=4: sum=5, temp=[1,4]
│ └─ backtracking(3,9,5,5)
│ └─ i=5: sum=10 → 剪枝(和>9)
├─ i=2: sum=2, temp=[2]
│ └─ backtracking(3,9,3,2)
│ ├─ i=3: sum=5, temp=[2,3]
│ │ └─ backtracking(3,9,4,5)
│ │ └─ i=4: sum=9, temp=[2,3,4] → 加入res
│ └─ i=4: sum=6, temp=[2,4] → 后续递归均剪枝
└─ i=3: sum=3, temp=[3] → 后续递归均剪枝
详细步骤:
-
初始调用:
backtracking(3,9,1,0)
- 从1开始选择,初始和为0
-
选择1:
sum=1
,temp=[1]
- 递归选择下一个数(从2开始)
-
选择2:
sum=3
,temp=[1,2]
- 递归选择下一个数(从3开始)
- 选择6时,
sum=9
,temp=[1,2,6]
→ 满足条件,加入结果集
-
回退到选择3:
sum=4
,temp=[1,3]
- 递归选择5,
sum=9
,temp=[1,3,5]
→ 加入结果集
-
回退到选择2:
- 选择3,再选择4,
sum=9
,temp=[2,3,4]
→ 加入结果集
- 选择3,再选择4,
-
继续回退与尝试:
- 其他路径因和超过9或无法选满3个数而被剪枝
五、算法复杂度分析
1. 时间复杂度
- O(C(9,k)×k):
- 组合数:C(9,k)为最终结果数量(从9个数中选k个的组合数)
- 每个组合需要O(k)时间复制到结果集
- 优化后的循环边界减少了无效搜索,但最坏情况下仍需遍历所有可能组合
2. 空间复杂度
- O(k):递归栈深度最大为k(每层递归代表一个数字选择)
temp
列表长度最多为k,res
空间为O(C(9,k)×k)
六、核心技术点总结:多约束回溯的关键要素
1. 多状态变量维护
- 元素和:通过
sum
变量动态维护,确保快速判断和约束 - 组合长度:通过
temp.size()
动态获取,确保满足长度约束 - 元素唯一性:通过
start
参数控制选择范围,确保元素不重复
2. 双重剪枝策略
- 和剪枝:当
sum > n
时提前终止递归 - 长度剪枝:当
temp.size() > k
时提前终止递归
3. 循环边界优化
- 数学推导动态上界,确保剩余元素足够选满k个
- 公式:
i <= 9 - (k - temp.size()) + 1
七、常见误区与优化建议
1. 状态变量未回溯
- 误区:忘记在递归后撤销
sum
的累加sum += i; backtracking(...); // 缺少 sum -= i; 导致状态未回退
- 正确做法:必须在递归调用后撤销状态变更,确保每次选择的独立性
2. 循环边界错误
- 误区:使用固定上界或错误的动态上界
for (int i = start; i <= 9; i++) { ... } // 未优化上界,导致无效搜索
- 正确做法:使用
i <= 9 - (k - temp.size()) + 1
动态计算上界
3. 优化建议:位运算实现
// 位运算解法(仅作示意)
List<List<Integer>> res = new ArrayList<>();
for (int mask = 0; mask < (1 << 9); mask++) {
if (Integer.bitCount(mask) == k) {
List<Integer> combo = new ArrayList<>();
int sum = 0;
for (int i = 0; i < 9; i++) {
if ((mask & (1 << i)) != 0) {
combo.add(i + 1);
sum += i + 1;
}
}
if (sum == n) {
res.add(combo);
}
}
}
- 优势:代码更简洁,无需递归
- 劣势:时间复杂度为O(2^9),对大规模问题不适用
八、总结:多约束回溯的状态管理之道
本算法通过回溯法在双重约束条件下系统地枚举所有可能组合,核心在于:
- 状态变量同步维护:同时跟踪元素和、组合长度与元素选择
- 双重剪枝优化:利用和约束与长度约束提前终止无效路径
- 动态边界计算:通过数学推导减少搜索空间
理解多约束回溯问题的关键在于把握各状态变量间的联动关系,以及如何通过剪枝策略和循环边界优化提升算法效率。这种方法不仅适用于组合总和问题,还可扩展到其他多约束条件下的组合优化问题。