JPS算法:让寻路速度提升百倍

文章摘要

JPS(跳点搜索)是一种高效的路径优化算法,相比传统A*算法,它通过识别关键“跳点”和“强迫邻居”减少节点扩展,提升寻路速度。核心机制包括直线/对角线方向的跳点检测和剪枝策略。优化版本如JPS-Bit(位运算加速)和JPS+(预处理跳点)进一步将性能提升15-273倍,但JPS+不支持动态地图。适用场景包括规则网格和高频寻路(如RTS游戏),但需注意路径直线化和动态权重限制。工程实践中,结合预处理(连通性检查、位图存储)和后处理(路径平滑)能显著优化效果。JPS犹如“聪明的快递员”,仅需在必要拐点停留,大幅提升效率。


一、核心机制

1. 跳点与强迫邻居的形象理解

  • JPS的本质
    想象你在一张棋盘上走迷宫,普通A*每走一步都要“左顾右盼”,每个格子都要考虑所有方向。而JPS像是“会飞的棋子”,它会沿着一个方向一直冲,直到遇到“必须转弯”的地方才停下,这些“必须转弯”的点就是跳点

  • 强迫邻居
    假设你正朝东走,突然发现正北方向有堵墙,但东北方向却是空的,这时你就被“强迫”考虑往东北拐弯。这个“被墙逼出来的选择”就是强迫邻居

  • 跳点的三种情况

    1. 起点/终点:自然是跳点。
    2. 遇到强迫邻居:如上例,被障碍“逼”出来的转折点。
    3. 对角线跳点:如果你斜着走,发现可以直接横着或竖着跳到下一个跳点,也算跳点。
跳点示意
S = 起点, G = 终点, # = 障碍, . = 空地
S . . . . . G
  . # . # . .
  . . . . . .
  • 普通A*每格都扩展,JPS会从S直线冲到第一个“必须拐弯”的点才停。

2. 节点扩展规则(形象流程)

当前方向
直线方向?
检查左/右强迫邻居
分解为水平/垂直分量
  • 直线方向:比如一直往右走,只有遇到障碍或强迫邻居才停。
  • 对角线方向:比如右上,分解为“右”和“上”两个方向,分别递归跳。

二、优化策略

优化版本技术手段性能提升倍数支持动态阻挡
JPS基础版跳点剪枝15x
JPS-Bit位运算加速跳点检测81x
JPS-BitPrune剪枝中间跳点110x
JPS+预处理跳点数据273x
  • 形象比喻
    • JPS基础版像是“会飞的棋子”,只在关键点停。
    • JPS-Bit像是“带望远镜的棋子”,用位运算一眼看清前方障碍。
    • JPS+像是“提前记好所有捷径的棋子”,查表就能知道下一个跳点在哪,但地图一变就得重记。

三、性能对比

时间消耗对比

A* 260ms
JPS 17ms
JPS-Bit 3.2ms
JPS+ 0.95ms

内存占用对比

算法预处理内存运行时内存
A*0MB13MB
JPS0MB28MB
JPS+150MB5MB
  • 形象理解:JPS+用“空间换时间”,提前把所有跳点都记下来,查表飞快,但地图一大内存就涨。

四、应用场景与限制

适用场景

  • 短距离寻路:如AI追击、RTS单位集群。
  • 规则网格:如2D格子地图。
  • 高频寻路:如大批量单位同时寻路。

主要限制

  • 不支持动态权重:不能处理不同地形消耗。
  • 路径直线化:路径很“直”,不够自然,需后处理平滑。
  • 超大地图需层次化:否则内存和预处理压力大。

五、工程实践

1. 预处理优化

  • 连通区域预计算:提前判断起点终点是否可达,避免无效寻路。
  • 跳点数据烘焙:JPS+核心,查表加速。
  • 位图存储:用bitmask存储障碍,节省空间,便于位运算。

