快来和叮当学算法吧!
From All CV to No CV!我的梦想是编程不再CV,算法不再死记硬背!
漫话算法[看电影学回溯算法]从《大话西游》到掌握回溯算法
回溯算法简介
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法
打个比方
回溯法就好比给《大话西游》中的至尊宝一个选择的机会,先爱上谁后爱上谁,总共会出现两种情况[1,2]、[2,1]也就是:[先爱“白晶晶”,后爱"紫霞"]、[先爱"紫霞",后爱“白晶晶”]两种情况!这实际上是一个LeetCode中46.全排列这道题目!
在你解决这种问题时你应该按照这个步骤分析
- 确定[选择项]:图中的[1,2]
- [做选择]:将选择项列举为一个决策树,选择一条路径
- [记录路径]:“路径”就是答案如下图中,路径1就是子集1[1,2],路径2是子集2[2,1],那么此问题的答案就为路径1+路径2
- [回溯]:重新选择,通常你可以使用栈(月光宝盒)辅助你回到上一层级,因为上一层刚好在栈底,只需要将本层弹栈就能得到上一个层级,从而选择新的路径,这里列举的情况较少,希望你能通过题目去实操。
- [输出结果]:当所有的“路径”都走完了你就得到了所有可能的答案。
知识路线
有了上面的理解你就入门回溯算法了,但是实际情况下回溯算法不仅仅要解决排列问题,还可能解决最优路径等等问题,这些问题其中比较经典的就是"背包问题"
文章总览:先了解以下知识路线,你就能很快知道我想要你了解的概念
- 基本思想:需要你理解的[关键字]
- 选择(选择一条路径)
- 路径(问题的解)
- 回溯(撤销当前选择项回溯到上一级选择项)
- 栈(月光宝盒)通常可以作为辅助的数据结构回到上一层选择,从而走新的路径得到更多的解,但不是必须的。
- 算法框架:简化的伪代码让你知道基本步骤,你多做几个类似的题你会发现写法都差不多,这样你就有了自己的算法套路。
- 基本步骤:入门解决问题时你应该按部就班的按照基本步骤来思考及编码,形成固定的习惯,熟练以后再考虑优化的事情。
- 问题分类:解决回溯算法的问题通常就是在一棵选择(决策)树中寻找答案,因此你就需要在纸上画出这棵做选择(决策)的树,根据问题通常会分为两类,排列树:求解所有排列,子集树:求解最优解,也就是部分的子树即为[原选择项]的一个子集。
- 优化策略:优化手段通常有两种,这两种都是为了"剪枝"即裁剪无效路径,避免无用的搜索。
- 约束法:就是通过if判断语句确保某一条路径不要超过问题中条件的限制。
- 限界法:通常用于求最优解的问题,当前一个选择的路径A已经得到了比后一个选择的路径B优秀的解时则不用再走路径B了。
ps:本文的目的还是在于让你有回溯的思想,一定要做题你才能掌握,后面我也会更新实战例题解析让你进一步知道这些概念,因为这种思想是通过很多题目提取的因此你不要忽视思想,同时也不能不实战,不然只懂理论。
回溯基本思想
比枚举法更高效的一种搜索算法,可以通过"剪枝"优化,通常可以用于解决"背包"类问题,也就是在一定限定条件下,从可选项中选择一个最优解或可能的全部解,这里面蕴含的就是数学中的"排列"及"组合"的思想!
世界上有后悔药:不断试错,走不通就重新选择
- 不断尝试(试错)
- 重新选择(回溯)
基本思想类似"枚举",就是从一个选择树(解空间)中,按照深度优先(就是选择一条路竖着走到底而不是横向的走)的策略,从根节点出发选择一条[路径]走到无路可走(走到底),存储本次[路径],如果想要回头则可以通过配合栈(可选)回溯(重新选择),从而得到更多的解!
算法框架
通过伪代码提取通用基本的写法!这里是递归写法
主函数
// 回溯中递归思想的体现:顶层选择[0]+剩余选择[1…n-1]
public result function() {
// 1.定义返回值
result = [];
// 2.[月光宝盒]: 根据情况不一定需要
Stack<E> = new Stack<>();
// 3.[回溯]
backtrack(nums, path, res);
return result;
}
回溯:重点记住这个
// [回溯]
backtrack([选择项]nums, 路径: path, 结果: result) {
// 1.[递归出口]: 当路径走到了尽头
if (最后一层) {
// 存储路径(答案)
result.add(path); // 存储路径答案
return;
}
// 2.迭代可选择项
for ([选择] in [选择项]) {
// [做选择]: 拿得起
path.addLast(nums[i]);
// [下潜]
backtrack(nums, path, res); // [下潜]到最后一层后开始[回溯]
// [重新选择]: 放得下
path.removeLast();
}
}
基本步骤
后面我会根据这些具体步骤再写一篇具体的实战文章帮助你理解掌握包括优化等步骤!
1.确定解的空间结构:通常是树结构
- 排列树:枚举列出所有可能的一棵[选择树(决策树)],如视频中的,先"爱"谁后"爱"谁有两种答情况[1,2]、[2,1]。
- 子集树:只选择部分子树(最优解或符合条件的解)。
2.深度优先遍历决策树
从根节点出发,选择一条路走到底,一条[路径]就是"一个解",根据不同的路径按需选择一条或多条路径。
3.确定优化策略:通过“剪枝”避免无效的搜索
-
约束法:通过约束条件"剪掉"不满足约束条件的子树。
-
限界法:通过限界条件"剪掉"不是最优解的子树(用于求最优解的问题)。
排列树-完全的解
排列类问题:求解所有可能的排列,则需要枚举出一课排列树!
子集树-不完全的解
解决子集类问题(背包问题中的一种)
排列的子集类问题:要求从n个元素中选择刚好等于背包容量的排列子集(允许颠倒顺序)。
示例说明:"背包"只是一个比喻,这里背包代表一个集合且最大限制是3
ps:由于背包问题的变种比较多因此以后会单独出一个合集
全排列的解
问题的"全集"
[1]
[1,2,3]
[1,3,3]
[2,1,3]
......
我们想要的解
只是全排列中的"子集"
[1,2]
[2,1]
[3]
约束法[剪枝策略]
通过约束条件,避免无效的搜索
问题:求解"刚好"能组合成"背包"容量的子集
约束函数
大于了背包容量则一定不是解
if(path > [背包容量]) {
return;
}
限界法[剪枝策略]
通过限定界限"裁剪"无效分支避免做无用功。
而"界限"指的就是当前最优的解,如果后面出现的情况还不如当前的最优解那就"裁剪"掉!
问题:求解刚好能组合成"背包"容量的最小子集(与上面的情况不同点就是3的顺序变为第2个了)
示例说明:限界法(剪枝策略):一旦出现最字就要考虑最优解的问题,此时可能就需要采用这种剪枝策略
限界函数
if(path.size > [当前最优解]) {
return;
}
实战为主
空谈误国实战兴邦,后面我会专门针对以上的理论整理相关[题解视频]分门别类的帮助到你掌握"回溯"!
错爱一生
如果给你一次重新选择的机会,你还会当"码农"吗?
代码获取
我会持续更新Leetcode刷题的题解以各种各样你想要的!我会同步到线上博客已经微信公众号:CVBear欢迎你的订阅!
代码获取 Github传送门 帮我点个start鼓励我一下呗我!