JPS是一種優化A*算法的路徑規劃算法,主要用于網格地圖,通過跳過不必要的節點來提高搜索效率。它利用路徑的對稱性,只擴展特定的“跳點”,從而減少計算量。
deepseek生成的總是無法完整運行,因此決定手寫一下。
需要注意的幾點:
-
跳點檢測:
jump()
?方法和?hasForcedNeighbor()
?方法是算法核心,需要完整實現強制鄰居檢查邏輯 -
鄰居剪枝:
findNeighbors()
?需要根據父節點方向進行方向剪枝,避免不必要的檢查 -
代價計算:對角線移動使用√2成本,直線移動使用1單位成本
-
啟發函數:使用適合網格移動的切比雪夫距離
以下是完整的跳點尋路算法,經測試可以運行:
type Grid = number[][];
type Point = { x: number; y: number };
type Direction = { dx: number; dy: number };class Cell {public x: number;public y: number;public parent: Cell | null;public g: number = 0;public h: number = 0;constructor(x: number,y: number,parent: Cell | null = null,g: number = 0,h: number = 0) {this.x = x;this.y = y;this.parent = parent;this.g = g;this.h = h;}get f(): number {return this.g + this.h;}
}class JPS {private openList: Cell[] = [];private closedSet = new Set<string>();private readonly directions: Direction[] = [{ dx: 0, dy: -1 }, // 上{ dx: 1, dy: 0 }, // 右{ dx: 0, dy: 1 }, // 下{ dx: -1, dy: 0 }, // 左{ dx: 1, dy: -1 }, // 右上{ dx: 1, dy: 1 }, // 右下{ dx: -1, dy: 1 }, // 左下{ dx: -1, dy: -1 } // 左上];constructor(private grid: Grid,private start: Point,private end: Point) {}public findPath(): Point[] {this.openList.push(new Cell(this.start.x, this.start.y));while (this.openList.length > 0) {// 按F值排序獲取最小節點this.openList.sort((a, b) => a.f - b.f);const currentCell = this.openList.shift()!;if (currentCell.x === this.end.x && currentCell.y === this.end.y) {return this.buildPath(currentCell);}const key = `${currentCell.x},${currentCell.y}`;this.closedSet.add(key);const neighbors = this.findNeighbors(currentCell);for (const neighbor of neighbors) {const jumpPoint = this.jump(neighbor, currentCell);if (jumpPoint) {const existing = this.openList.find(n => n.x === jumpPoint.x && n.y === jumpPoint.y);const g = currentCell.g + this.calculateCost(currentCell, jumpPoint);if (!existing || g < existing.g) {const newCell = new Cell(jumpPoint.x,jumpPoint.y,currentCell,g,this.heuristic(jumpPoint));if (!existing) {this.openList.push(newCell);} else {existing.parent = newCell.parent;existing.g = newCell.g;}}}}}return []; // 無路徑}private findNeighbors(node: Cell): Point[] {// 實現鄰居查找(考慮父節點方向)// 此處需要根據父節點方向剪枝不必要的方向// 完整實現需要處理不同移動方向的情況return this.directions.map(d => ({ x: node.x + d.dx, y: node.y + d.dy })).filter(p => this.isWalkable(p.x, p.y));}private jump(current: Point, from: Cell): Point | null {// 實現跳點檢測的核心邏輯if (!this.isWalkable(current.x, current.y)) return null;if (current.x === this.end.x && current.y === this.end.y) return current;// 檢查強制鄰居(核心跳點邏輯)if (this.hasForcedNeighbor(current, from)) {return current;}// 直線跳躍和對角線跳躍處理const dx = current.x - from.x;const dy = current.y - from.y;// 對角線移動if (dx !== 0 && dy !== 0) {// 嘗試水平/垂直方向跳躍if (this.jump({ x: current.x + dx, y: current.y }, from) ||this.jump({ x: current.x, y: current.y + dy }, from)) {return current;}}// 繼續沿方向跳躍return this.jump({x: current.x + dx,y: current.y + dy}, from);}private hasForcedNeighbor(point: Point, parent: Cell): boolean {const dx = point.x - parent.x;const dy = point.y - parent.y;// 直線移動方向檢測if (dx === 0 || dy === 0) {return this.checkStraightForcedNeighbors(point, dx, dy);}// 對角線移動方向檢測return this.checkDiagonalForcedNeighbors(point, dx, dy);}private checkStraightForcedNeighbors(point: Point, dx: number, dy: number): boolean {// 水平/垂直移動時的強制鄰居檢查const forcedDirs: Direction[] = [];if (dx !== 0) { // 水平移動const checkDir = dy === 0 ? [1, -1] : [dy]; // 處理可能的誤差forcedDirs.push({ dx: 0, dy: 1 }, // 下方強制鄰居方向{ dx: 0, dy: -1 } // 上方強制鄰居方向);} else { // 垂直移動forcedDirs.push({ dx: 1, dy: 0 }, // 右側強制鄰居方向{ dx: -1, dy: 0 } // 左側強制鄰居方向);}// 主移動方向const mainDir = { dx, dy };return forcedDirs.some(dir => {// 檢查障礙物+可行走區域模式const obstaclePos = {x: point.x + (mainDir.dx !== 0 ? mainDir.dx : dir.dx),y: point.y + (mainDir.dy !== 0 ? mainDir.dy : dir.dy)};const neighborPos = {x: point.x + dir.dx,y: point.y + dir.dy};// 必須滿足:障礙物位置不可行走 + 鄰居位置可行走return !this.isWalkable(obstaclePos.x, obstaclePos.y) && this.isWalkable(neighborPos.x, neighborPos.y);});}private checkDiagonalForcedNeighbors(point: Point, dx: number, dy: number): boolean {// 對角線移動時的強制鄰居檢查const horizontalCheck = { dx, dy: 0 };const verticalCheck = { dx: 0, dy };// 檢查水平方向是否有障礙物導致強制鄰居const hasHorizontal = !this.isWalkable(point.x, point.y - dy) && this.isWalkable(point.x + dx, point.y - dy);// 檢查垂直方向是否有障礙物導致強制鄰居const hasVertical = !this.isWalkable(point.x - dx, point.y) && this.isWalkable(point.x - dx, point.y + dy);// 檢查自然鄰居const naturalNeighbor = this.isWalkable(point.x + dx, point.y) || this.isWalkable(point.x, point.y + dy);return hasHorizontal || hasVertical || naturalNeighbor;}private isWalkable(x: number, y: number): boolean {return x >= 0 && y >= 0 && x < this.grid[0].length && y < this.grid.length &&this.grid[y][x] === 0;}private buildPath(node: Cell): Point[] {const path: Point[] = [];let current: Cell | null = node;while (current) {path.unshift({ x: current.x, y: current.y });current = current.parent;}return path;}private heuristic(point: Point): number {// 使用對角線距離(切比雪夫距離)const dx = Math.abs(point.x - this.end.x);const dy = Math.abs(point.y - this.end.y);return Math.max(dx, dy);}private calculateCost(a: Cell, b: Point): number {// 對角線移動成本為√2,直線為1const dx = Math.abs(a.x - b.x);const dy = Math.abs(a.y - b.y);return dx === dy ? Math.SQRT2 : 1;}
}// 使用示例
const grid: Grid = [[0, 0, 0, 0, 0],[0, 1, 1, 1, 0],[0, 0, 0, 0, 0],[0, 1, 1, 1, 0],[0, 0, 0, 0, 0]
];const jps = new JPS(grid, { x: 0, y: 0 }, { x: 4, y: 4 });
const path = jps.findPath();
console.log("Found path:", path);