云的博客 logo 云的博客

知识分享 经验总结 技术前沿

  1. Home
  2. Posts
  3. 用python演示A星算法

用python演示A星算法

Feb 1, 2026 bayview

ChatGPT Image 2026年2月1日 23_07_31.png

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> 事件,支持按住左键拖动连续放置障碍物

参考资料

  • A* Search Algorithm — Wikipedia
  • Introduction to A* — Red Blob Games(图文并茂,强烈推荐)
  • Heuristics for A* — Theory of Admissibility
  • Python heapq 模块文档 — docs.python.org
  • Tkinter 官方文档 — docs.python.org

Table of Contents

  • 一、功能概览
  • 二、交互说明
    • 鼠标操作
    • 搜索过程颜色说明
  • 三、A* 算法核心原理
    • 3.1 核心公式
    • 3.2 启发函数的选择
    • 3.3 算法执行流程
  • 四、完整代码
    • 4.1 代码结构说明
    • 4.2 完整源码
  • 五、延伸阅读
  • 参考资料

Recent Posts

  • 用python演示A星算法 Feb 1, 2026
  • 手动部署 Mattermost 到 Ubuntu Server 20.04 Apr 17, 2025
  • 在 Ubuntu Server 上部署 FTP 服务 Jul 9, 2024
  • LFS安装笔记本无线网卡驱动需要注意的几点问题 Jun 30, 2024
  • 为Typecho代码块添加一键复制按钮 Jun 28, 2024
← 手动部署 Mattermost 到 Ubuntu Server 20.04
Powered by Hugo & Explore Theme.