
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_astar |
A* 主循环,含逐帧动画 |
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>事件,支持按住左键拖动连续放置障碍物