组合问题(Combinations)

📌 定义

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

示例:
输入: n = 4, k = 2
输出:
[
  [1,2],
  [1,3],
  [1,4],
  [2,3],
  [2,4],
  [3,4]
]

输入: n = 1, k = 1
输出: [[1]]

核心思路

使用回溯算法,与全排列不同:

  • 组合不考虑顺序:[1,2] 和 [2,1] 是同一个组合
  • 使用start_index避免重复:只选择当前位置之后的元素
决策树示例 n=4, k=2:

                    []
         /     |      |      \
       [1]   [2]    [3]    [4]
      / | \   |  \    |
   [1,2][1,3][1,4][2,3][2,4][3,4]

只向右展开,避免重复

复杂度分析

方法时间复杂度空间复杂度说明
回溯法O(C(n,k) × k)O(k)C(n,k)种组合
迭代法O(C(n,k) × k)O(C(n,k) × k)需要存储所有结果
二进制枚举O(2^n × k)O(k)适用于小n

Go 代码

Go 实现

package main
 
import (
    "fmt"
    "sort"
)
 
// 基本组合
func combine(n int, k int) [][]int {
    result := [][]int{}
    path := []int{}
 
    var backtrack func(start int)
    backtrack = func(start int) {
        // 终止条件
        if len(path) == k {
            temp := make([]int, len(path))
            copy(temp, path)
            result = append(result, temp)
            return
        }
 
        // 剪枝优化
        for i := start; i <= n-(k-len(path))+1; i++ {
            path = append(path, i)
            backtrack(i + 1)
            path = path[:len(path)-1]
        }
    }
 
    backtrack(1)
    return result
}
 
// 组合总和
func combinationSum(candidates []int, target int) [][]int {
    result := [][]int{}
    path := []int{}
    sort.Ints(candidates)
 
    var backtrack func(start, currentSum int)
    backtrack = func(start, currentSum int) {
        if currentSum == target {
            temp := make([]int, len(path))
            copy(temp, path)
            result = append(result, temp)
            return
        }
 
        if currentSum > target {
            return
        }
 
        for i := start; i < len(candidates); i++ {
            if currentSum+candidates[i] > target {
                break
            }
 
            path = append(path, candidates[i])
            backtrack(i, currentSum+candidates[i])
            path = path[:len(path)-1]
        }
    }
 
    backtrack(0, 0)
    return result
}
 
func main() {
    // 测试基本组合
    result := combine(4, 2)
 
    fmt.Println("组合结果:")
    for _, comb := range result {
        fmt.Println(comb)
    }
}

思路展开

组合 vs 排列

排列:[1,2] 和 [2,1] 是不同的
组合:[1,2] 和 [2,1] 是相同的

避免重复的方法:
- 使用start_index,只向后选择
- 排列需要visited数组,组合不需要

示例 n=3, k=2:
排列会产生: [1,2], [1,3], [2,1], [2,3], [3,1], [3,2]
组合只产生: [1,2], [1,3], [2,3]

剪枝优化详解

n=4, k=2,需要选2个数

在i=3时:
- path = [3]
- 还需要选 k - len(path) = 2 - 1 = 1 个数
- 剩余元素:[4],共 n - i = 4 - 3 = 1 个
- 1 >= 1 ✓ 可以继续

在i=4时:
- path = []
- 还需要选 k - len(path) = 2 - 0 = 2 个数
- 剩余元素:[],共 n - i = 4 - 4 = 0 个
- 0 < 2 ✗ 无法继续,剪枝

剪枝条件:
需要:n - i + 1 >= k - len(path)
即:i <= n - (k - len(path)) + 1

组合总和去重详解

candidates = [1, 1, 2], target = 3

排序后:[1, 1, 2]

树形结构:
                    []
         /          |          \
       [1]        [1]×         [2]
      /  \        /  \           |
   [1,1][1,2]  [1,1]×[1,2]×   [2,1]
     |    |      |    |
  [1,1,1][1,1,2] ...  ...

× 标记的是被剪枝的分支

去重条件(树层去重):
if i > start and candidates[i] == candidates[i-1]:
    continue

解释:
- i > start: 不是当前层的第一个元素
- candidates[i] == candidates[i-1]: 与前一个相同
→ 在同一层会产生重复,需要剪枝

经典题目

LeetCode 问题

扩展问题

  • 大小为k且和为s的所有组合
  • 组合的字典序第k个

⚖️ 优缺点

优点

  • ✅ 避免重复:使用start_index自然去重
  • ✅ 易于剪枝:多种剪枝条件
  • ✅ 空间高效:不需要visited数组

缺点

  • ❌ 指数时间复杂度:C(n,k)可能很大
  • ❌ 不适合大规模:n > 30时性能差

🎨 应用场景

💡 优化技巧

相关主题


返回:回溯算法 | 算法学习导航