算法题 组合总和 III

216. 组合总和 III

问题描述

找出所有相加之和为 nk 个数的组合,且满足:

  • 只使用数字 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,没有有效的组合。

算法思路

回溯法(DFS)

  1. 从数字 1 开始遍历到 9
  2. 将当前数字加入组合
  3. 递归处理后续数字(从当前数字+1开始)
  4. 回溯时移除最后添加的数字
  5. 当组合大小等于 k 且和为 n 时保存结果

剪枝优化

  1. 和剪枝:当当前和超过 n 时终止当前分支
  2. 数量剪枝:当剩余数字数量不足以填满组合时提前终止
    • 剩余数字数量 = 9 - i + 1
    • 还需数字数量 = k - path.size()
    • 剪枝条件:9 - i + 1 < k - path.size()

代码实现

class Solution {
    public List<List<Integer>> combinationSum3(int k, int n) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(k, n, 1, 0, new ArrayList<>(), result);
        return result;
    }
    
    /**
     * 回溯核心方法
     * 
     * @param k      组合大小
     * @param n      目标和
     * @param start  当前起始数字
     * @param sum    当前组合和
     * @param path   当前组合
     * @param result 结果集
     */
    private void backtrack(int k, int n, int start, int sum,
                          List<Integer> path, List<List<Integer>> result) {
        // 终止条件1:组合大小等于k
        if (path.size() == k) {
            // 检查组合和是否等于n
            if (sum == n) {
                result.add(new ArrayList<>(path));
            }
            return;
        }
        
        // 遍历可选择的数字(从start到9)
        for (int i = start; i <= 9; i++) {
            // 剪枝1:当前和超过目标值
            if (sum + i > n) {
                break;
            }
            
            // 剪枝2:剩余数字不足
            if (9 - i + 1 < k - path.size()) {
                break;
            }
            
            // 选择当前数字
            path.add(i);
            
            // 递归处理后续数字
            backtrack(k, n, i + 1, sum + i, path, result);
            
            // 回溯:移除最后添加的数字
            path.remove(path.size() - 1);
        }
    }
}

算法分析

  • 时间复杂度:O(C(9,k) × k)
    • 组合数 C(9,k) = 9!/(k!(9-k)!)
    • 每个组合需要 O(k) 时间复制到结果
  • 空间复杂度:O(k)
    • 递归栈深度 O(k)
    • 路径列表 O(k)

算法过程

k=3, n=7

开始:start=1, sum=0, path=[]
├─ i=1: path=[1], sum=1
│  ├─ i=2: path=[1,2], sum=3
│  │  ├─ i=3: 1+2+3=6 <7 → 组合大小等于k→ 回溯到第2层
│  │  ├─ i=4: 1+2+4=7 → 保存[1,2,4]
│  │  ├─ i=5: 1+2+5=8 >7 → 剪枝
│  └─ i=3: path=[1,3], sum=4
│     ├─ i=4: 1+3+4=8 >7 → 剪枝
└─ i=2: path=[2], sum=2
   └─ i=3: path=[2,3], sum=5
      └─ i=4: 2+3+4=9 >7 → 剪枝
最终结果:[[1,2,4]]

关键点

  1. 起始位置:每次递归从 i+1 开始,避免重复使用数字
  2. 双重剪枝
    • 和剪枝sum + i > n
    • 数量剪枝9 - i + 1 < k - path.size()
  3. 结果保存:当 path.size()==ksum==n 时保存
  4. 回溯操作:递归后移除最后添加的数字

剪枝原理

数量剪枝(k=3, path.size()=1):

  • 还需数字数量 = 3 - 1 = 2
  • 剩余数字数量 = 9 - 2 + 1 = 8(从2开始:2,3,…,9)
  • 实际遍历范围:i 从 2 到 8
    • i=8:剩余数字[9](1个 < 2)→ 剪枝
    • i=9:剩余数字[](0个 < 2)→ 剪枝

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1:标准示例
    System.out.println("Test 1 (k=3,n=7): " + solution.combinationSum3(3, 7));
    // 预期:[[1,2,4]]
    
    // 测试用例2:多解情况
    System.out.println("Test 2 (k=3,n=9): " + solution.combinationSum3(3, 9));
    // 预期:[[1,2,6],[1,3,5],[2,3,4]]
    
    // 测试用例3:无解情况
    System.out.println("Test 3 (k=4,n=1): " + solution.combinationSum3(4, 1)); // []
    
    // 测试用例4:边界值
    System.out.println("Test 4 (k=1,n=5): " + solution.combinationSum3(1, 5)); // [[5]]
    
    // 测试用例5:最大组合
    System.out.println("Test 5 (k=9,n=45): " + solution.combinationSum3(9, 45));
    // 预期:[[1,2,3,4,5,6,7,8,9]]
    
    // 测试用例6:超出范围
    System.out.println("Test 6 (k=2,n=18): " + solution.combinationSum3(2, 18)); // []
}

常见问题

  1. 为什么需要 start 参数?
    避免组合重复(如 [1,2] 和 [2,1]),保证组合内数字升序排列。

  2. 剪枝条件如何推导?

    • 和剪枝:sum + i > n
    • 数量剪枝:剩余数字数量 9-i+1 必须 ≥ 还需数字数量 k-path.size()
  3. 为什么数量剪枝放在循环内?
    因为 path.size() 在循环中动态变化,需在每次迭代时重新计算。

  4. 能否使用动态规划?
    可以,但回溯法更直观且空间效率更高,因为需要输出所有组合而非计数

  5. 大数情况下是否会栈溢出?
    k 最大为 9,递归深度最多 9 层,不会栈溢出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值