携手创作,共同成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
1091. 二进制矩阵中的最短路径
给你一个
n x n
的二进制矩阵grid
中,返回矩阵中最短 畅通路径 的长度。如果不存在这样的路径,返回-1
。二进制矩阵中的 畅通路径 是一条从 左上角 单元格(即,(0, 0)
)到 右下角 单元格(即,(n - 1, n - 1)
)的路径,该路径同时满足下述要求:
- 路径途经的所有单元格都的值都是
0
。- 路径中所有相邻的单元格应当在 8 个方向之一 上连通(即,相邻两单元之间彼此不同且共享一条边或者一个角)。
分析
- 这个题目求矩阵中符合要求:路径途径所有单元格都是0,并且单元格互相都是连通的。首要会想到的就是BFS搜索,从(0,0)位置开始,相邻位置一层一层的搜索,最终会得到最短路径。
- 除了BFS,当我们把这个矩形放在游戏中,是0的路,不是0的是障碍,比如树、建筑等,按题目要求的路径就像是游戏中NPC的移动路径。从而可以想到使用一种启发式搜索——
A*搜索
,一种在某些情况下比Dijkstra的性能更高的算法。
A*搜索
- 以下是科普时间,
- 核心是:f(n) = g(n) + h(n)
- f(n)是每个可能试探点的估值,它有两部分组成:一部分,为g(n),它表示从起始搜索点到当前点的代价(通常用某结点在搜索树中的深度来表示)。另一部分,即h(n),它表示启发式搜索中最为重要的一部分,即当前结点到目标结点的估值
A*搜索
与广度、深度优先和Dijkstra的联系就在于:当g(n)=0时,算法类似于DFS,当h(n)=0时,算法类似于BFS。且同时,如果h(n)为0,只需求出g(n),即求出起点到任意顶点n的最短路径,则转化为单源最短路径问题,即Dijkstra算法。
先实现BFS,A*在其基础上实现
BFS
需要主要的是:
- (0,0)和(n,n)两个点需要是0,不然整个路径就是不存在
- 使用的队列来实现当前搜索矩阵的位置以及路径长度
- 搜索过的位置要及时标记
/**
* @param {number[][]} grid
* @return {number}
*/
var shortestPathBinaryMatrix = function(grid) {
const directions = [[1, 0],[-1, 0],[0, 1],[0, -1],[1, 1],[1, -1],[-1, 1],[-1, -1]];
if (grid[0][0] === 1) return -1;
const N = grid.length;
const queue = [[0, 0, 1]];
while (queue.length) {
const [row, col, path] = queue.shift();
if (row === N - 1 && col === N - 1) return path;
for (const [dx, dy] of directions) {
let x = row + dx;
let y = col + dy;
if (x < 0 || x >= N) continue;
if (y < 0 || y >= N) continue;
if (grid[x][y] !== 0) continue;
queue.push([x, y, path + 1]);
grid[x][y] = 1;
}
}
return -1;
};
A*
- g(n)在BFS的基础上,思路是从终点开始搜索,寻路算法有一些现实意义,因此只算最短路径是不够的,需要知道这个最短路径上的点都是什么,所以先找终点的父亲,再找父亲的父亲,可以通过回溯来找到一条具体的路径,h(n)是启发函数,h(n)的选择与算法速度相关,关于启发函数,则是矩阵上的可能是最短路径上的点之间的距离。
关于距离的计算
- 以下还是科普时间,有几种常用的距离:
- 欧几里得距离:两点之间的线段长度,角度不受限制,但是计算的时候需要平方、开方
- 曼哈顿距离:方向只有上下左右,比较适合计算街区之间的距离
- 切比雪夫距离:又叫棋盘距离,有8个方向
- 结论:使用切比雪夫距离更好一些
确定好h(n)和g(n)后,对矩阵的数据使用类来管理,数据更清晰
class Node {
constructor(row, col, value) {
this.row = row
this.col = col
this.value = value
this.g = Infinity
this.h = Infinity
this.f = Infinity
this.parent = null
}
}
const initNodes = (grid) => {
const nodes = []
for (let r = 0; r < grid.length; r++) {
nodes.push([])
for (let c = 0; c < grid[0].length; c++) {
nodes[r].push(new Node(r, c, grid[r][c]))
}
}
return nodes
}
const calcDistance = (currNode, endNode) => {
return Math.max(endNode.row - currNode.row, endNode.col - currNode.col)
}
const getNeighborNodes = (currNode, nodes) => {
const numRows = nodes.length
const numCols = nodes[0].length
const neighbors = []
for (let r = -1; r <= 1; r++) {
for (let c = -1; c <= 1; c++) {
if (r === 0 && c === 0) continue
const row = currNode.row + r
const col = currNode.col + c
if (row < 0 || row >= numRows || col < 0 || col >= numCols) continue
neighbors.push(nodes[row][col])
}
}
return neighbors
}
const getPathLength = (endNode) => {
if (endNode.parent === null) return -1
let pathLength = 0
let currNode = endNode
while (currNode) {
pathLength++
currNode = currNode.parent
}
return pathLength
}
const shortestPathBinaryMatrix = (grid) => {
const nodes = initNodes(grid)
const n = grid.length
const startNode = nodes[0][0]
const endNode = nodes[n - 1][n - 1]
if (startNode === endNode) return 1
if (startNode.value === 1 || endNode.value === 1) return -1
startNode.g = 0
startNode.h = calcDistance(startNode, endNode)
startNode.f = 0
const nodesToVisit = [startNode]
const visited = new Set()
while (nodesToVisit.length) {
let currNode = nodesToVisit[0]
let currNodeIdx = 0
for (let i = 0; i < nodesToVisit.length; i++) {
const node = nodesToVisit[i]
if (node.f <= currNode.f) {
currNode = node
currNodeIdx = i
}
}
nodesToVisit.splice(currNodeIdx, 1)
visited.add(currNode)
if (currNode === endNode) break
const neighbors = getNeighborNodes(currNode, nodes)
for (const neighbor of neighbors) {
if (neighbor.value === 1 || visited.has(neighbor)) continue
const distanceToNeighbor = currNode.g + 1
if (distanceToNeighbor >= neighbor.g) continue
neighbor.g = distanceToNeighbor
neighbor.h = calcDistance(neighbor, endNode)
neighbor.f = neighbor.g + neighbor.h
neighbor.parent = currNode
if (!nodesToVisit.includes(neighbor)) {
nodesToVisit.push(neighbor)
}
}
}
return getPathLength(endNode)
};
一些可以改进的想法
- 在搜索矩阵的时候,也许可以优先处理最有“前途”的,可能找到最短路径的时间会更短
总结
A*搜索
在这道题里用确实是有些大材小用,真实想法是体验一下A*搜索
,通过一些简单的场景来实现一个A*搜索
也是一种收获- 今天也是有收获的一天