回溯法,一般可以解决如下几种问题:
● 组合问题:N个数里面按一定规则找出k个数的集合
● 切割问题:一个字符串按一定规则有几种切割方式
● 子集问题:一个N个数的集合里有多少符合条件的子集
● 排列问题:N个数按一定规则全排列,有几种排列方式
● 棋盘问题:N皇后,解数独等等
如何理解?
抽象为树形结构
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
77.组合
如何理解这个树形结构?
for循环横向遍历,纵向是递归遍历。
树的每一层可以看作是一个递归,每一个非叶子结点横向是一个for循环遍历。
在每一层里,去循环操作数组里的取数。
横向遍历(for循环):在每一层的递归中,for循环用于遍历从startIndex开始到n结束的所有可能选项。每次循环尝试将一个新的数字加入到当前的组合path中。
纵向遍历(递归调用):当加入一个新的元素到path后,代码通过递归调用进入下一层,这相当于在树形结构中向下走一层,寻找下一个可以加入path的元素,直到达到组合的长度k。
非叶子节点:树形结构中的非叶子节点表示一个部分构建的组合。每一次递归调用都尝试向这个部分构建的组合中添加一个新的元素,这是通过for循环实现的。
叶子节点:当构建的组合长度达到k时,达到一个叶子节点,这个完整的组合被添加到结果列表result中。
回溯:在达到叶子节点后,回溯发生,即移除path的最后一个元素(通过removeLast()方法),返回到上一个非叶子节点,继续for循环的下一次迭代,寻找下一个可能的元素。
回溯三部曲
● 递归函数的返回值及参数
● 回溯函数终止条件
○ path的大小=k,就可以终止
● 单层搜索的过程
○ backtracking通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到叶子节点就要返回。
○ 撤销本次处理结果。
■ 我们得到了[1,2],需要再把2拿出来,放入3得到[1,3],同理也要把3拿出来4放进去,得到[1,4]。
class Solution {
List<Integer> path=new ArrayList<>();
List<List<Integer>> result=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 startIndex){
//回溯终止条件
if(path.size()==k){
//result.add(new ArrayList<>(path));确保了每次向result添加的都是当前path的一个独立的副本,从而使得每个组合都能正确无误地保存下来,避免了因path后续修改导致的数据错误。
result.add(new ArrayList<>(path));
return;
}
//控制树的横向遍历
for(int i=startIndex;i<=n;i++){
path.add(i);
//递归,控制树的纵向遍历,下一层搜索从i+1开始
backtracking(n, k,i+1);
//回溯,撤销处理的节点
path.removeLast();
}
}
}
● 第一层递归:初始startIndex=1,path加入1,变为[1]。
○ 调用backtracking(4, 2, 2),即从2开始寻找剩下的数加入组合。
● 第二层递归:这里开始遍历i的值从2开始。
○ 当i=2,path加入2,变为[1, 2],满足条件(path.size() == k),将[1, 2]添加到result中,然后移除2,path回到[1]。
○ 当i=3,path加入3,变为[1, 3],同样满足条件,将[1, 3]添加到result中,然后移除3,path回到[1]。
○ 当i=4,path加入4,变为[1, 4],同样满足条件,将[1, 4]添加到result中,然后移除4,path回到[1]。
● 回到第一层递归:完成所有i=1时的递归,移除1,path变回[]。
● i=2的递归开始:此时从2开始重复上述过程,直到遍历完所有可能的组合。