回溯是递归的副产品,只要有递归,就会有对应的回溯过程。
回溯实际上就是“撤销上一次递归操作”的一个过程。
回溯法是由递归+循环组成的,其中每次循环执行的次数应该是可知的。
每一次完成递归都会收集一次可能的结果,因此结果集的大小是不确定的,需要使用递归去找,我们称之为纵向搜索;
而每次循环会从待找集合中依次遍历,是一个横向搜索的过程。
void backtracking(参数){
if(终止条件){
收集结果
return;
}
//单层搜索,横向遍历
for(集合){
处理节点;
//纵向遍历
backtracking();
回溯(撤销)
}
}
每一个回溯算法都可以抽象为N叉树。画图可以更清晰的理解算法过程。
lc77.
给定两个整数 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]
]
提示:
1 <= n <= 20
1 <= k <= n
void
。本题中,待遍历集合则为[1,...,n]
,由于它是一个连续的自然数列,我们传入n即可,通过for循环就可以遍历了。同时,还需要传入组合大小k。List
。public static List<List<Integer>> combine(int n, int k) {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
backtrackingCombine(path,result,n,k,1);
return result;
}
static void backtrackingCombine(LinkedList<Integer> path,List<List<Integer>> result,int n,int k,int startIndex){
//终止条件
if(path.size()==k){
//收集结果
result.add(new LinkedList<>(path));
return;
}
for(int i = startIndex;i<=n;i++){
path.add(i);//处理结点
backtrackingCombine(path,result,n,k,i+1);//递归到下一层
path.removeLast();//回溯,撤销处理
}
}
本题中需要关注的点有:
remove(path.size()-1)
的方式删除,并且本题中是完全合法的,但是由于remove重载了remove(Object o)
和remove(int index)
两种实现,在List集合元素是Integer
类型的情况下,也许某种情况下会出现歧义。因此选用LinkedList
。在上面的代码中,我们会发现以下情况是不必要考虑的:
我们会发现,在组合问题中,剪枝通常发生在横向遍历中,即使用for循环遍历剩余可选结果集这个过程中。因此,我们只需要在for循环的终止条件上进行改动即可。
假设需要选的元素个数为 k k k,总共有 n n n个元素,已选的元素个数为 x x x,则至少还需要 k − x k-x k−x个元素。为了后面能选上 k − x k-x k−x个元素,for循环的i最多能取到 n − ( k − x ) + 1 n-(k-x)+1 n−(k−x)+1.
代码实现也很简单,x其实也就是是path.size()。改变for循环中的循环终止条件即可。如下:
if(path.size()==k){
//收集结果
result.add(new LinkedList<>(path));
return;
}
for(int i = startIndex;i<=n-(k-path.size())+1;i++){
path.add(i);//处理结点
backtrackingCombine(path,result,n,k,i+1);//递归到下一层
path.removeLast();//回溯,撤销处理
}