目录
A * 算法精讲
- 题目链接:卡码网:126. 骑士的攻击
文章讲解:代码随想录
在象棋中,马和象的移动规则分别是“马走日”和“象走田”。现给定骑士的起始坐标和目标坐标,要求根据骑士的移动规则,计算从起点到达目标点所需的最短步数。
骑士移动规则如图,红色是起始位置,黄色是骑士可以走的地方。
棋盘大小 1000 x 1000(棋盘的 x 和 y 坐标均在 [1, 1000] 区间内,包含边界)
输入描述
第一行包含一个整数 n,表示测试用例的数量。
接下来的 n 行,每行包含四个整数 a1, a2, b1, b2,分别表示骑士的起始位置 (a1, a2) 和目标位置 (b1, b2)。
输出描述
输出共 n 行,每行输出一个整数,表示骑士从起点到目标点的最短路径长度。
输入示例
6 5 2 5 4 1 1 2 2 1 1 8 8 1 1 8 7 2 1 3 3 4 6 4 6
输出示例
2 4 6 5 1 0
思路
我们看到这道题目的第一个想法就是广搜,这也是最经典的广搜类型题目。
提交后,大家会发现,超时了。
因为本题地图足够大,且 n 也有可能很大,导致有非常多的查询。
我们来看一下广搜的搜索过程,如图,红色是起点,绿色是终点,黄色是要遍历的点,最后从 起点 找到 达到终点的最短路径是棕色。
可以看出 广搜中,做了很多无用的遍历, 黄色的格子是广搜遍历到的点。
这里我们能不能让便利方向,向这终点的方向去遍历呢?
这样我们就可以避免很多无用遍历。
Astar
Astar 是一种 广搜的改良版。 有的是 Astar是 dijkstra 的改良版。
其实只是场景不同而已 我们在搜索最短路的时候, 如果是无权图(边的权值都是1) 那就用广搜,代码简洁,时间效率和 dijkstra 差不多 (具体要取决于图的稠密)
如果是有权图(边有不同的权值),优先考虑 dijkstra。
而 Astar 关键在于 启发式函数, 也就是 影响 广搜或者 dijkstra 从 容器(队列)里取元素的优先顺序。
以下,我用BFS版本的A * 来进行讲解。
在BFS中,我们想搜索,从起点到终点的最短路径,要一层一层去遍历。
如果 使用A * 的话,其搜索过程是这样的,如图,图中着色的都是我们要遍历的点。
(上面两图中 最短路长度都是8,只是走的方式不同而已)
大家可以发现 BFS 是没有目的性的 一圈一圈去搜索, 而 A * 是有方向性的去搜索。
看出 A * 可以节省很多没有必要的遍历步骤。
为了让大家可以明显看到区别,我将 BFS 和 A * 制作成可视化动图,大家可以自己看看动图,效果更好。
地址:https://kamacoder.com/tools/knight.html
那么 A * 为什么可以有方向性的去搜索,它的如何知道方向呢?
其关键在于 启发式函数。
那么启发式函数落实到代码处,如果指引搜索的方向?
在本篇开篇中给出了BFS代码,指引 搜索的方向的关键代码在这里:
int m=q.front();q.pop();
int n=q.front();q.pop();
从队列里取出什么元素,接下来就是从哪里开始搜索。
所以 启发式函数 要影响的就是队列里元素的排序!
这是影响BFS搜索方向的关键。
对队列里节点进行排序,就需要给每一个节点权值,如何计算权值呢?
每个节点的权值为F,给出公式为:F = G + H
G:起点达到目前遍历节点的距离
F:目前遍历的节点到达终点的距离
起点达到目前遍历节点的距离 + 目前遍历的节点到达终点的距离 就是起点到达终点的距离。
本题的图是无权网格状,在计算两点距离通常有如下三种计算方式:
- 曼哈顿距离,计算方式: d = abs(x1-x2)+abs(y1-y2)
- 欧氏距离(欧拉距离) ,计算方式:d = sqrt( (x1-x2)^2 + (y1-y2)^2 )
- 切比雪夫距离,计算方式:d = max(abs(x1 - x2), abs(y1 - y2))
x1, x2 为起点坐标,y1, y2 为终点坐标 ,abs 为求绝对值,sqrt 为求开根号,
选择哪一种距离计算方式 也会导致 A * 算法的结果不同。
本题,采用欧拉距离才能最大程度体现 点与点之间的距离。
所以 使用欧拉距离计算 和 广搜搜出来的最短路的节点数是一样的。 (路径可能不同,但路径上的节点数是相同的)
我在制作动画演示的过程中,分别给出了曼哈顿、欧拉以及契比雪夫 三种计算方式下,A * 算法的寻路过程,大家可以自己看看看其区别。
动画地址:https://kamacoder.com/tools/knight.html
计算出来 F 之后,按照 F 的 大小,来选去出队列的节点。
可以使用 优先级队列 帮我们排好序,每次出队列,就是F最小的节点。
方法一:广搜
import heapq
n = int(input())
moves = [(1, 2), (2, 1), (-1, 2), (2, -1), (1, -2), (-2, 1), (-1, -2), (-2, -1)]
def distance(a, b):
return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5
def bfs(start, end):
q = [(distance(start, end), start)]
step = {start: 0}
while q:
d, cur = heapq.heappop(q)
if cur == end:
return step[cur]
for move in moves:
new = (move[0] + cur[0], move[1] + cur[1])
if 1 <= new[0] <= 1000 and 1 <= new[1] <= 1000:
step_new = step[cur] + 1
if step_new < step.get(new, float('inf')):
step[new] = step_new
heapq.heappush(q, (distance(new, end) + step_new, new))
return False
for _ in range(n):
a1, a2, b1, b2 = map(int, input().split())
print(bfs((a1, a2), (b1, b2)))
方法二:Astar
import heapq
# 定义骑士的8个移动方向
dir = [(-2, -1), (-2, 1), (-1, 2), (1, 2), (2, 1), (2, -1), (1, -2), (-1, -2)]
# 启发式函数,计算当前节点到目标节点的估计代价(使用欧几里得距离的平方)
def heuristic(x1, y1, x2, y2):
return (x1 - x2) ** 2 + (y1 - y2) ** 2
# A*算法函数,返回从起点到终点的最短路径
def astar(a1, a2, b1, b2):
# 初始化棋盘,用于记录每个点的最短路径步数
moves = [[0] * 1001 for _ in range(1001)]
# 定义优先队列,初始加入起点
pq = []
heapq.heappush(pq, (0, a1, a2)) # (F值, x, y)
# 处理队列,开始A*搜索
while pq:
f, x, y = heapq.heappop(pq)
# 如果当前点已经到达目标点,返回步数
if x == b1 and y == b2:
return moves[x][y]
# 扩展当前点的8个移动方向
for dx, dy in dir:
nx, ny = x + dx, y + dy
# 判断是否越界
if 1 <= nx <= 1000 and 1 <= ny <= 1000:
if moves[nx][ny] == 0: # 如果该点还没有访问过
moves[nx][ny] = moves[x][y] + 1 # 更新步数
g = moves[nx][ny] * 5 # G值,每次移动的代价是5
h = heuristic(nx, ny, b1, b2) # 计算启发式估计值H
f = g + h # 计算F值
heapq.heappush(pq, (f, nx, ny)) # 将新状态加入优先队列
return -1 # 如果无法到达目标点
# 处理输入输出
if __name__ == "__main__":
n = int(input()) # 读取测试用例数量
for _ in range(n):
a1, a2, b1, b2 = map(int, input().split()) # 读取起点和终点坐标
if a1 == b1 and a2 == b2:
print(0) # 如果起点和终点相同,步数为0
else:
print(astar(a1, a2, b1, b2)) # 输出从起点到终点的最短步数
心得体会
A * 的缺点
大家看上述 A * 代码的时候,可以看到 我们想 队列里添加了很多节点,但真正从队列里取出来的 仅仅是 靠启发式函数判断 距离终点最近的节点。
相对了 普通BFS,A * 算法只从 队列里取出 距离终点最近的节点。
那么问题来了,A * 在一次路径搜索中,大量不需要访问的节点都在队列里,会造成空间的过度消耗。
IDA * 算法 对这一空间增长问题进行了优化,关于 IDA * 算法,本篇不再做讲解,感兴趣的录友可以自行找资料学习。
另外还有一种场景 是 A * 解决不了的。
如果题目中,给出 多个可能的目标,然后在这多个目标中 选择最近的目标,这种 A * 就不擅长了, A * 只擅长给出明确的目标 然后找到最短路径。
如果是多个目标找最近目标(特别是潜在目标数量很多的时候),可以考虑 Dijkstra ,BFS 或者 Floyd。