本次分享一下經過廣度優先搜索解決八數碼問題並展現其最短路徑的動畫效果。css
歡迎關注個人博客,不按期更新中——html
該效果爲從[[2, 6, 3],[4, 8, 0],[7, 1, 5]] ==> [[[1, 2, 3],[4, 5, 6],[7, 8, 0]]]的效果展現node
源碼地址git
配置方式以下:github
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能夠和其餘數字交換。同時有一個開始狀態和結束狀態,如今須要求解出從初始到結束所須要的步數與過程。canvas
網上有不少算法能夠解決八數碼問題,本次咱們採用最容易理解也是最簡單的廣度優先搜索(BFS),雖然是無序搜索而且浪費效率,不過咱們仍是先解決問題要緊,優化的方式你們能夠接着百(谷)度(歌)一下。好比A*之類的,由於做者也不太會(逃。數組
<center>原圖來自JS 中的廣度與深度優先遍歷</center >
這張圖很好的展現了最基本的廣度優先搜索的概念,即一層一層來遍歷節點。在代碼實現中咱們須要按照上面圖中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所在位置以前的逆序數量出現了變化。七、8都比6大,則總體逆序數量會減小2,可是逆序數-2仍然保持了奇偶性。與此同時咱們能夠知道,當0縱向移動的時候,中間的兩個數(當前例子七、8的位置)只會有三種狀況。要不都比被交換數大(好比七、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下交流。