用python演示A星算法

用python演示A星算法

周日 5月 03 2026
1830 字 · 11 分钟
未分配分类 未分配标签

A*(A-Star)是游戏开发、机器人路径规划、地图导航等领域中最经典的启发式搜索算法之一。本文通过一个基于 Tkinter 的交互式网格演示程序,直观展示 A* 算法逐步探索的过程——无需安装任何第三方库,开箱即用。

运行环境:Python 3.x(标准库,无额外依赖)
核心模块tkinter(GUI)· heapq(优先队列)· time(动画延迟)


一、功能概览

本演示程序包含以下核心功能:

  • 可交互的 20×20 网格地图
  • 自定义起点终点
  • 自由放置障碍物
  • 逐帧可视化 A* 搜索过程(Open Set / Closed Set 实时着色)
  • 搜索完成后高亮显示最终最短路径

二、交互说明

鼠标操作

程序启动后,通过鼠标左键在网格上依次点击:

点击顺序效果颜色
第 1 次点击设置起点🟢 绿色
第 2 次点击设置终点🔴 红色
第 3 次及之后放置障碍物⬛ 黑色

搜索过程颜色说明

点击”开始 A*“按钮后,算法开始执行,网格颜色实时变化:

颜色含义
🔵 蓝色(Deep Sky Blue)Open Set:已发现但尚未展开的节点
🟡 黄色(Gold)Closed Set:已完成评估的节点
🟣 紫色最终最短路径

💡 通过观察蓝色(待探索)和黄色(已探索)区域的扩展方式,可以直观理解 A* 如何利用启发函数引导搜索方向,与 Dijkstra 的”向四周均匀扩散”形成鲜明对比。


三、A* 算法核心原理

3.1 核心公式

A* 算法的核心在于对每个节点计算一个综合评分 f(n),并以此作为优先队列的排序依据:

f(n) = g(n) + h(n)
各项含义
符号名称说明
g(n)实际代价从起点到当前节点 n 的已知最短路径长度
h(n)启发代价从当前节点 n 到终点的估算距离(启发函数)
f(n)综合评分两者之和,值越小优先级越高

3.2 启发函数的选择

本示例使用曼哈顿距离(Manhattan Distance)作为启发函数:

h(n) = |n.row - end.row| + |n.col - end.col|

曼哈顿距离适用于只允许上下左右四方向移动的网格(不允许斜向移动)。若允许斜向移动,则应改用切比雪夫距离(Chebyshev Distance)或欧几里得距离

常见启发函数对比
启发函数适用场景特点
曼哈顿距离四方向网格不可超估(admissible),结果最优
切比雪夫距离八方向网格允许斜向移动时更准确
欧几里得距离连续空间适合无网格限制的路径规划

启发函数必须满足可采纳性(Admissibility):即对真实代价的估算永远不超过实际值。满足此条件,A* 算法才能保证找到最优路径。

3.3 算法执行流程

1. 将起点加入 Open Set,初始化 g(start) = 0
2. 循环:
a. 从 Open Set 中取出 f(n) 最小的节点 current
b. 若 current == 终点,回溯路径,算法结束
c. 将 current 移入 Closed Set(已探索)
d. 对 current 的每个邻居 neighbor:
- 跳过障碍物和已在 Closed Set 中的节点
- 计算 tentative_g = g(current) + 移动代价
- 若 tentative_g 更优,更新 came_from、g、f,加入 Open Set
3. Open Set 为空仍未到达终点 → 无可行路径

四、完整代码

4.1 代码结构说明

程序以单一类 AStarGUI 组织所有逻辑,职责划分如下:

方法职责
__init__初始化窗口、画布、网格状态、事件绑定
draw_grid重绘整个网格(初始化或重置时调用)
on_click处理鼠标点击,依次设置起点、终点、障碍物
heuristic计算曼哈顿距离启发值
neighbors生成当前节点的有效邻居(排除越界和障碍物)
run_astarA* 主循环,含逐帧动画
reconstruct_path从终点回溯 came_from 字典,绘制最终路径
draw_cell以指定颜色绘制单个格子

4.2 完整源码

