廣度優先搜索 ( BFS ) 尋找最短路徑

分享一個簡單的小遊戲,利用 BFS 尋找距離出口的最短路徑。javascript

遊戲規則以下:html

玩家點擊空白塊截住硬幣,雙方一次一步, 若硬幣到達邊界處則逃脫, 成功圍住硬幣則算贏。java

硬幣如何找到最近出口呢,這裏就用到了 BFS 搜索。git

首先咱們來看下 BFS ( 廣度優先搜索 ) 的描述:github

BFS 的思想是從一個頂點 V0 開始,輻射狀地優先遍歷其周圍較廣的區域。相似於樹的按層次遍歷的過程算法

這種搜索很適合咱們遊戲裏硬幣的尋找出口。瀏覽器

在我們遊戲中,每一個點通常有六個相鄰方向:ui

其中,尋找的規則大體以下:this

1. 從起點開始,查看周圍相鄰點是否存在出口;
2. 若不存在,標記每一個點爲已訪問,接着從每一個點的相鄰方向繼續尋找下去;
3. 直至找到一個出口或遍歷完全部相鄰點爲止。
複製代碼

搜索的過程如圖所示,從起點向外擴散:spa

算法代碼:

/** * BFS 求離出口最短路徑 * * 1. 初始化一個隊列,添加起點 * 2. 遍歷隊列中每個元素,與它周圍 6 個相鄰塊 * * 若沒遍歷過,查找周圍是否有出口 * * 若無出口,添加進隊列循環查找 * 3. 循環中止條件 * * 尋找到出口,即爲最短路徑,返回該出口 * * 全部相鄰點均遍歷,無出口,玩家勝利 ✌️ */
 
function getNearestSolver (point) {
  const records = {}
  // 將當前位置做爲隊列第一個元素
  let arr = [point]
  let des = null

  // 若隊列還有元素,則繼續查找
  while (arr.length) {
    // 取出隊首元素
    const V = arr.shift()
    // 獲取隊首元素的相鄰節點並過濾障礙物和已訪問過的節點
    const neighbours = getNeighbours(V).filter(p => typeof status[p] !== 'undefined' && !status[p].isWall && !status[p].hasVisited)
    // 將節點添加進 records 對象中並指明來源節點,同時狀態設爲已訪問
    neighbours.forEach(p => {
      records[p] = { source: V }
      status[p].hasVisited = true
    })

    // 查看相鄰節點中是否有邊界節點,即出口
    const hasEdge = neighbours.find(p => status[p].isEdge)
    arr = arr.concat(neighbours)
    if (hasEdge) {
      des = hasEdge
      break
    }
  }

  // 遍歷結束,重置元素訪問狀態
  for (const s of Object.values(status)) {
    s.hasVisited = false
  }

  // 返回查找的終點和路徑記錄對象
  return { des, records }
}
複製代碼

根據終點倒推第一步的座標:

function getSolution (point) {
  const [x, y] = point
  const { des, records } = getNearestSolver(`${x},${y}`)
  const sixDir = getNeighbours(`${x},${y}`)
  const routes = []
  let nextPos = des

  // 反向尋找第一個座標
  while (nextPos && !sixDir.includes(nextPos)) {
    routes.push(nextPos)
    nextPos = records[nextPos].source
  }

  return { nextPos, routes: routes.concat(nextPos) }
}
複製代碼

用於肯定給定點周圍六個相鄰塊的位置:

function getNeighbours (point) {
  const split = point.split(',')
  const x = +split[0]
  const y = +split[1]

  // 奇偶行共同的相鄰座標
  const commonDir = [`${x},${y+1}`, `${x+1},${y}`, `${x},${y-1}`, `${x-1},${y}`]
  // 因奇偶行偏移量致使不一樣的相鄰座標
  const differentDir = y % 2 ? [`${x+1},${y+1}`, , `${x+1},${y-1}`] : [`${x-1},${y+1}`, `${x-1},${y-1}`]

  return commonDir.concat(differentDir)
}
複製代碼

至此,JS 部分就結束了,當咱們隨意點擊棋盤上某一空白處時,該位置會變成障礙物,同時硬幣會將本身位置傳進 getSolution(point) 方法以肯定下一步行走的方向。

來看下效果吧:

最後,小小總結下 廣度優先搜索

1. 查找與當前頂點相鄰的未訪問頂點,將其添加到隊列並標爲已訪問;
2. 從隊列中取出下一個頂點,繼續(1)的步驟,查找相鄰頂點是否存在目標頂點;
3. 循環往復,直至找到目標頂點或隊列爲空。
複製代碼

用一段通用代碼表示以下:

function BFS(s) {
    const queue = []
    s.visited = true
    queue.push(s)
    while (queue.length) {
        const V = queue.shift()
        for each(const w in this.neighbours(V)) {
            if (!w.visited) {
                w.source = V
                w.visited = true
                queue.push(w)
                if (w.isEdge) break
            }
        }
    }
}
複製代碼

代碼倉庫在 Catch-the-coin,下載後在瀏覽器打開 index.html 便可試玩哦(喜歡就點個贊吧,逃~ 😂

相關文章
相關標籤/搜索