77. 组合

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

你可以按 任何顺序 返回答案。

示例 1:

1
2
3
4
5
6
7
8
9
10
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

示例 2:

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

提示:

  • 1 <= n <= 20
  • 1 <= k <= n

思路:

回溯算法

回溯法解决的问题都可以抽象为树形结构来理解,如下图

77.组合

其中n(集合长度)相当于树的宽度,k(组合子集的长度)相当于树的深度。

每次搜索到叶子结点,就找到了一个子集结果,然后只要把所有的从根节点搜索到叶子结点的路径收集起来,就可以求出结果集。

回溯三部曲:

  1. 递归函数的返回值以及参数
    1. 返回值,一般递归函数的返回值都为空
    2. 参数:
      1. n:集合中元素个数,遍历宽度(for循环宽度)
      2. k:组合子集中元素个数,遍历深度(递归深度)
      3. startIndex:用于记录下一层递归搜索的起始位置
  2. 回溯终止条件
    1. 从根节点到达叶子节点即结束本层递归
    2. 也就是说path数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径
  3. 单层搜索逻辑
    1. for循环每次从startIndex开始遍历,然后用path保存取到的节点i
    2. 然后通过递归函数不断调用自己 一直往深处遍历,直到遇到了叶子节点再返回。
    3. 回溯操作,比如这次的遍历结果是{1,2},这个结果已经保存在结果集res中了,但是我们还要遍历{1,3},{1,4},所以需要进行回溯,把2剔除,然后3,4才能进去。

回溯算法模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}

for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
// 递归
backtracking(路径,选择列表);
回溯,撤销处理结果
}
}

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//单条路径
public LinkedList<Integer> path = new LinkedList<>();
//路径结果集
public List<List<Integer>> res = new ArrayList<>();

public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return res;
}

public void backtracking(int n, int k, int startIndex) {
//******************************************************//
//回溯终止条件,单条路径大小达到k,说明找到了一个子集大小为k的组合
if (path.size() == k) {
res.add(new ArrayList<>(path));
return;
}

//******************************************************//

//单层搜索过程,从startIndex开始遍历
for (int i = startIndex; i <= n; i++) {
path.add(i);
backtracking(n, k, i + 1);
//回溯,撤销当前处理的节点
path.removeLast();
}
}

剪枝:

来举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了,因为从元素2到元素4一共才三个元素,构不成一个4位的集合。 在第二层for循环,从元素3开始的遍历都没有意义了。

如图:

77.组合4

也就是说:

如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了

优化过程:
  1. 已经选择了的元素个数:path.size();
  2. 还需要选择的元素个数:k - path.size();
  3. 从集合n和还需要的元素个数(k - path.size)反推startIndex,可得在集合n中至多要从该起始位置 : n - (k - path.size()) + 1的地方开始遍历。

IMG_4306

代码优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//单条路径
public LinkedList<Integer> path = new LinkedList<>();
//路径结果集
public List<List<Integer>> res = new ArrayList<>();

public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return res;
}

public void backtracking(int n, int k, int startIndex) {
//******************************************************//
//回溯终止条件,单条路径大小达到k,说明找到了一个子集大小为k的组合
if (path.size() == k) {
res.add(new ArrayList<>(path));
return;
}

//******************************************************//

//单层搜索过程,从startIndex开始遍历
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
path.add(i);
backtracking(n, k, i + 1);
//回溯,撤销当前处理的节点
path.removeLast();
}
}

参考自代码随想录:https://programmercarl.com/0077.%E7%BB%84%E5%90%88.html#%E5%89%AA%E6%9E%9D%E4%BC%98%E5%8C%96