77. 组合
题目来源
题目分析
给定两个整数 n
和 k
,要求从 1
到 n
中选择 k
个数,并返回所有可能的组合。这实际上是经典的组合数学问题,可以通过回溯算法来解决。
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
题目难度
- 难度:中等
题目标签
- 标签:数组, 回溯
题目限制
1 <= n <= 20
1 <= k <= n
解题思路
要解决这个问题,我们可以使用回溯算法。回溯的基本思想是从 1
到 n
中逐个选择数字,每次选择后进入下一层递归,直到选满 k
个数字为止。为了提高效率,我们可以在递归过程中剪枝,避免无效的递归。
核心算法步骤
-
无剪枝的回溯:
- 从当前数字开始逐个选择,并递归选择剩余的数字。
- 当选满
k
个数字后,将当前组合添加到结果集中。
-
剪枝的回溯:
- 在递归过程中,通过计算剩余数字是否足够选择,决定是否继续递归,避免不必要的计算。
- 通过从大到小枚举剩余数字,保证剩下的数字足够填满剩余的选择,减少搜索空间。
代码实现
以下是生成组合的Java代码:
/**
* 77. 组合
* @param n 输入的整数n
* @param k 输入的整数k
* @return ans 所有可能的组合
*/
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> path = new ArrayList<>();
if (n < k) {
return ans;
}
combine(k, n, path, ans);
return ans;
}
// 回溯生成组合
private void combine(int k, int i, List<Integer> path, List<List<Integer>> ans) {
//无剪枝
// if(path.size() == k){
// ans.add(new ArrayList<>(path));
// return;
// }
// for (int j = i; j <= n; j++) {
// path.add(j);
// combine(n, k, j + 1, path, ans);
// path.remove(path.size() - 1);
// }
// 剪枝
int d = k - path.size(); // 还需选择d个数
if (d == 0) {
ans.add(new ArrayList<>(path));
return;
}
// 从大到小枚举,剪枝条件为剩余数不足以填满
for (int j = i; j >= d; j--) {
path.add(j);
combine(k, j - 1, path, ans);
path.remove(path.size() - 1);
}
}
代码解读
-
回溯逻辑:
- 递归调用
combine
函数时,我们在当前路径path
中添加一个新的数字,并继续递归处理下一个数字。 - 一旦路径长度达到了
k
,即已经选择了k
个数字,则将当前路径添加到结果集中。
- 递归调用
-
剪枝逻辑:
- 剪枝的关键在于通过计算
d
,判断剩余数字是否足够填满所需的选择。 - 如果剩余数字不够,则立即终止当前分支的递归,节省时间。
- 剪枝的关键在于通过计算
性能分析
- 时间复杂度:
O(k*C(n, k))
,即n
中选k
个数的组合数乘以搜索树的路径长度k
。回溯算法会遍历每一个可能的组合。 - 空间复杂度:
O(k)
,递归调用栈的最大深度为k
。
测试用例
你可以使用以下测试用例来验证代码的正确性:
int n = 4, k = 2;
List<List<Integer>> result = combine(n, k);
System.out.println(result);
// 输出: [[2, 1], [3, 1], [3, 2], [4, 1], [4, 2], [4, 3]]
扩展讨论
优化写法
可以通过预先计算组合数的方式,减少递归的深度,使算法更加高效。
其他实现
除了回溯法,还可以通过动态规划或字典序生成法来求解组合问题。
总结
这道题目通过回溯算法帮助我们理解了如何生成所有可能的组合。通过剪枝,可以有效减少搜索空间,提高算法的效率。这种方法在解决组合数学问题时非常常见且有效。