前言
提示:我对你透露一个大密码,这是人类最古老的玩笑。往哪走,都是往前走。 --米兰·昆德拉
回溯是非常重要的算法思想之一,主要解决一些暴力枚举也搞不定的问题(这里埋个坑💣)例如组合、分割、子集、棋盘等等。从性能角度来看回溯算法的效率并不是很高,但是对于暴力也解决不了的问题,它往往很快可以出结果,效率低就可以理解了吧。接下来,就看看回溯的事情吧🤩
回溯可以视为递归的拓展,很多思想和解法都和递归密切相关,再很多材料中都将回溯与递归(一同解释)
,后续我们会遇到路径问题,这就是很典型的问题(相结合的形式),学习回溯时,可以更好的理解递归,相反也是一样的。
关于递归和回溯,这里也区分一下,假设一个场景是这样的:我想脱单怎么办
- 递归策略:先与意中人制造偶遇,然后了解人家的情况,然后约人家吃饭,有好感以后尝试拉手,如果没拒绝,就尝试表白啦。
- 回溯策略:先统计周围所有的单身女孩,然后一个一个表白,被拒绝就说“我喝醉了”,然后就当什么事情也没发生,继续找下一个。
其实回溯的本质就是这么一个过程,好好想一下,看看有没有感觉~。
回溯最大的好处就是有非常明确的模板,所以这里的回溯都是有一个大框架,因此透彻理解回溯的框架是解决一切回溯问题的基础。所以这里做到是就是分析这个框架,让你彻底搞明白。
当然回溯并不是万能的,而且解决问题也是非常明确,例如:
- 组合
- 分割
- 子集
- 排序
- 棋盘
不过这些问题,在处理的时候具体措施也有不同,这里我们接着往下看。
回溯可以理解为递归的扩展,代码层面上又很像深度遍历的N叉树,因此只要知道递归,理解回溯并不难,难在很多人不理解为什么在递归之后会有个“撤销”的动作。这里通过图示给你解释清楚,这里假设一个场景,如果你这个时候谈新女朋友了,来你家之前,你时候会将你前任的东西藏起来?回溯就是这样,有些信息是前任的,需要处理掉,重新开始才行。
回溯最让人激动的是有非常清晰的解题模板,如下所示,大部分的回溯代码的框架都是这个样子,具体的我们后面在解释:
void backtracking(参数){
if(终止条件){
存放结果;
return;
}
for(选择本层集中元素(画成树,就是树节点孩子的大小)){
处理节点;
backtracking();
回溯,撤销处理结果;
}
}
回溯是有明确的解题模板,下面我们就具体分析一下。
从N叉树说起
解释之前,还是看看N叉树的遍历吧,我们知道二叉树的前序过程:
void treeDFS(TreeNode root) {
if(root == null){
return;
}
system.out.println(root.val);
treeDFS(root.left);
treeDFS(root.right);
}
class TreeNode{
int val;
TreeNode left;
TreeNode right;
}
这里如果是三叉树,四叉树甚至N叉树该怎么处理呢?很显然这里不能用left和right来表示分支了,使用一个List比较好,也就是下面这个例子:
class TreeNode{
int val;
List<TreeNode> nodes;
}
代码就需要改一下了:
public statis void treeDFS(TreeNode root) {
// 递归的必要终止条件
if(root == null){
return;
}
// 处理节点
system.out.println(root.val);
// 通过循环,分别遍历N个字数
for(int i = 1; i <= node.length; i++){
treeDFS("第i个字节点");
}
}
到这里,你有没有发现已经很相似了,说明两者之间确实存在某种必要的联系。其他的咱们暂时不管,我们是否已经记住刚才的那个框架了。(N叉树的遍历)
为什么有的问题暴力搜索也不行
试想一下,回溯主要解决暴力枚举处理不了的问题,为什么这么神奇,暴力解决不了?
参考题目介绍:77. 组合 - 力扣(LeetCode)
这里说先明确一点:如果n=4,k=2.那就是说从4个数种选择2个,问你最后能选出多少组数据。
这里类似高中的一个数学题:大致说一下过程,如果数字n=4,能用的数字就是{1,2,3,4}
- 先取出一个1,则有[1,2]、[1,3]、[1,4]三种可能。
- 再取出一个2,1已经取过了,不能再取了,则可以取[2,3]、[2,4]两种可能。
- 再取一个3,1和2都已经取过了,不能再取了,这里就剩下[3,4]一种可能了。
- 取4,以为1,2,3都已经取过了,所以就直接返回null。
- 最后的结果就是[1,2]、[1,3]、[1,4、[2,3]、[2,4]、[3,4]
这里我们思考下该问题要怎么实现,假如只有两个数,采用双层循环就可以了:
int n = 4;
for(int i = 1; i <= n; i++){
for(int j = i + 1; j <= n; j++){
system.out.println(i + " "+j);
}
}
那如果n和k都变大,比如说n=200,k是3呢?也可以采用三层循环搞定:
int n = 200;
for(int i = 1; i <= n; i++){
for(int j = i + 1; j <= n; j++){
for(int l = j + 1; l <= n; l++){
system.out.println(i + " "+j +" " + l);
}
}
}
如果这里的k是5呢?如果更大呢?这里就不好写了吧?甚至告诉你k就是一个未知的正整数k,你需要怎么写循环呀,是不是无解了,这里已经无能为力的吧,这里就是说暴力搜素不能解决。
这就是组合类型问题,除此以外子集、排序、切割、棋盘等方面都有类似的问题,我们可以好好学习一下。
总结
提示:回溯算法;初始回溯;什么叫回溯;回溯的套路;回溯的核心问题
如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/
如有不理解的地方,欢迎你在评论区给我留言,我都会逐一回复 ~
也欢迎你 关注我 ,喜欢交朋友,喜欢一起探讨问题。