2. 路径后处理(平滑)

  • Floyd算法:让路径更自然,去掉多余拐点。
  • 示例代码(Python伪代码,C#同理):
optimized_path = [path[0]]
i = 0
while i < len(path) - 1:
    j = i + 1
    while j < len(path) and can_reach_direct(optimized_path[-1], path[j]):
        j += 1
    optimized_path.append(path[j-1])
    i = j - 1
  • 形象理解:像“拉直绳子”,能直走就不拐弯。

总结形象比喻

  • JPS就像“聪明的快递员”,不在每个路口都停,而是一路飞奔到必须拐弯的地方才停下,极大减少了“问路”的次数。
  • 优化策略让快递员更聪明(用望远镜、记笔记、查表),但有的策略(如JPS+)不适合经常变动的城市(动态地图)。
  • 工程实践中,JPS适合“规则、变化不大、寻路频繁”的场景,配合路径平滑算法,能让AI走得又快又自然。

下面给出Jump Point Search(JPS)算法的C#完整实现,包括跳点检测(Jump Point Detection)核心代码。JPS是A*的优化变体,常用于网格寻路,极大减少了节点扩展数量。实现以4方向(上下左右)为主,如需8方向可扩展。


1. 基本数据结构

public class Node
{
    public int X, Y;
    public bool Walkable;
    public float G, H, F;
    public Node Parent;

    public Node(int x, int y, bool walkable)
    {
        X = x; Y = y; Walkable = walkable;
    }
}

2. JPS主流程

using System;
using System.Collections.Generic;

public class JPS
{
    private int width, height;
    private Node[,] grid;
    private static readonly int[,] DIRS = { {0,1}, {1,0}, {0,-1}, {-1,0} }; // 4方向

    public JPS(bool[,] map)
    {
        height = map.GetLength(0);
        width = map.GetLength(1);
        grid = new Node[height, width];
        for (int y = 0; y < height; y++)
            for (int x = 0; x < width; x++)
                grid[y, x] = new Node(x, y, map[y, x]);
    }

    public List<Node> FindPath(int sx, int sy, int ex, int ey)
    {
        var open = new SimplePriorityQueue<Node>();
        var closed = new HashSet<Node>();
        Node start = grid[sy, sx], end = grid[ey, ex];
        start.G = 0; start.H = Heuristic(start, end); start.F = start.H;
        open.Enqueue(start, start.F);

        while (open.Count > 0)
        {
            Node cur = open.Dequeue();
            if (cur == end)
                return ReconstructPath(end);

            closed.Add(cur);

            foreach (var dir in DIRS)
            {
                Node jumpNode = Jump(cur.X, cur.Y, dir[0], dir[1], ex, ey);
                if (jumpNode == null || closed.Contains(jumpNode)) continue;

                float ng = cur.G + Heuristic(cur, jumpNode);
                if (!open.Contains(jumpNode) || ng < jumpNode.G)
                {
                    jumpNode.G = ng;
                    jumpNode.H = Heuristic(jumpNode, end);
                    jumpNode.F = jumpNode.G + jumpNode.H;
                    jumpNode.Parent = cur;
                    if (!open.Contains(jumpNode))
                        open.Enqueue(jumpNode, jumpNode.F);
                    else
                        open.UpdatePriority(jumpNode, jumpNode.F);
                }
            }
        }
        return null; // no path
    }

    private List<Node> ReconstructPath(Node end)
    {
        var path = new List<Node>();
        Node cur = end;
        while (cur != null)
        {
            path.Add(cur);
            cur = cur.Parent;
        }
        path.Reverse();
        return path;
    }

    private float Heuristic(Node a, Node b)
    {
        // 曼哈顿距离
        return Math.Abs(a.X - b.X) + Math.Abs(a.Y - b.Y);
    }

    private bool InBounds(int x, int y)
    {
        return x >= 0 && x < width && y >= 0 && y < height;
    }

    private bool Walkable(int x, int y)
    {
        return InBounds(x, y) && grid[y, x].Walkable;
    }

3. 跳点检测(Jump Point Detection)核心

    // dx, dy为方向(如(1,0)右,(0,1)下)
    private Node Jump(int x, int y, int dx, int dy, int ex, int ey)
    {
        int nx = x + dx, ny = y + dy;
        if (!Walkable(nx, ny)) return null;
        if (nx == ex && ny == ey) return grid[ny, nx];

        // 检查强制邻居(forced neighbor)
        // 4方向下,只有直行时,若左右有障碍且旁边可走,则为跳点
        if (dx != 0)
        {
            // 水平移动,检测上下
            if ((Walkable(nx, ny + 1) && !Walkable(x, y + 1)) ||
                (Walkable(nx, ny - 1) && !Walkable(x, y - 1)))
            {
                return grid[ny, nx];
            }
        }
        else if (dy != 0)
        {
            // 垂直移动,检测左右
            if ((Walkable(nx + 1, ny) && !Walkable(x + 1, y)) ||
                (Walkable(nx - 1, ny) && !Walkable(x - 1, y)))
            {
                return grid[ny, nx];
            }
        }

        // 递归向前跳
        return Jump(nx, ny, dx, dy, ex, ey);
    }
}

优先队列实现说明

JPS需要一个支持按F值优先出队的优先队列。你可以用第三方库(如 Priority Queue for C#),或者简单实现如下:

public class SimplePriorityQueue<T>
{
    private List<(T item, float priority)> list = new List<(T, float)>();

    public int Count => list.Count;

    public void Enqueue(T item, float priority)
    {
        list.Add((item, priority));
    }

    public T Dequeue()
    {
        int minIdx = 0;
        float minPriority = list[0].priority;
        for (int i = 1; i < list.Count; i++)
        {
            if (list[i].priority < minPriority)
            {
                minPriority = list[i].priority;
                minIdx = i;
            }
        }
        var result = list[minIdx].item;
        list.RemoveAt(minIdx);
        return result;
    }

    public bool Contains(T item)
    {
        return list.Exists(x => EqualityComparer<T>.Default.Equals(x.item, item));
    }

    public void UpdatePriority(T item, float newPriority)
    {
        for (int i = 0; i < list.Count; i++)
        {
            if (EqualityComparer<T>.Default.Equals(list[i].item, item))
            {
                list[i] = (item, newPriority);
                break;
            }
        }
    }
}

用法举例

// 假设map为bool[,],true为可走,false为障碍
bool[,] map = new bool[5,5] {
    { true, true, true, true, true },
    { true, false, false, false, true },
    { true, true, true, true, true },
    { true, false, false, false, true },
    { true, true, true, true, true }
};

JPS jps = new JPS(map);
var path = jps.FindPath(0, 0, 4, 4);
if (path != null)
{
    foreach (var node in path)
        Console.WriteLine($"({node.X},{node.Y})");
}
else
{
    Console.WriteLine("No path found.");
}

说明与扩展

  • 该实现为4方向JPS,如需8方向(斜对角),需扩展DIRS和Jump函数的斜向逻辑。
  • 跳点检测的核心思想是:遇到强制邻居或到达终点时返回跳点,否则递归前进
  • 若需更高性能,建议用二叉堆实现优先队列。
  • 若需可视化或调试,可在Jump函数中打印跳点过程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值