文章目录
概念理解
回溯是一种算法思想,能够回到过去,恢复现场。
回溯的可以看作是用递归遍历数组。参考经典回溯算法:集合划分问题「重要更新 🔥🔥🔥」
因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
回溯算法就是个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置做一些操作,算法框架如下:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」
像类似于List<List> list保存结果
list.add(new ArrayList<>(li));
/*
list不能直接添加li,因为存入的li是集合li的地址,
当li集合发生变化,list里的li也会同时发生变化。
new ArrayList<>(li) :用参数li的元素构造一个新的ArrayList集合,元素顺序是按照li的迭代顺序。
*/
递归调用
process(...,step+1,...);
/*注意不要写step++或++step,否则step自身会改变。而step+1既没有改编自身,又向下一层传递了数值。*/
回退上一层
li.remove(li.size()-1);//li是一个保存暂时结果的List集合
技巧总结
子集、子序列、子数组之间的区别
子集:元素属于数组,但不一定是原数组中的顺序以及可以不连接
子序列:元素属于数组,且排列顺序按照原数组顺序排列,但元素可以不挨着的
子数组:元素属于数组,从原数组切割出来的一部分。
题型总结
- 22. 括号生成
n对括号,每次选择要么是’(‘,要么是’)',做2的2n次次选择可以穷尽所有可能。使用回溯法,用左右括号剩余的数量来判断生产的括号是否合法。 - AAA638. 大礼包
每次递归更新的是一个List集合。 - AAA526. 优美的排列
每个位置考虑与下标整除或被整除的数字,直到所有的位置都被填满了。用一个整除标记哪些数已经被使用过了。
数位DP
常用来计算某一个范围内满足条件的数字个数。
优点是某一个范围的情况计算之后会存储下来,不会计算第二次,另外可以处理临界情况。
0AAAA788. 旋转数字
考虑一个数位的所有可以取值情况,向下递归,这样所有数位上的取值都能考虑到
- 0AAAA902. 最大为 N 的数字组合
遍历n的数位长度,计算从每个数位开始到最后一个位置的所有小于等于n的数字个数。
去重:
递归的过程中当遇到当前值与前面的值相同且前面的值也没有加入到集合,跳过这个值。这样做的理由是,当能遍历到目前这个节点,说明前面的元素已经被遍历过了,标记数组used又显示没遍历,说明前面这个元素已经恢复了现场。考虑到当前元素与前面的元素相等,把当前元素加入集合生成的各种排列其实已经前面的元素已经实现过了,再加入集合会造成重复。
去重:1.boolean数组去重 2.同层set集合去重
boolean数组去重:
条件:需要对数组排序,改变了数组的元素顺序。
使用方法:当前元素与前面的元素相同且前面的元素的使用标志为false,则跳过当前元素。
缺点:要排序
优点:效率高,时间复杂度小
同层set集合去重:
条件:无
使用方法:每进入一个递归,新建一个set集合,单层遍历(横向遍历)时,若当前元素已在set中存在,则跳过当前元素。
缺陷:因为只能本层的元素去重,不能对不同父节点的元素一致顺序不一致的情况做排除。
优点:不需要排序
在不排序的情况下,使用set可以排除掉元素一致,顺序一致的情况,但是对元素一致,顺序不一致的情况不能排除,如对数组{2,1,2,2}子集,会出现{1,2},{2,1}。
这也是可以使用同层的set集合可以给“递增子序列”去重,却不能给未排序的“子集问题”“组合问题”去重的原因。(比如对于数组{1,2,1,2}求递增子序列,使用set不会出现{1,2}两次,因为同层像元素会跳过)
排列问题:求一个数组的所有排列组合
横向遍历:从头遍历整个数组,选择一个没有被加入的元素,进入下一轮递归
纵向遍历(回溯):深度就是数组长度
- 257. 二叉树的所有路径
每次递归传递的形参都是创建的新对象,不会对原来的对象造成影响,所以也就不用恢复现场了。
子集问题: 求一个数组的所有子集。
横向遍历:变量start作为起始下标,从start开始往后,每个元素都一个个加入集合,每加入一个,便进入下一轮递归,递归结束后,恢复现场,在集合中删除这个元素。
纵向遍历(回溯):深度等于start到最后一个元素的长度。终止条件是start指向了数组长度(刚越界)。
组合问题:N个数里面按一定规则找出k个数的集合
拓展题型:限定长度;去重;
变量:start标记单层循环的起始,target是目标对象,每次递归target减去选中的那个元素值,当target为0,结束回溯。
横向遍历:从start开始向左遍历,target依次减去每个不大于target的元素,进入下一层递归。递归参数start取i,不要给i加1,start的移动靠单层遍历来实现。
纵向遍历(回溯):当target为0,结束回溯。
针对限定长度问题,在递归开始处判断集合li的长度等于目标长度,同时target减小为0
去重:数组排序,boolean使用标记数组
分割问题:一个字符串按一定规则有几种切割方式
有判断方法,当满足分割条件,便进行切割,加入集合。
横向遍历:start表示起始下标,横向遍历的i充当右边界,右边界的取值范围是从start开始到末尾,截取start到右边界的字符串,判断这些字符串是否满足条件,如果满足条件,下一层的递归start取右边界+1。
纵向遍历(回溯):终止条件是start指向了数组长度(刚越界),深度不固定。
- AAAA698. 划分为k个相等的子集
用递归遍历数组nums,插入到长度是k的数组中,如果顺利全部插入进去,说明可以划分为k个相等的子集。