理论基础
- 组合问题:N个数里面按一定规则找出k个数的集合(不强调顺序)
- 切割问题:一个字符串按一定规则,有几种切割方式
- 子集问题:一个N个数的集合里,有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式(强调顺序)
- 棋盘问题:N皇后,解数独等
回溯法模板
- 回溯函数模板返回值以及参数
返回值一般为void
参数的确定比较困难,一般是需要什么参数,就填什么参数
- 回溯函数终止条件
回溯函数一般用在树的结构上,当遍历到树的叶子节点,就找到了满足条件的一条答案,把这个答案存放起来,并结束本层循环。
- 回溯搜索的遍历过程
回溯一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度,for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多久。
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
第77题. 组合
给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
解析:
可以按照上面的回溯方式,照猫画虎。首先,要知道这是个组合问题,那么就是没有顺序的,即[1,2]和[2,1]算是一种组合,而不是两种。
按照模板套的话即是如下的代码:
class Solution {
List<Integer> list = new ArrayList<>();
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backTracking(n,k,1);
return result;
}
void backTracking(int n, int k, int startIndex){
if(list.size() == k){
result.add(new ArrayList<>(list));
return ;
}
for(int i = startIndex; i <= n; i++){
list.add(i);
backTracking(n,k,i+1);
list.remove(list.size()-1);
}
}
}
优化:
剪枝操作,有点深,我是看了卡尔哥的视频,再结合底下“独舞鸾”的解析,才明白剪枝操作如何找边界。
首先,list.size(): 表示已经找到的个数
k-list.size(): 表示还需要找的个数
则[startIndex , n] 的数组长度起码应该是k - list.size() 才有可能继续搜索,则存在n - startIndex + 1 = k - list.size() , 则startIndex = n + 1 - (k-list.size()) , 也就是说startIndex 不能超过这个值。
class Solution {
List<Integer> list = new ArrayList<>();
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backTracking(n,k,1);
return result;
}
void backTracking(int n, int k, int startIndex){
if(list.size() == k){
result.add(new ArrayList<>(list));
return ;
}
for(int i = startIndex; i <= n+1-k+list.size(); i++){
list.add(i);
backTracking(n,k,i+1);
list.remove(list.size()-1);
}
}
}