Open Aaaaaaaty opened 6 years ago
本次分享一下通过广度优先搜索解决八数码问题并展示其最短路径的动画效果。
欢迎关注我的博客,不定期更新中——
该效果为从[[2, 6, 3],[4, 8, 0],[7, 1, 5]] ==> [[[1, 2, 3],[4, 5, 6],[7, 8, 0]]]的效果展示
源码地址
配置方式如下:
var option = { startNode: [ [2, 6, 3], [4, 8, 0], [7, 1, 5] ], endNode: [ [1, 2, 3], [4, 5, 6], [7, 8, 0] ], animateTime: '300' //每次交换数字所需要的动画时间 } var eightPuzzles = new EightPuzzles(option)
百度一下可以百度出来很多介绍,在此简单说明一下八数码问题所要解决的东西是什么,即将一幅图分成3*3的格子其中八个是图一个空白,俗称拼图游戏=。=,我们需要求解的就是从一个散乱的状态到恢复原状最少需要多少步,以及每步怎么走。
我们可以抽象为现有数字0-8在九宫格中,0可以和其他数字交换。同时有一个开始状态和结束状态,现在需要求解出从初始到结束所需要的步数与过程。
网上有很多算法可以解决八数码问题,本次我们采用最容易理解也是最简单的广度优先搜索(BFS),虽然是无序搜索并且浪费效率,不过我们还是先解决问题要紧,优化的方式大家可以接着百(谷)度(歌)一下。比如A*之类的,因为作者也不太会(逃。
原图来自JS 中的广度与深度优先遍历 这张图很好的展示了最基本的广度优先搜索的概念,即一层一层来遍历节点。在代码实现中我们需要按照上面图中1-12的顺序来遍历节点。实现方式可以为维护一个先入先出的队列Queue,按顺序将一层的节点从队尾推入,之后从从队头取出。当某个节点存在子节点,则将子节点推入队列的队尾,这样就可以保证子节点均会排在上层节点的后面。
现在我们已知广搜的相关概念,那么如何结合到八数码问题中呢?
当你明白了思想之后,我们将其转化为代码思路既可以表示为如下步骤:
看起来一切都很美好是不是?但是我们仍然忽略了一个问题,很关键。
如果真的像拼图一样,从一个已知状态打散到另一个状态,那么肯定是可以复原的。但是我们现在的配置策略是任意的,从而我们需要判断起始状态是否可以达到结束状态。判断方式是通过起始状态和结束状态的逆序数是否同奇偶来判断。
逆序数:在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序的总数就称为这个排列的逆序数。一个排列中所有逆序总数叫做这个排列的逆序数。
如果起始状态与结束状态的逆序数的奇偶性相同,则说明状态可达,反之亦然。至于为什么,作者尝试通过简单的例子来试图说明并推广到整个结论:
//起始状态为[[1,2,3],[4,5,6],[7,8,0]] //可以看做字符串123456780 //结束状态为[[1,2,3],[4,5,6],[7,0,8]] //可以看做字符串123456708
这个变换只需要一步,即0向左与8进行交换。那么对于逆序数而言,0所在的位置是无关紧要的,因为它比谁都小,不会导致位置变化逆序数改变。所以0的横向移动不会改变逆序数的奇偶性。
//起始状态为[[1,2,3],[4,5,6],[7,8,0]] //可以看做字符串123456780 //结束状态为[[1,2,3],[4,5,0],[7,8,6]] //可以看做字符串123450786
这个变换同样只需要一步,即0向上与6进行交换。我们已知0的位置不会影响逆序数的值。那么现在我们只需要关注6的变化。6从第6位置变为第9位置,导致7与8所在位置之前的逆序数量出现了变化。7、8都比6大,则整体逆序数量会减少2,但是逆序数-2仍然保持了奇偶性。与此同时我们可以知道,当0纵向移动的时候,中间的两个数(当前例子7、8的位置)只会有三种情况。要不都比被交换数大(比如7、8比6大)要不一个大一个小,要不都小。如果一大一小,则逆序数仍会保持不变,因为总量上会是+1-1;都小的话则逆序数会+2,奇偶性同样不受到影响。故我们可以认为,0的横向与纵向移动并不会改变逆序数的奇偶性。从而我们可以在一开始通过两个状态的逆序数的奇偶性来判断是否可达。
EightPuzzles.prototype.isCanMoveToEnd = function(startNode, endNode) { startNode = startNode.toString().split(',') endNode = endNode.toString().split(',') if(this.calParity(startNode) === this.calParity(endNode)) { return true } else { return false } } EightPuzzles.prototype.calParity = function(node) { var num = 0 console.log(node) node.forEach(function(item, index) { for(var i = 0; i < index; i++) { if(node[i] != 0) { if (node[i] < item) { num++ } } } }) if(num % 2) { return 1 } else { return 0 } }
EightPuzzles.prototype.solveEightPuzzles = function() { if(this.isCanMoveToEnd(this.startNode, this.endNode)) { var _ = this this.queue.push(this.startNode) this.hash[this.startNodeStr] = this.startNode while(!this.isFind) { var currentNode = this.queue.shift(), currentNodeStr = currentNode.toString().split(',').join('') //二维数组变为字符串 if(_.endNodeStr === currentNodeStr) { //找到结束状态 var path = []; // 用于保存路径 var pathLength = 0 var resultPath = [] for (var v = _.endNodeStr; v != _.startNodeStr; v = _.prevVertx[v]) { path.push(_.hash[v]) // 顶点添加进路径 } path.push(_.hash[_.startNodeStr]) pathLength = path.length for(var i = 0; i < pathLength; i++) { resultPath.push(path.pop()) } setTimeout(function(){ _.showDomMove(resultPath) }, 500) _.isFind = true return } result = this.getChildNodes(currentNode) //获得节点子节点 result.forEach(function (item, i) { var itemStr = item.toString().split(',').join('') if (!_.hash[itemStr]) { //判断是否已存在该节点 _.queue.push(item) _.hash[itemStr] = item _.prevVertx[itemStr] = currentNodeStr //记录节点的父节点 } }) } } else { console.log('无法进行变换得到结果') } }
EightPuzzles.prototype.calDom = function(node) { //根据当前状态渲染各数字位置 node.forEach(function(item, index) { item.forEach(function(obj, i) { $('#' + obj).css({left: i * (100+2), top: index* (100 + 2)}) }) }) } EightPuzzles.prototype.showDomMove = function(path) { var _ = this path.forEach(function(item, index) { //每次状态改变调用一次渲染函数 setTimeout(function(node) { this.calDom(node) }.bind(_, item), index * _.timer) }) }
惯例po作者的博客,不定时更新中——
有问题欢迎在issues下交流。
getChildNode方法能否分享一下
有深度优先吗
深度优先遍历逻辑上可行,但是递归调用会爆栈
getChildNodes 没有啊 , 大佬
写在最前
本次分享一下通过广度优先搜索解决八数码问题并展示其最短路径的动画效果。
欢迎关注我的博客,不定期更新中——
效果预览
该效果为从[[2, 6, 3],[4, 8, 0],[7, 1, 5]] ==> [[[1, 2, 3],[4, 5, 6],[7, 8, 0]]]的效果展示
源码地址
配置方式如下:
八数码问题
百度一下可以百度出来很多介绍,在此简单说明一下八数码问题所要解决的东西是什么,即将一幅图分成3*3的格子其中八个是图一个空白,俗称拼图游戏=。=,我们需要求解的就是从一个散乱的状态到恢复原状最少需要多少步,以及每步怎么走。
我们可以抽象为现有数字0-8在九宫格中,0可以和其他数字交换。同时有一个开始状态和结束状态,现在需要求解出从初始到结束所需要的步数与过程。
解决思路
网上有很多算法可以解决八数码问题,本次我们采用最容易理解也是最简单的广度优先搜索(BFS),虽然是无序搜索并且浪费效率,不过我们还是先解决问题要紧,优化的方式大家可以接着百(谷)度(歌)一下。比如A*之类的,因为作者也不太会(逃。
广度优先搜索
结合八数码与广度优先搜索
现在我们已知广搜的相关概念,那么如何结合到八数码问题中呢?
当你明白了思想之后,我们将其转化为代码思路既可以表示为如下步骤:
看起来一切都很美好是不是?但是我们仍然忽略了一个问题,很关键。
八数码的可解性问题
如果真的像拼图一样,从一个已知状态打散到另一个状态,那么肯定是可以复原的。但是我们现在的配置策略是任意的,从而我们需要判断起始状态是否可以达到结束状态。判断方式是通过起始状态和结束状态的逆序数是否同奇偶来判断。
如果起始状态与结束状态的逆序数的奇偶性相同,则说明状态可达,反之亦然。至于为什么,作者尝试通过简单的例子来试图说明并推广到整个结论:
这个变换只需要一步,即0向左与8进行交换。那么对于逆序数而言,0所在的位置是无关紧要的,因为它比谁都小,不会导致位置变化逆序数改变。所以0的横向移动不会改变逆序数的奇偶性。
这个变换同样只需要一步,即0向上与6进行交换。我们已知0的位置不会影响逆序数的值。那么现在我们只需要关注6的变化。6从第6位置变为第9位置,导致7与8所在位置之前的逆序数量出现了变化。7、8都比6大,则整体逆序数量会减少2,但是逆序数-2仍然保持了奇偶性。与此同时我们可以知道,当0纵向移动的时候,中间的两个数(当前例子7、8的位置)只会有三种情况。要不都比被交换数大(比如7、8比6大)要不一个大一个小,要不都小。如果一大一小,则逆序数仍会保持不变,因为总量上会是+1-1;都小的话则逆序数会+2,奇偶性同样不受到影响。故我们可以认为,0的横向与纵向移动并不会改变逆序数的奇偶性。从而我们可以在一开始通过两个状态的逆序数的奇偶性来判断是否可达。
核心代码
判断可解性
广度优先搜索
生成动画
参考文章
最后
惯例po作者的博客,不定时更新中——
有问题欢迎在issues下交流。