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算法通过估价函数,会将探测快速的导向目标点的方向,其不能够保证寻找到一条最短路径的点,但是其搜索的效率上相对于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* 寻路在有障碍物时寻找的路径和Dijkstra算法寻路结果相同,找到了最短的路径且效率和BFS差不多
距离计算方式
(图片来源于网络)
曼哈顿距离
- 上图中的红线表示的是两个点之间的曼哈顿距离
- 表示的是标准坐标系上的两个点的绝对轴距之和(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