题目
Problem: 77. 组合
给定两个整数 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]]
前知
回溯
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯,但是有回溯不一定存在递归。回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案
回溯法,一般可以解决如下几种问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
回溯法模版
回溯三部曲
- 回溯函数模板返回值以及参数
返回值通常为void,参数则根据逻辑需要什么参数就填什么参数,先写逻辑 - 回溯函数终止条件
当到达终止条件时,就把满足条件的答案存起来,退出递归 - 回溯搜索的遍历过程
先进行for循环对本层集合进行横向遍历,再递归纵向遍历,最后回溯,返回上一步
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
题解
一、思路
组合问题用回溯来解决,把该回溯问题抽象成一棵树,for循环为横向遍历,递归则是纵向遍历,n相当于树的宽度,k相当于树的深度,最后在树中每找到一个叶子结点时,就找到了一个结果,把结果添加到结果集中
二、解题方法
回溯三部曲
- 递归函数的返回值以及参数:
首先创建两个全局变量存放每条的结果path以及结果集result,以及n和k,还需要一个startIndex整数参数,用来记录开始遍历的位置,例如startIndex从1开始,下一层只需要取startIndex+1开始的数字:2,3…n,startIndex防止出现重复的组合,下一层就不会再重新遍历1了,所以需要startIndex来记录下一层递归,搜索的起始位置。
public void backtracking(int n,int k,int startIndex)
- 回溯函数终止条件:
当path集合的大小到达k时,说明遍历到了叶子结点,path里存的就是根结点到叶子结点的路径,将此处的path保存到result结果集里,终止本层递归
if (path.size() == k){
result.add(new ArrayList<>(path));
return;
}
- 单层搜索的过程:
for循环每次从startIndex开始遍历,当startIndex超过横向遍历的最大n时,则横向遍历完成,每次循环用path保存遍历到的节点i,向下一层递归startIndex+1开始的数字,递归完成后,回溯到上一层,撤销处理过的结点。再for循环横向遍历该层其它结点,重复过程
for (int i = startIndex;i<=n;i++){
path.add(i);
backtracking(n,k,i+1);
path.removeLast();
}
剪枝优化:横向for循环遍历的时候,可以缩小循环的范围,如果for循环选择的起始位置之后的元素个数已经不足我们需要的元素个数了,那么就没有必要搜索了。
例如以下图片所示情况n=4,k=4的一个组合,当startIndex为1后,再向下一层遍历之前,只有3个数字可以选了分别是2,3,4,刚好满足剩下的3个数字组合,而当startIndex为2甚至是3,4时,剩下可以挑选的数字只剩下2,1,0个了,还没遍历到叶子结点前,就已经没有3个数字可供遍历了,此时可以直接剪去这几种情况
优化过程如下:
- 已经遍历过的数字个数
path.size()
- 还需要遍历的数字个数
k - path.size()
- 在集合n中至多要从该起始位置
n - (k - path.size()) + 1
开始遍历,例如:n=4,k=4时,当还没有遍历数字之前,path为0,4-(4-0)+1=1,从1位置开始遍历都是合理的,而2,3,4就不行。
//未优化之前
for (int i = startIndex; i <= n; i++)
//优化之后
for (int i = startIndex;i<=n - (k - path.size()) + 1;i++)
三、Code
class Solution {
List<List<Integer>> result= new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n,k,1);
return result;
}
/**
* 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex
* @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。
*/
public void backtracking(int n,int k,int startIndex){
if (path.size() == k){
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex;i<=n - (k - path.size()) + 1;i++){
path.add(i);
backtracking(n,k,i+1);
path.removeLast();
}
}
}
总结
以上就是针对这道题的刷题笔记,讲解了回溯算法的使用方法,回溯法解决的问题都可以抽象为树形结构,因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度,递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)