A*算法 和 最佳优先搜索算法(Best-First-Search)

BFS算法

算法原理

最佳优先搜索算法是一种启发式搜索算法(Heuristic Algorithm),其基于广度优先搜索算法,不同点是其依赖于估价函数对将要遍历的节点进行估价,选择代价小的节点进行遍历,直到找到目标点为止。BFS算法不能保证找到的路径是一条最短路径,但是其计算过程相对于Dijkstra
算法会快很多

算法流程

  • 算法实现需要有两个优先队列Open和Closed,Open队列用来存放未遍历并将要被遍历的节点;Closed队列用来存放已经遍历过的节点。
  • 初始时将根节点放入Open队列中,此时Closed队列为空。
  • 开启算法循环(遍历代价最小的节点的所有子节点)
  • 1、 从Open队列中选出一个优先级最高的节点X(根据估价函数计算出来的),并生成子节点Y。
  • a. 如果Y不在Open队列和Closed队列中时(未被遍历过),通过估价函数计算出Y节点的估价,并将Y节点添加到Open队列中,并根据估价对Open队列中的节点进行排序。

  • b. 如果Y节点在Open队列中时,通过估价函数重新计算出Y节点的估价,并和Open队列中的Y节点的估价进行比较,如果估计高于Open队列中的Y节点的估价,替换Open队列中的Y节点的估价,并对Open队列中的节点重新排序。

  • c. 如果Y节点在Closed队列中时,通过估价函数计算出Y节点的估值,并和Closed队列中的Y节点的估价进行比较,如果高于Closed队列中的估价时,将Y节点从Closed队列中移除,并添加到Open队列中,并对Open队列进行排序。

  • 2、将节点X从Open队列中移除并添加到Closed队列中(X节点就应该是路径上的一个点,通过从最终点向上找父节点查到完整的路径)。
  • 3、循环上序操作直到找到目标点(算法找到目标点)或则Open队列中没有节点为止(算法未能找到结果)。

估价函数(或叫启发函数)的实现原理

最佳优先搜索是一种启发式搜索算法。广度优先搜索和深度优先搜索都属于穷举类型的搜索,需要依次遍历所有的节点,当空间非常大的时候,这种方式的效率就会非常差。而启发式的搜索是对状态控件中的每个点进行评估,然后选出最好的位置。

启发估价函数公式为:

  • BFS算法只关注当前点到目标点之间的代价:

    f(n) = h(n)

n表示当前的点,g(n)为从起始点到点n的实际代价,h(n)为从点n到目标点的估价。

BFS算法的探测过程

  • BFS没有障碍物时的寻路
BFS算法的探测过程
  • BFS遇到障碍物时的寻路
BFS遇到障碍物时的寻路方式

(图片来源于网络)

  • BFS算法通过估价函数,会将探测快速的导向目标点的方向,其不能够保证寻找到一条最短路径的点,但是其搜索的效率上相对于Dijestra算法会快上很多。
  • 在地图上有障碍物的情况,BFS寻找的路径一般都不是最短路径,在寻路过程中可以尝试配合其他的方法,对寻路进行修正。

A*算法

算法介绍

A*算法将BFS算法和Dijkstra算法结合在一起,结合两算法的优点,既可以查找最短路径的,有拥有和BFS差不多的效率。

  • A星算法会结合从出发点到当前点的实际代价和当前点到目标点之间的总代价:

    f(n) = g(n) + h(n)

  • 一种极端情况,如果h(n)是0,则只有g(n)起作用,此时A*演变成Dijkstra算法,这保证能找到最短路径。
  • 如果h(n)经常都比从n移动到目标的实际代价小(或者相等),则A保证能找到一条最短路径。h(n)越小,A扩展的结点越多,运行就得越慢。
  • 如果h(n)精确地等于从n移动到目标的代价,则A将会仅仅寻找最佳路径而不扩展别的任何结点,这会运行得非常快。尽管这不可能在所有情况下发生,你仍可以在一些特殊情况下让它们精确地相等(译者:指让h(n)精确地等于实际值)。只要提供完美的信息,A会运行得很完美,认识这一点很好。
  • 如果h(n)有时比从n移动到目标的实际代价高,则A*不能保证找到一条最短路径,但它运行得更快。
  • 另一种极端情况,如果h(n)比g(n)大很多,则只有h(n)起作用,A*演变成BFS算法。

A*寻路的探测过程

  • A*寻路在没有障碍物时的寻路过程与BFS相同
A* 没有障碍物时的寻路
  • A* 寻路在有障碍物时寻找的路径和Dijkstra算法寻路结果相同,找到了最短的路径且效率和BFS差不多
A* 在寻路过程中遇到障碍物时的寻路过程

距离计算方式