import tkinter as tk
import heapq
import time
ROWS = 20
COLS = 20
CELL_SIZE = 30
# 颜色定义
COLORS = {
"empty": "white",
"wall": "black",
"start": "green",
"end": "red",
"open": "deepskyblue", # Open Set:待探索
"closed": "gold", # Closed Set:已探索
"path": "purple", # 最终路径
}
class AStarGUI:
def __init__(self, root):
self.root = root
self.root.title("A* 算法演示")
self.canvas = tk.Canvas(
root,
width=COLS * CELL_SIZE,
height=ROWS * CELL_SIZE,
bg="white"
)
self.canvas.pack()
self.grid = [[0 for _ in range(COLS)] for _ in range(ROWS)]
self.start = None
self.end = None
self.canvas.bind("<Button-1>", self.on_click)
btn = tk.Button(root, text="开始 A*", command=self.run_astar)
btn.pack(pady=5)
self.draw_grid()
def draw_grid(self):
"""重绘整个网格"""
self.canvas.delete("all")
for r in range(ROWS):
for c in range(COLS):
x1, y1 = c * CELL_SIZE, r * CELL_SIZE
x2, y2 = x1 + CELL_SIZE, y1 + CELL_SIZE
color = COLORS["empty"]
if self.grid[r][c] == 1:
color = COLORS["wall"]
elif self.start == (r, c):
color = COLORS["start"]
elif self.end == (r, c):
color = COLORS["end"]
self.canvas.create_rectangle(
x1, y1, x2, y2,
fill=color, outline="gray"
)
def on_click(self, event):
"""依次设置起点 → 终点 → 障碍物"""
r = event.y // CELL_SIZE
c = event.x // CELL_SIZE
if r >= ROWS or c >= COLS:
return
if not self.start:
self.start = (r, c)
elif not self.end:
self.end = (r, c)
else:
self.grid[r][c] = 1 # 标记为障碍物
self.draw_grid()
def heuristic(self, a, b):
"""曼哈顿距离启发函数"""
return abs(a[0] - b[0]) + abs(a[1] - b[1])
def neighbors(self, node):
"""生成四方向有效邻居(排除越界和障碍物)"""
r, c = node
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nr, nc = r + dr, c + dc
if 0 <= nr < ROWS and 0 <= nc < COLS:
if self.grid[nr][nc] == 0:
yield (nr, nc)
def run_astar(self):
"""A* 主循环,含逐帧动画"""
if not self.start or not self.end:
return
# 初始化数据结构
open_set = []
heapq.heappush(open_set, (0, self.start))
came_from = {}
g_score = {self.start: 0}
f_score = {self.start: self.heuristic(self.start, self.end)}
open_nodes = {self.start}
while open_set:
_, current = heapq.heappop(open_set)
open_nodes.discard(current)
# 到达终点,回溯并绘制路径
if current == self.end:
self.reconstruct_path(came_from, current)
return
# 标记为已探索(Closed Set)
r, c = current
self.draw_cell(r, c, COLORS["closed"])
self.root.update()
time.sleep(0.02)
for neighbor in self.neighbors(current):
tentative_g = g_score[current] + 1 # 均匀代价网格,每步代价为 1
if tentative_g < g_score.get(neighbor, float("inf")):
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + self.heuristic(neighbor, self.end)
if neighbor not in open_nodes:
heapq.heappush(open_set, (f_score[neighbor], neighbor))
open_nodes.add(neighbor)
nr, nc = neighbor
self.draw_cell(nr, nc, COLORS["open"])
print("未找到路径")
def reconstruct_path(self, came_from, current):
"""从终点回溯 came_from,逐步绘制最短路径"""
while current in came_from:
current = came_from[current]
if current != self.start:
r, c = current
self.draw_cell(r, c, COLORS["path"])
self.root.update()
time.sleep(0.05)
def draw_cell(self, r, c, color):
"""以指定颜色绘制单个格子"""
x1, y1 = c * CELL_SIZE, r * CELL_SIZE
x2, y2 = x1 + CELL_SIZE, y1 + CELL_SIZE
self.canvas.create_rectangle(
x1, y1, x2, y2,
fill=color, outline="gray"
)
if __name__ == "__main__":
root = tk.Tk()
app = AStarGUI(root)
root.mainloop()

五、延伸阅读

如果你希望进一步扩展或深入理解这个示例,以下方向可供参考:

  • 支持斜向移动:在 neighbors() 中加入四个对角方向(dr, dc±1, ±1),并将移动代价改为 √2 ≈ 1.414
  • 加权地形:将网格值从二值(0/1)改为权重值,g_score 累加时乘以地形权重,模拟沼泽、山地等地形
  • 双向 A*:同时从起点和终点各发起一次搜索,两个搜索波前相遇时结束,可显著减少探索节点数
  • 实时拖拽障碍物:绑定 <B1-Motion> 事件,支持按住左键拖动连续放置障碍物

参考资料


Thanks for reading!

用python演示A星算法

周日 5月 03 2026
1830 字 · 11 分钟
未分配分类 未分配标签

© bayview | CC BY-NC-SA 4.0