一起刷LeetCode——二进制矩阵中的最短路径(A*搜索)

100 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

1091. 二进制矩阵中的最短路径

给你一个 n x n 的二进制矩阵 grid 中,返回矩阵中最短 畅通路径 的长度。如果不存在这样的路径,返回 -1 。二进制矩阵中的 畅通路径 是一条从 左上角 单元格(即,(0, 0))到 右下角 单元格(即,(n - 1, n - 1))的路径,该路径同时满足下述要求:

  • 路径途经的所有单元格都的值都是 0 。
  • 路径中所有相邻的单元格应当在 8 个方向之一 上连通(即,相邻两单元之间彼此不同且共享一条边或者一个角)。

来源 1091. 二进制矩阵中的最短路径

分析

  • 这个题目求矩阵中符合要求:路径途径所有单元格都是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*搜索也是一种收获
  • 今天也是有收获的一天