距离距离

(图片来源于网络)

曼哈顿距离

  • 上图中的红线表示的是两个点之间的曼哈顿距离
  • 表示的是标准坐标系上的两个点的绝对轴距之和(d = |x1 - x2| + |y1 - y2|)

欧式距离(欧几里得距离)

  • 上图中的绿线表示的是欧式距离
  • 其代表的是两个点之间的直线距离(通过勾股定理计算出来的)。

A*算法的lua实现

  • 点的数据定义
--[[
--      A*算法的点的结构
 ]]
local AStarPointObj = ns.class("AStarPointObj")

function AStarPointObj:ctor()
    ns.utils.registerVariableAndSetGetFunc(self, "pointId"  ,  0)            -- 点的ID
    ns.utils.registerVariableAndSetGetFunc(self, "line"     ,  0)            -- 列
    ns.utils.registerVariableAndSetGetFunc(self, "row"      ,  0)            -- 行
    ns.utils.registerVariableAndSetGetFunc(self, "pos"      ,  ns.p(0,0))    -- 点的位置
    ns.utils.registerVariableAndSetGetFunc(self, "canReach" ,  true)         -- 该点是否可以通过

    ns.utils.registerVariableAndSetGetFunc(self, "pre"      ,  nil)          -- 前一个节点(在移动的过程中记录上一个点、便于路径追溯)
    ns.utils.registerVariableAndSetGetFunc(self, "evaluate" ,  0)            -- 当前的估价(从起始点开始经过当前点到达目标点的总的代价的预估)
    ns.utils.registerVariableAndSetGetFunc(self, "cost"     ,  0)            -- 从起始点开始到当前点已经消耗的代价
end

--[[
--      初始化顶点数据
 ]]
function AStarPointObj:initWithParams(pointId,line,row,pos,canReach)
    self._pointId  = pointId    or 0
    self._line     = line       or 0
    self._row      = row        or 0
    self._pos      = pos        or ns.p(0,0)
    self._canReach = (canReach == nil) and false or canReach

    return self
end


return AStarPointObj
  • 寻路代码的实现
--[[
--      A*算法
 ]]

local MAX_DISTANCE  = 999999999999999
local AStarCommand = ns.class("AStarCommand")

--[[
--      @searchType 0:曼哈顿距离; 1:欧几里德距离
 ]]
function AStarCommand:ctor(pointMap,searchType)
    self._pointMap      = pointMap or {}
    self._searchType    = searchType or 0
end


function AStarCommand:find(beginPointId,endPointId)
    local timeCommand = app.command.timeRecordCommand()

    self:resetPoint()

    local openedList    = {}    -- 优先队列,记录将要处理的点,每次从队列中查找估值最小的点
    local openedKeyMap  = {}    -- openList中的点的索引的map,避免相同的点被多次添加到openedList中
    local closedMap     = {}    -- map,记录已经处理过的点、避免同一个点被反复查询

    -- 对OpenedList列表排序
    local sortOpenList = function()
        table.sort(openedList,function(a,b)
            local evaluateA = a:getEvaluate()
            local evaluateB = b:getEvaluate()

            if evaluateA ~= evaluateB then
                return evaluateA < evaluateB
            end

            return a:getPointId() < b:getPointId()
        end)
    end

    local beginPoint = self:getPointObj(beginPointId)
    beginPoint:setEvaluate(self:getEvaluate(beginPointId,endPointId))
    table.insert(openedList,beginPoint)
    openedKeyMap[beginPointId] = true
    timeCommand:start("AStarCommand:find")
    while true do
        -- 对opened列表排序,选择估价最小的点
        sortOpenList()

        -- opened中没有点了,查找失败
        if #openedList <= 0 then
            ns.control.app.createView("Toast", "路径查找失败 - 1"):open()
            return {}
        end

        -- 查找到了目标点
        local firstObj = openedList[1]
        if firstObj:getPointId() == endPointId then
--            ns.control.app.createView("Toast", "寻路成功!!! "):open()
            timeCommand:stop("AStarCommand:find")
            local pointIds = {}
            local curPoint = firstObj
            while true do
                if curPoint and curPoint:getPointId() ~= beginPointId then
                    table.insert(pointIds,1,curPoint:getPointId())
                    curPoint = curPoint:getPre()
                else
                    break
                end
            end

            table.insert(pointIds,1,beginPointId)

            return pointIds
        end

        -- 将第一个点从Opened中移除并添加到closed中,将所有的和firstPoint相关的点添加到Opened列表中
        closedMap[firstObj:getPointId()] = firstObj
        table.remove(openedList,1)
        openedKeyMap[firstObj:getPointId()] = false

        -- 当前的行和列
        local curLine = firstObj:getLine()
        local curRow  = firstObj:getRow()

        -- 查找相邻的节点,并处理
        for i = -1,1 do
            for j = -1,1 do
                if (i ~= j or i ~= 0) and (self._searchType == 1 or (self._searchType == 0 and (i == 0 or j == 0))) then -- 处理欧几里德距离和曼哈顿距离
                    local newLine = curLine + i
                    local newRow  = curRow  + j
                    local newObj  = self:getPointObjByRowAndLine(newLine,newRow)
                    if newObj and newObj:getCanReach() then
                        local pointId  = newObj:getPointId()

                        -- 新节点基于当前节点的总的代价
                        local cost     = firstObj:getCost() + self:getEvaluate(firstObj:getPointId(),pointId)
                        local evaluate = cost + self:getEvaluate(pointId,endPointId)
