回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
那么既然回溯法并不高效为什么还要用它呢?
因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。
回溯法解决的问题都可以抽象为树形结构,是的,我指的是所有回溯法的问题都可以抽象为树形结构!
因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度构成的树的深度。
递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。
回溯模板:
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],
]
难点:
剪枝
思路:
时间复杂度:O()
空间复杂度:O()
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
public void backtracking(int n, int k, int startIdx) {
if (n < 1 || n < k) return;
if (path.size() == k) {
result.add(new ArrayList<>(path));
// result.add(path); 这是错误的!
return;
}
for (int i = startIdx; i <= n; i++) {
path.add(i);
backtracking(n, k, i+1);
path.remove(path.size()-1);
}
}
}
剪枝:
for (int i = startIdx; i <= n-(k-path.size())+1; i++) {
path.add(i);
backtracking(n, k, i+1);
path.remove(path.size()-1);
}
时长:
15min
收获:
注意了!向列表中添加对象元素时,要考虑这个对象被添加后是否不能改变了。如果不能改变,必须要重新new一个对象,而不是指向原来的对象,否则结果集中的结果会产生改变。