摘要:小结使用深度优先算法,我们能够检测性格测试游戏的逻辑正确性,相比以往课堂上的理论,在这里算是一个具体的应用场景吧。其实深度优先算法的应用面也很广,迟早还会再碰面的。
写在前面
在开始前想先说一下关于这个课题的感想——能学以致用是一件很快乐的事情。
深度优先算法(简称DFS),在大学的数据结构课本中有这一个章节,依稀记得另外一个叫广度优先算法(简称BFS),在当时的我看来,它们都还只是理论。万万没想到的是,在毕业后的两年,我会接触到它们,并写下关于这个算法的应用文章,而契机是一个跟性格测试有关的游戏。
这个系列文章的重点,是如何利用DFS算法来检测有向图的回路,而具体的应用场景,就是性格测试。相比于纯讲理论,我更喜欢从实际应用出发,如果你对此感兴趣,就请继续看下去吧。
性格测试游戏想必你肯定玩过问答类的性格测试游戏,游戏规则非常简单,按照心中所想回答问题即可。回答完一个问题后会跳转到另外一个问题,不同的回答可能进入不同的分支。回答完所有问题后会给出一个关于你性格的解答,如下图。
问题就来了,这种性格测试游戏的模型其实是一张有向图。一般而言,题目及答案都是作者设定好的,因此不会出现死循环,也就是环路。例如 1->2->4->1,就是一个死循环,玩家可能一直在第1、2、4这三道题一直循环,游戏不能结束。
如果游戏很复杂,有很多道题目,有可能会设计出死循环。那么像这种环路,我们能用程序检测出来吗?答案是肯定的。
下面先来POST一些概念。
什么是图?什么是有向图?摘自:百度百科 - 图
在数学中,一个图(Graph)是表示物件与物件之间的关系的数学对象,是图论的基本研究。
什么是深度优先算法?摘自:百度百科 - 图
如果给图的每条边规定一个方向,那么得到的图称为有向图。
摘自:百度百科 - 深度优先搜索
深度优先搜索算法(Depth-First-Search),是搜索算法的一种。是沿着树或图的深度遍历节点,尽可能深的搜索分支。当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。
如上图,按DFS的方式以A为起点去遍历的话,遍历顺序为:
A-B-D-E-C-F-G
如果还有不明白的可以自行Google一下。
实践出真知/** * 测试数据,1代表第一题,2代表第二题,-1代表结果A,-2代表结果B,以此类推 * @type {Array} */ var testData = [ [2, 3], [4, -3], [-1, -2], [1, -2] ]; /** * 递归测试,使用深度优先算法 * @param {Array} data 测试数据 * @param {Number} qIndex 问题下标 * @param {Number} aIndex 答案下标 * @param {Array} path 当前回答路径,例如[1,2,4]代表1->2->4的回答顺序 */ function recurseTest(data, qIndex, aIndex, path) { var question = data[qIndex]; // 当前问题 var answer = question[aIndex]; // 要遍历的答案 // 1.判断是否跳转到结果 if (answer > 0) { // 跳转到其他问题 if (path.indexOf(answer) > -1) { // 逻辑错误,当前回答路径已存在,死循环 var result = path.concat([answer, "wrong"]).join(", "); showResult(result); } else { // 逻辑正确,继续沿着这个答案遍历下去 path.push(answer); recurseTest(data, answer - 1, 0, path); } } else { // 跳转到结果 path.push(answer); } // 2.判断是否最后一个答案 if (aIndex === question.length - 1) { // 已经是当前这道题的最后一个答案,返回上层 var result = path.concat(["true"]).join(", "); showResult(result); path.pop(); } else if (aIndex < question.length - 1) { // 还有其他答案,使用下一个答案遍历下去 recurseTest(data, qIndex, aIndex + 1, path); } } /** * 显示回答结果 * @param {String} content 内容 */ function showResult(content) { console.log(content); if (typeof document !== "undefined") { var div = document.createElement("div"); div.innerText = content; document.body.appendChild(div); } } // 测试一下 showResult("测试结果:"); recurseTest(testData, 0, 0, [1]);
https://jsfiddle.net/Vincent_...
要点解读上述代码中的数组path,应该理解成一个栈,它记录的是当前递归的回答顺序,比如[1, 2, 4],代表着,先回答第一题,再回答第二题,再回答第四题。
假如下一个要移动到的问题的序号,存在于栈中,就代表出现了环路,例如[1, 2, 4, 1],此时代表出现了死循环。
这个时候就体现出栈的作用了,比如我们跑完了1->2->?的分支后,需要跑1->3->?的分支,即返回上层,则使2出栈,3入栈。
时间复杂度的延伸DFS算法的时间复杂度是:O(b^m) (b-分支系数,m-图的最大深度)
因此可以看出如果分支系数越大(也就是每一题的答案越多),图深度越大(题目的数量越多),时间复杂度就越高。
为此,我们可以来看看运行这个检测的方法,花了多少时间,递归了多少次:
上面我们只有几个节点,每个节点只有2个出度,因此运算起来很快。如果增加到12个节点呢,每个节点4个出度呢?
没错,是两千多万次递归,时间也来到了接近300ms,越多的顶点和边将带来更多的检测时间,因此检测过多的顶点和边将带来性能问题,这是使用深度优先算法来检测的时候需要注意的。(之前就是因为一个游戏配了20道题,运行一下这个检测方法,直接跑到崩溃。。。)
小结使用深度优先算法,我们能够检测性格测试游戏的逻辑正确性,相比以往课堂上的理论,在这里算是一个具体的应用场景吧。其实深度优先算法的应用面也很广,迟早还会再碰面的。
另一方面,我们讨论了DFS算法的时间复杂度,当图的顶点数增加到一定程度时,运算量暴涨,也因此抛出了一个性能的问题。在看似简单的实现中,我们其实要注意处理好细节,毕竟,放大到1亿次运算,都不是小事!
最后,希望大家会喜欢这样的文章吧。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/84764.html
摘要:多个异步任务的顺序执行通过方法,取得了一个描述加载顺序的二维数组。同时,二维数组的长度也是不定的,更不能穷举。利用这个特性,只需要遍历原二维数组,将每个放在一个中的函数中执行并返回即可因为的返回值就是一个,有一种惰性执行的感觉。 问题 bowl 是一个利用 local storage 进行静态资源缓存和加载的工具库,在开发过程中遇到过一些问题,其中比较典型的是加载多个资源的时候资源之间...
摘要:回顾上一节我们完成了游戏核心场景的大部分工作,能操控主角,能随机掉落苹果了。于是我们修改之前的方法,也就是接到苹果后的方法。接到炸弹后结束和苹果掉地上的调用方式是一样的。 showImg(https://segmentfault.com/img/bVNawu?w=900&h=500); 回顾 上一节我们完成了游戏核心场景play的大部分工作,能操控主角,能随机掉落苹果了。那么这一节我们...
阅读 3529·2023-04-26 00:05
阅读 934·2021-11-11 16:55
阅读 3491·2021-09-26 09:46
阅读 3491·2019-08-30 15:56
阅读 890·2019-08-30 15:55
阅读 2913·2019-08-30 15:53
阅读 1915·2019-08-29 17:11
阅读 784·2019-08-29 16:52