文章目录
题目
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
方法一 经典回溯法
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtracjing(k,n,1);
return res;
}
public void backtracjing (int k,int n ,int startIndex){
if(path.size()==k){
res.add(new ArrayList<>(path));//必须new一个ArrayList
return ;
}
for(int i = startIndex;i<=n;i++){
path.add(i);
backtracjing(k,n,i+1);
path.remove(path.size()-1);
}
}
}
方法二:递归实现组合型枚举
基本思想
从 n 个当中选 k 个的所有方案对应的枚举是组合型枚举。在「方法一」中我们用递归来实现组合型枚举。
首先我们先回忆一下如何用递归实现二进制枚举(子集枚举),假设我们需要找到一个长度为 n 的序列 a 的所有子序列,代码框架是这样的:
vector<int> temp;
void dfs(int cur, int n) {
if (cur == n + 1) {
// 记录答案
// ...
return;
}
// 考虑选择当前位置
temp.push_back(cur);
dfs(cur + 1, n, k);
temp.pop_back();
// 考虑不选择当前位置
dfs(cur + 1, n, k);
}
vector<int> temp;
void dfs(int cur, int n) {
// 记录合法的答案
if (temp.size() == k) {
ans.push_back(temp);
return;
}
// cur == n + 1 的时候结束递归
if (cur == n + 1) {
return;
}
// 考虑选择当前位置
temp.push_back(cur);
dfs(cur + 1, n, k);
temp.pop_back();
// 考虑不选择当前位置
dfs(cur + 1, n, k);
}
这个时候我们可以做一个剪枝,如果当前 temp 的大小为 s,未确定状态的区间 [cur,n] 的长度为 t,如果 s+t<k,那么即使 ttt 个都被选中,也不可能构造出一个长度为 k 的序列,故这种情况就没有必要继续向下递归,即我们可以在每次递归开始的时候做一次这样的判断:
if (temp.size() + (n - cur + 1) < k) {
return;
}
代码就变成了这样:
vector<int> temp;
void dfs(int cur, int n) {
// 剪枝:temp 长度加上区间 [cur, n] 的长度小于 k,不可能构造出长度为 k 的 temp
if (temp.size() + (n - cur + 1) < k) {
return;
}
// 记录合法的答案
if (temp.size() == k) {
ans.push_back(temp);
return;
}
// cur == n + 1 的时候结束递归
if (cur == n + 1) {
return;
}
// 考虑选择当前位置
temp.push_back(cur);
dfs(cur + 1, n, k);
temp.pop_back();
// 考虑不选择当前位置
dfs(cur + 1, n, k);
}
JAVA代码
class Solution {
List<Integer> temp = new ArrayList<Integer>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
public List<List<Integer>> combine(int n, int k) {
dfs(1, n, k);
return ans;
}
public void dfs(int cur, int n, int k) {
// 剪枝:temp 长度加上区间 [cur, n] 的长度小于 k,不可能构造出长度为 k 的 temp
if (temp.size() + (n - cur + 1) < k) {
return;
}
// 记录合法的答案
if (temp.size() == k) {
ans.add(new ArrayList<Integer>(temp));
return;
}
// 考虑选择当前位置
temp.add(cur);
dfs(cur + 1, n, k);
temp.remove(temp.size() - 1);
// 考虑不选择当前位置
dfs(cur + 1, n, k);
}
}
复杂度![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/925d215f1f80349ad5faa413904eb918.png)
方法三:非递归(字典序法)实现组合型枚举
基本思想
这里的非递归版不是简单的用栈模拟递归转化为非递归:我们希望通过合适的手段,消除递归栈带来的额外空间代价。
假设我们把原序列中被选中的位置记为 1,不被选中的位置记为 0,对于每个方案都可以构造出一个二进制数。我们让原序列从大到小排列(即 {n,n−1,⋯1,0})。我们先看一看 n=4,k=2 的例子:
JAVA代码
class Solution {
List<Integer> temp = new ArrayList<Integer>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
public List<List<Integer>> combine(int n, int k) {
List<Integer> temp = new ArrayList<Integer>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
// 初始化
// 将 temp 中 [0, k - 1] 每个位置 i 设置为 i + 1,即 [0, k - 1] 存 [1, k]
// 末尾加一位 n + 1 作为哨兵
for (int i = 1; i <= k; ++i) {
temp.add(i);
}
temp.add(n + 1);
int j = 0;
while (j < k) {
ans.add(new ArrayList<Integer>(temp.subList(0, k)));
j = 0;
// 寻找第一个 temp[j] + 1 != temp[j + 1] 的位置 t
// 我们需要把 [0, t - 1] 区间内的每个位置重置成 [1, t]
while (j < k && temp.get(j) + 1 == temp.get(j + 1)) {
temp.set(j, j + 1);
++j;
}
// j 是第一个 temp[j] + 1 != temp[j + 1] 的位置
temp.set(j, temp.get(j) + 1);
}
return ans;
}
}