回溯算法基本流程
说明:
将回溯算法解决的问题表示为二叉树形式,for表示树层的循环,即横向;backtracking表示树枝的循环,即纵向。
每次进入backtracking时,都相当于进入了一个节点。需要判断是否符合最后需要求的结果。if用来判断处理结果。然后开始进行横向遍历(树层遍历)。
在处理完本层的节点后,进入了backtracking(即进入了下一层),从下一层返回到上一层时,需要对从下一层返回来的结果进行回溯,撤销在下一层backtracking处理的结果。即图中蓝色线条的路径。
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
startIndex的用法
树层的处理逻辑:backtracking的参数不是一次性确定的,可以慢慢确定,随写随用。这里只分析重点参数startIndex的用法。
1、当每次进入到一个树层,都需要重头开始遍历时,此时在树层的遍历中,应该是置 i = 0,如下:此时用不上startIndex。每次进入树层都会从 0 开始遍历。比如全排列问题,每次进去都需要从头开始处理(不过会跳过已经处理过的节点,这个后面再说)
for (int i =0; i<=n; i++)
2、当我们处理组合问题时,例如如下问题,每次进入一个树层,都需要从下一个开始遍历。举个例子,比如我上一个取了1(上一个树层),进入下一个树层时(backtracking)我就不能再取1了,我从2开始取(下一个树层)。说人话就是,1、2、3、4 这四个数我用过了就不能再用了,只能用一次。
因此如下问题的树层代码处理为:这里的 startIndex 表示从树层的哪里开始遍历。在进入下一层时,将 i + 1 ,即实现了从下一个开始取(不重复)。
如果单纯是 i 呢?那么表示直接从当前这个数开始取,可以重复,比如我上一层取了 2 ,下一层我依然可以取 2 。这里只需要将 backtracking 的递归层的 i + 1 改成 i 。
其实说到这里提一嘴,这是纵向的去重,即用过的不再重复用了,称为树枝去重。下面的used数组进行的实际上是树层的去重。
//backtracking(int n,int k,int startIndex)
for (int i = startIndex; i <= n; i++){
path.add(i);
backtracking(n,k,i+1);
path.removeLast();
}
去重的逻辑
1、used数组去重的用法
考虑问题比如如下问题。candidates 中实际上有重复元素。找出组合需要去重,比如我前两个 2 组成的组合 和 后两个 2 组成的组合就一样。如果用 map 或者 set去重。可能会超时 。
这里提供的去重思路如下:先进行 sort 排列。(为什么要sort呢?为了把一样的数弄到一起)当我们前面使用过 2 这个数之后,我们后面的 2 就不再考虑了。为什么呢?因为这里是求组合问题。比如在这里排序后是 1 2 2 2 5 ,第一个 2 和后面的每个数进行组合所得出的结果集 A ,一定包含第二个 2 和后面的每个数进行组合所得出的集合 B 。即如下代码可以表示重复的数,重复即跳过。
i > 0 && candidates[i] == candidates[i - 1]
但是这么去重确定没有问题吗?这么去重实际上是把树层和树枝的重复全部去掉了。这么得出来的结果如下:
实际上他是把树枝的重复也去掉了,即 我在选定了一个 2 后,后面就不允许再选 2 了。为什么会这样呢?看看下面的图上:
黄色的圈内是上一层和下一层的取数,实际上这里就candidates[i] == candidates[i - 1]。即上一层我取了1,下一层我取的还是1。
粉色的圈内部也同样是candidates[i] == candidates[i - 1]。
那么一个是树层一个是树枝,怎么区分呢?用used数组表示使用过了的数。其实我们要去除的只是 上一个 2 没有用但是下一个 2 用了 的这种情况。因此上一个 2 的 used 数组的对应位置表示 0, 下一个 2 的对应位置表示为 1 (即中间那个1的情况的used数组)。
这是树层的used数组。和下一层有什么关系呢?还记得我在第一个图标出的蓝色箭头吗?表示的就是回溯方向。同一个树层的used数组是回溯来的,因此used[ i - 1 ] 肯定是没有用过的。即used[ i - 1 ] = 0 表示是树层的重复。
而上下层关系中,下一层的used数组是通过backtracking递归下去的,不存在回溯,因此used[ i - 1 ]肯定是用过了的。即used[ i - 1 ] = 1 表示是树枝的重复。
2、用set去重
set去重的逻辑:set存储是否使用过。在当前树层判断当前元素是否存在set中(即是否使用过)。如果没有使用过,那么本层使用后就直接加入set。如果使过用直接continue即可。去重逻辑比起used数组要简单一些,但是不够通用,在处理一些问题的时候比较复杂,这里不详细去说了。
基本上掌握了used数组的去重,能做出大部分回溯的题目了。后续更新二叉树的遍历方式。包括前中后序、层序的通用方法。根回溯差不多,基本上套用模板想明白改进的地方就能AC一些题目了。