--                        local evaluate = self:getEvaluate(pointId,endPointId)
                        if not closedMap[pointId] or newObj:getEvaluate() > evaluate then
                            -- 将该节点从closedMap中移除
                            closedMap[pointId] = nil

                            if not openedKeyMap[pointId] then
                                newObj:setEvaluate(evaluate)
                                newObj:setCost(cost)
                                newObj:setPre(firstObj)

                                table.insert(openedList,newObj)
                                openedKeyMap[pointId] = true
--                                ns.logW("添加点到OpenList列表中:"..pointId.. "估价 = "..evaluate)
                            elseif newObj:getEvaluate() > evaluate then    -- 如果该节点已经添加到OpenedList中了,比较并更新估值、父节点等信息
                                newObj:setEvaluate(evaluate)
                                newObj:setCost(cost)
                                newObj:setPre(firstObj)
--                                ns.logW("更新节点的估价:"..pointId.. "估价 = "..evaluate)
                            end
                        end
                    end
                end
            end
        end
    end
end

--[[
--      重置点位的数据
 ]]
function AStarCommand:resetPoint()
    for i = 1,#self._pointMap do
        local rows = self._pointMap[i]
        for j =1,#rows do
            local obj = rows[j]
            if obj then
                obj:setPre(nil)
                obj:setEvaluate(MAX_DISTANCE)
            end
        end
    end
end

--[[
--      估价函数(当前点到目标点的代价)
 ]]
function AStarCommand:getEvaluate(beginPointId,endPointId)
    local evaluate = self:getDistance(beginPointId,endPointId)

    return evaluate
end

--[[
--      获取两个点之间的距离两点之间的代价
 ]]
function AStarCommand:getDistance(beginPointId,endPointId)
    local beginPoint = self:getPointObj(beginPointId)
    local endPoint   = self:getPointObj(endPointId)
    if not beginPoint or not endPoint then
        return MAX_DISTANCE
    end

    local beginPos = beginPoint:getPos()
    local endPos   = endPoint:getPos()
    if self._searchType == 0 then       -- 曼哈顿距离
        local distance = math.abs(endPos.x - beginPos.x) + math.abs(endPos.y - beginPos.y)
        return distance
    elseif self._searchType == 1 then   -- 欧几里德距离(三角距离)
        return app.commonUtils.getDistance(beginPos,endPos)
    end

    return MAX_DISTANCE
end

--[[
--      获取点的数据
 ]]
function AStarCommand:getPointObj(pointId)
    for i = 1,#self._pointMap do
        local rows = self._pointMap[i]
        for j =1,#rows do
            local obj = rows[j]
            if obj and obj:getPointId() == pointId then
                return obj
            end
        end
    end

    return nil
end

--[[
--      通过行和列获取到点的数据
 ]]
function AStarCommand:getPointObjByRowAndLine(line,row)
    for i = 1,#self._pointMap do
        local rows = self._pointMap[i]
        for j =1,#rows do
            local obj = rows[j]
            if obj and obj:getLine() == line and obj:getRow() == row then
                return obj
            end
        end
    end

    return nil
end

return AStarCommand

参考文档

A*算法详解

模拟寻路的地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 谢谢你母亲。 还记得一次,我发的是烧了。
    倾城夏末阅读 166评论 0 1
  • 中午睡三十分钟就可以了。从一点四十五开始,睡到两点十五或者二十就足够了。这样晚上状态会比较好。 晚上回来先学习,然...
    静之雪阅读 245评论 1 0
  • 本人跑步两年多,参加过一些线上线下全程马拉松比赛,关注跑步的一点小感想,简单总结,希望能帮到你: 1.安全第一,任...
    每天进步1点点阅读 605评论 5 10
  • 柯爷高中的时候虽然离林俊杰越走越远,但是在我们这种理科男群里依然不落窠臼,不对,比下有余,不对,鹤立鸡群。 所以丫...
    by丸子哥阅读 186评论 0 0