当前位置: 首页 > news >正文

算法与数据结构-回溯算法

文章目录

  • 如何理解“回溯算法”?
  • 两个回溯算法的经典应用
    • 0-1 背包
    • 正则表达式


如何理解“回溯算法”?

笼统地讲,回溯算法很多时候都应用在“搜索”这类问题上。不过这里说的搜索,并不是狭义的指我们前面讲过的图的搜索算法,而是在一组可能的解中,搜索满足期望的解。

回溯的处理思想,有点类似枚举搜索。我们枚举所有的解,找到满足期望的解。为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段,我们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走。

理论的东西还是过于抽象,老规矩,我还是举例说明一下。我举一个经典的回溯例子,我想你可能已经猜到了,那就是八皇后问题。

我们有一个 8x8 的棋盘,希望往里放 8 个棋子(皇后),每个棋子所在的行、列、对角线都不能有另一个棋子。你可以看我画的图,第一幅图是满足条件的一种方法,第二幅图是不满足条件的。八皇后问题就是期望找到所有满足这种要求的放棋子方式。
在这里插入图片描述
我们把这个问题划分成 8 个阶段,依次将 8 个棋子放到第一行、第二行、第三行……第八行。在放置的过程中,我们不停地检查当前的方法,是否满足要求。如果满足,则跳到下一行继续放置棋子;如果不满足,那就再换一种方法,继续尝试。

回溯算法非常适合用递归代码实现,所以,我把八皇后的算法翻译成代码。我在代码里添加了详细的注释,你可以对比着看下。如果你之前没有接触过八皇后问题,建议你自己用熟悉的编程语言实现一遍,这对你理解回溯思想非常有帮助。

int[] result = new int[8];// 全局或成员变量, 下标表示行, 值表示 queen 存储在哪一列
public void cal8queens(int row) { // 调用方式:cal8queens(0);if (row == 8) { // 8 个棋子都放置好了,打印结果printQueens(result);return; // 8 行棋子都放好了,已经没法再往下递归了,所以就 return}for (int column = 0; column < 8; ++column) { // 每一行都有 8 中放法if (isOk(row, column)) { // 有些放法不满足要求result[row] = column; // 第 row 行的棋子放到了 column 列cal8queens(row+1); // 考察下一行}}
}private boolean isOk(int row, int column) {// 判断 row 行 column 列放置是否合适int leftup = column - 1, rightup = column + 1;for (int i = row-1; i >= 0; --i) { // 逐行往上考察每一行if (result[i] == column) return false; // 第 i 行的 column 列有棋子吗?if (leftup >= 0) { // 考察左上对角线:第 i 行 leftup 列有棋子吗?if (result[i] == leftup) return false;}if (rightup < 8) { // 考察右上对角线:第 i 行 rightup 列有棋子吗?if (result[i] == rightup) return false;}--leftup; ++rightup;}return true;
}private void printQueens(int[] result) { // 打印出一个二维矩阵for (int row = 0; row < 8; ++row) {for (int column = 0; column < 8; ++column) {if (result[row] == column) System.out.print("Q ");else System.out.print("* ");}System.out.println();}System.out.println();
}

两个回溯算法的经典应用

回溯算法的理论知识很容易弄懂。不过,对于新手来说,比较难的是用递归来实现。所以,我们再通过两个例子,来练习一下回溯算法的应用和实现。

0-1 背包

0-1 背包是非常经典的算法问题,很多场景都可以抽象成这个问题模型。这个问题的经典解法是动态规划,不过还有一种简单但没有那么高效的解法,那就是今天讲的回溯算法。

0-1 背包问题有很多变体,我这里介绍一种比较基础的。我们有一个背包,背包总的承载重量是 Wkg。现在我们有 n 个物品,每个物品的重量不等,并且不可分割。我们现在期望选择几件物品,装载到背包中。在不超过背包所能装载重量的前提下,如何让背包中物品的总重量最大?

实际上,背包问题我们在贪心算法那一节,已经讲过一个了,不过那里讲的物品是可以分割的,我可以装某个物品的一部分到背包里面。今天讲的这个背包问题,物品是不可分割的,要么装要么不装,所以叫 0-1 背包问题。显然,这个问题已经无法通过贪心算法来解决了。我们现在来看看,用回溯算法如何来解决。

对于每个物品来说,都有两种选择,装进背包或者不装进背包。对于 n 个物品来说,总的装法就有 2n 种,去掉总重量超过 Wkg 的,从剩下的装法中选择总重量最接近 Wkg 的。不过,我们如何才能不重复地穷举出这 2n 种装法呢?

这里就可以用回溯的方法。我们可以把物品依次排列,整个问题就分解为了 n 个阶段,每个阶段对应一个物品怎么选择。先对第一个物品进行处理,选择装进去或者不装进去,然后再递归地处理剩下的物品。描述起来很费劲,我们直接看代码,反而会更加清晰一些。

这里还稍微用到了一点搜索剪枝的技巧,就是当发现已经选择的物品的重量超过 Wkg 之后,我们就停止继续探测剩下的物品。你可以看我写的具体的代码。

