欢迎来到DIVCSS5查找CSS资料与学习DIV CSS布局技术!
如何尝试走迷宫呢?遇到障碍物就从头 “回溯” 继续探索,这就是回溯算法的形象解释。
 
更抽象的,可以将回溯算法理解为深度遍历一颗树,每个叶子结点都是一种方案的终态,而对某条路线的判断可能在访问到叶子结点之前就结束。
 
相比动态规划,回溯可以解决的问题更复杂,尤其是针对具有后效性的问题。
 
动态规划之所以无法处理有后效性问题,原因是其 dp(i)=F(dp(j)) 其中 0<=j<i 导致的,因为 i 通过 i-1 推导,如果 i-1 的某种选择会对 i 的选择产生影响,那么这个推导就是无效的。
 
而回溯,由于每条分支判断是相互独立的,互不影响,所以即便前面的选择具有后效性,这个后效性也可以在这条选择线路持续影响下去,而不影响其他分支。
 
所以回溯是一种适用性更广的算法,但相对的,其代价(时间复杂度)也更高,所以只有当没有更优算法时,才应当考虑回溯算法。
 
精读
 
经过上述思考,回溯算法的实现思路就清晰了:递归或迭代。由于两者可以相互转换,而递归理解成本较低,因此我更倾向于递归方式解决问题。
 
这里必须提到一点,即工作与算法竞赛思维的区别:由于递归调用堆栈深度较大,整体性能不如迭代好,且迭代写法不如递归自然,所以做算法题时,为了提升那么一点儿性能,以及不经意间流露自己的实力,可能大家更倾向用迭代方式解决问题。
 
但工作中,大部分是性能不敏感场景,可维护性反而是更重要的,所以工程代码建议用更易理解的递归方式解决问题,把堆栈调用交给计算机去做。
 
其实算法代码追求更简短,能写成一行的绝不换行也是同样的道理,希望大家能在不同环境里自由切换习惯,而不要拘泥于一种风格。
 
用递归解决回溯的套路不止一种,我介绍一下自己常用的 TS 语言方法:
 
function func(params: any[], results: any[] = []) {
 
  // 消耗 params 生成 currentResult
 
  const { currentResult, restParams } = doSomething(params);
 
  // 如果 params 还有剩余,则递归消耗,直到 params 耗尽为止
 
  if (restParams.length > 0) func(restParams, results.concat(currentResult));
 
}
 
这里 params 就类似迷宫后面的路线,而 results 记录了已走的最佳路线,当 params 路线消耗完了,就走出了迷宫,否则终止,让其它递归继续走。
 
所以回溯逻辑其实挺好写的,难在如何判断这道题应该用回溯做,以及如何优化算法复杂度。
 
先从两道入门题讲起,分别是电话号码的字母组合与复原 IP 地址。
 
电话号码的字母组合
 
电话号码的字母组合是一道中等题,题目如下:
 
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
 
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
 
电话号码数字对应的字母其实是个映射表,比如 2 映射 a,b,c,3 映射 d,e,f,那么 2,3 能表示的字母组合就有 3x3=9 种,而要打印出比如 ad、ae 这种组合,肯定要用穷举法,穷举法也是回溯的一种,只不过每一种可能性都要而已,而复杂点儿的回溯可能并不是每条路径都符合要求。

如需转载,请注明文章出处和来源网址:http://www.divcss5.com/html/h63788.shtml