LeetCode 第216题:组合总和 III
题目描述
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字最多使用一次
返回所有可能的有效组合的列表。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
难度
中等
题目链接
示例
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
解释: 1 + 2 + 4 = 7
没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
示例 3:
输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
提示
2 <= k <= 9
1 <= n <= 60
解题思路
这是一个组合问题,可以使用回溯算法解决。回溯算法是一种通过探索所有可能的候选解来找出所有解的算法,如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化来舍弃该解,即回溯并且尝试另一种可能。
针对本题,我们有以下解题思路:
方法一:回溯法(Backtracking)
回溯法是解决这类组合问题的标准方法,我们通过以下步骤解决:
- 定义一个回溯函数,参数包括:当前组合、目标和n的剩余值、已使用的数字个数k、起始数字
- 在回溯函数中,我们有三种情况:
- 如果k = 0且n = 0,说明找到了一个有效组合,将其加入结果集
- 如果k = 0或n < 0,说明当前组合不满足条件,直接返回
- 否则,从起始数字开始尝试每个可能的数字(范围是1-9)
- 对于每个尝试的数字i,我们将其加入当前组合,递归调用回溯函数(参数为:当前组合、n-i、k-1、i+1)
- 回溯时,将最后加入的数字从组合中移除,尝试下一个数字
这种方法会遍历所有可能的组合,并筛选出满足条件的组合。
方法二:位运算(Bit Manipulation)
由于本题的数字范围非常有限(仅使用1-9),我们也可以使用位运算来模拟组合的生成:
- 使用一个9位的二进制数表示选择状态,第i位为1表示选择了数字i+1
- 枚举所有可能的选择状态(最多有2^9种)
- 对于每种状态,检查是否恰好选择了k个数字,且它们的和为n
- 如果满足条件,将该组合加入结果集
这种方法的时间复杂度为O(2^9),对于本题的规模来说是可接受的。
代码实现
C# 实现
public class Solution {
// 回溯法实现
public IList<IList<int>> CombinationSum3(int k, int n) {
List<IList<int>> result = new List<IList<int>>();
Backtrack(result, new List<int>(), k, n, 1);
return result;
}
private void Backtrack(List<IList<int>> result, List<int> current, int k, int remain, int start) {
// 如果已经选择了k个数字且剩余和为0,找到一个有效组合
if (k == 0 && remain == 0) {
result.Add(new List<int>(current));
return;
}
// 提前剪枝:如果k小于0或remain小于0,无法构成有效组合
if (k <= 0 || remain <= 0) {
return;
}
// 从start到9尝试每个数字
for (int i = start; i <= 9; i++) {
// 尝试选择当前数字
current.Add(i);
// 递归寻找下一个数字
Backtrack(result, current, k - 1, remain - i, i + 1);
// 回溯,移除当前数字,尝试其他可能
current.RemoveAt(current.Count - 1);
}
}
// 位运算实现
public IList<IList<int>> CombinationSum3BitManipulation(int k, int n) {
List<IList<int>> result = new List<IList<int>>();
// 遍历所有可能的组合(使用位掩码表示)
for (int mask = 0; mask < (1 << 9); mask++) {
// 检查是否恰好选择了k个数字
if (CountBits(mask) == k) {
// 计算所选数字的和
int sum = 0;
List<int> combination = new List<int>();
for (int i = 0; i < 9; i++) {
if ((mask & (1 << i)) != 0) {
combination.Add(i + 1);
sum += (i + 1);
}
}
// 检查和是否等于n
if (sum == n) {
result.Add(combination);
}
}
}
return result;
}
// 计算二进制数中1的个数
private int CountBits(int n) {
int count = 0;
while (n > 0) {
count += n & 1;
n >>= 1;
}
return count;
}
}
Python 实现
class Solution:
# 回溯法实现
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
result = []
self.backtrack(result, [], k, n, 1)
return result
def backtrack(self, result, current, k, remain, start):
# 如果已经选择了k个数字且剩余和为0,找到一个有效组合
if k == 0 and remain == 0:
result.append(current[:])
return
# 提前剪枝:如果k小于0或remain小于0,无法构成有效组合
if k <= 0 or remain <= 0:
return
# 从start到9尝试每个数字
for i in range(start, 10):
# 尝试选择当前数字
current.append(i)
# 递归寻找下一个数字
self.backtrack(result, current, k - 1, remain - i, i + 1)
# 回溯,移除当前数字,尝试其他可能
current.pop()
# 位运算实现
def combinationSum3BitManipulation(self, k: int, n: int) -> List[List[int]]:
result = []
# 遍历所有可能的组合(使用位掩码表示)
for mask in range(1 << 9):
# 检查是否恰好选择了k个数字
if bin(mask).count('1') == k:
# 计算所选数字的和
combination = []
sum_val = 0
for i in range(9):
if mask & (1 << i):
combination.append(i + 1)
sum_val += (i + 1)
# 检查和是否等于n
if sum_val == n:
result.append(combination)
return result
C++ 实现
class Solution {
public:
// 回溯法实现
vector<vector<int>> combinationSum3(int k, int n) {
vector<vector<int>> result;
vector<int> current;
backtrack(result, current, k, n, 1);
return result;
}
private:
void backtrack(vector<vector<int>>& result, vector<int>& current, int k, int remain, int start) {
// 如果已经选择了k个数字且剩余和为0,找到一个有效组合
if (k == 0 && remain == 0) {
result.push_back(current);
return;
}
// 提前剪枝:如果k小于0或remain小于0,无法构成有效组合
if (k <= 0 || remain <= 0) {
return;
}
// 从start到9尝试每个数字
for (int i = start; i <= 9; i++) {
// 尝试选择当前数字
current.push_back(i);
// 递归寻找下一个数字
backtrack(result, current, k - 1, remain - i, i + 1);
// 回溯,移除当前数字,尝试其他可能
current.pop_back();
}
}
public:
// 位运算实现
vector<vector<int>> combinationSum3BitManipulation(int k, int n) {
vector<vector<int>> result;
// 遍历所有可能的组合(使用位掩码表示)
for (int mask = 0; mask < (1 << 9); mask++) {
// 检查是否恰好选择了k个数字
if (__builtin_popcount(mask) == k) {
// 计算所选数字的和
int sum = 0;
vector<int> combination;
for (int i = 0; i < 9; i++) {
if (mask & (1 << i)) {
combination.push_back(i + 1);
sum += (i + 1);
}
}
// 检查和是否等于n
if (sum == n) {
result.push_back(combination);
}
}
}
return result;
}
};
性能分析
各语言实现的性能对比:
实现语言 | 执行用时 | 内存消耗 | 特点 |
---|---|---|---|
C# | 96 ms | 25.8 MB | 回溯法实现简洁,位运算实现较为高效 |
Python | 36 ms | 13.9 MB | Python的列表操作方便,适合回溯算法 |
C++ | 0 ms | 6.7 MB | 性能最优,尤其是位运算实现非常高效 |
补充说明
代码亮点
- 回溯法中使用了剪枝条件(k <= 0 || remain <= 0),提前结束不可能的分支,优化了时间复杂度
- 位运算实现利用了二进制表示选择状态,使得代码更简洁
- C++实现中使用了内置函数
__builtin_popcount
计算位数,简化了代码 - 对于每种实现,我们都确保了不会产生重复组合,这是因为我们每次选择数字时都从前一个数字的下一个开始
优化方向
- 可以增加更多的剪枝条件,例如当剩余的数字不足以组成k个数或剩余和太大无法达成时,可以提前返回
- 在回溯过程中,可以先排序可选数字,以便更好地剪枝
- 对于位运算实现,可以进一步优化遍历范围,只考虑含有k个1的位掩码
解题难点
- 理解回溯算法的核心思想和实现细节
- 正确处理组合条件,确保每个数字最多使用一次
- 避免生成重复组合
- 有效剪枝以提高算法效率
常见错误
- 忘记检查选择的数字个数是否为k
- 忘记回溯(移除当前尝试的数字)
- 不正确的递归终止条件
- 在生成组合时没有避免重复
相关题目
- 39. 组合总和 - 允许重复使用元素的组合总和问题
- 40. 组合总和 II - 每个数字只能使用一次的组合总和问题
- 77. 组合 - 从1到n中选择k个数的所有组合
- 90. 子集 II - 包含重复元素的数组的所有子集