public int maxW = Integer.MIN_VALUE; // 存储背包中物品总重量的最大值
// cw 表示当前已经装进去的物品的重量和;i 表示考察到哪个物品了;
// w 背包重量;items 表示每个物品的重量;n 表示物品个数
// 假设背包可承受重量 100,物品个数 10,物品重量存储在数组 a 中,那可以这样调用函数:
// f(0, 0, a, 10, 100)
public void f(int i, int cw, int[] items, int n, int w) {if (cw == w || i == n) { // cw==w 表示装满了 ;i==n 表示已经考察完所有的物品if (cw > maxW) maxW = cw;return;}f(i+1, cw, items, n, w);if (cw + items[i] <= w) {// 已经超过可以背包承受的重量的时候,就不要再装了f(i+1,cw + items[i], items, n, w);}
}

正则表达式

正则表达式中,最重要的就是通配符,通配符结合在一起,可以表达非常丰富的语义。为了方便讲解,我假设正表达式中只包含“”和“?”这两种通配符,并且对这两个通配符的语义稍微做些改变,其中,“”匹配任意多个(大于等于 0 个)任意字符,“?”匹配零个或者一个任意字符。基于以上背景假设,我们看下,如何用回溯算法,判断一个给定的文本,能否跟给定的正则表达式匹配?

我们依次考察正则表达式中的每个字符,当是非通配符时,我们就直接跟文本的字符进行匹配,如果相同,则继续往下处理;如果不同,则回溯。

如果遇到特殊字符的时候,我们就有多种处理方式了,也就是所谓的岔路口,比如“*”有多种匹配方案,可以匹配任意个文本串中的字符,我们就先随意的选择一种匹配方案,然后继续考察剩下的字符。如果中途发现无法继续匹配下去了,我们就回到这个岔路口,重新选择一种匹配方案,然后再继续匹配剩下的字符。

有了前面的基础,是不是这个问题就好懂多了呢?我把这个过程翻译成了代码,你可以结合着一块看下,应该有助于你理解。

public class Pattern {private boolean matched = false;private char[] pattern; // 正则表达式private int plen; // 正则表达式长度public Pattern(char[] pattern, int plen) {this.pattern = pattern;this.plen = plen;}public boolean match(char[] text, int tlen) { // 文本串及长度matched = false;rmatch(0, 0, text, tlen);return matched;}private void rmatch(int ti, int pj, char[] text, int tlen) {if (matched) return; // 如果已经匹配了,就不要继续递归了if (pj == plen) { // 正则表达式到结尾了if (ti == tlen) matched = true; // 文本串也到结尾了return;}if (pattern[pj] == '*') { // * 匹配任意个字符for (int k = 0; k <= tlen-ti; ++k) {rmatch(ti+k, pj+1, text, tlen);}} else if (pattern[pj] == '?') { // ? 匹配 0 个或者 1 个字符rmatch(ti, pj+1, text, tlen);rmatch(ti+1, pj+1, text, tlen);} else if (ti < tlen && pattern[pj] == text[ti]) { // 纯字符匹配才行rmatch(ti+1, pj+1, text, tlen);}}
}

相关文章:

  • EthernetIP主站转EtherCAT协议网关采集电力变压器的 Ethernet IP 数据
  • QT5.15.2搭建Android编译环境及使用模拟器调试(全)
  • ONES插件开发的学习笔记
  • Linux内核分析(一)--内核架构和子系统
  • P3393 逃离僵尸岛
  • MySQL数据库入门到大牛_01_数据库概述
  • 在Linux上通过NTLM认证连接到AD服务器(未完结)
  • 利用maven的dependency插件分析工程的依赖
  • AI智能分析网关高空抛物算法如何实时检测高楼外立面剥落?
  • CN考研真题知识点二轮归纳(5)
  • 第十五章 EM期望极大算法及其推广
  • react_14
  • 香港金融科技周2023:AIGC重塑金融形态
  • 性能指标>软硬件的性能指标
  • Linux中阶教程:bash shell基础
  • 【跃迁之路】【519天】程序员高效学习方法论探索系列(实验阶段276-2018.07.09)...
  • Hexo+码云+git快速搭建免费的静态Blog
  • JavaScript工作原理(五):深入了解WebSockets,HTTP/2和SSE,以及如何选择
  • JavaScript类型识别
  • mysql常用命令汇总
  • 百度小程序遇到的问题
  • 翻译:Hystrix - How To Use
  • 来,膜拜下android roadmap,强大的执行力
  • 模仿 Go Sort 排序接口实现的自定义排序
  • 一天一个设计模式之JS实现——适配器模式
  • LIGO、Virgo第三轮探测告捷,同时探测到一对黑洞合并产生的引力波事件 ...
  • 基于django的视频点播网站开发-step3-注册登录功能 ...
  • 进程与线程(三)——进程/线程间通信
  • 没有任何编程基础可以直接学习python语言吗?学会后能够做什么? ...
  • (+3)1.3敏捷宣言与敏捷过程的特点
  • (1) caustics\
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第2节(共同的基类)
  • (LeetCode) T14. Longest Common Prefix
  • (二)JAVA使用POI操作excel
  • (二十五)admin-boot项目之集成消息队列Rabbitmq
  • (附源码)springboot炼糖厂地磅全自动控制系统 毕业设计 341357
  • (附源码)计算机毕业设计高校学生选课系统
  • (剑指Offer)面试题34:丑数
  • (蓝桥杯每日一题)love
  • (全部习题答案)研究生英语读写教程基础级教师用书PDF|| 研究生英语读写教程提高级教师用书PDF
  • (译) 函数式 JS #1:简介
  • *(长期更新)软考网络工程师学习笔记——Section 22 无线局域网
  • ***通过什么方式***网吧
  • .NET Core 和 .NET Framework 中的 MEF2
  • .NET MVC第三章、三种传值方式
  • .net 生成二级域名
  • ??eclipse的安装配置问题!??
  • @Autowired和@Resource装配
  • @LoadBalanced 和 @RefreshScope 同时使用,负载均衡失效分析
  • @Mapper作用
  • @param注解什么意思_9000字,通俗易懂的讲解下Java注解
  • [04] Android逐帧动画(一)
  • [100天算法】-目标和(day 79)
  • [20180129]bash显示path环境变量.txt
  • [30期] 我的学习方法