## 为什么选择A*算法?
在众多路径规划算法中,A*算法以其高效性和灵活性脱颖而出。它结合了Dijkstra算法的"准确性"和贪婪最佳优先搜索的"速度",被广泛应用于:
- 🎮 游戏AI角色导航
- 🗺️ 地图导航应用(如高德、百度地图)
- 🤖 机器人路径规划
- 📱 网络路由优化
这个可视化工具将帮助你深入理解A*算法的工作原理,观察它如何在实时探索过程中权衡"已知成本"和"估计成本",以找到最优解。
## 🚀 功能特色
- **自动生成随机迷宫**:每次都能体验不同的挑战场景
- **A*算法实时可视化**:观察搜索前沿如何动态扩展
- **详细的统计指标**:展示搜索时间、已探索节点数、路径长度等关键指标
- **可调节的演示速度**:根据需要调整算法运行速度,深入观察或快速获取结果
- **直观的视觉反馈**:通过不同颜色直观区分算法的各个环节
## 💡 教学价值
这个工具特别适合:
- 计算机科学专业学生学习路径规划算法
- 算法爱好者直观理解启发式搜索的工作原理
- 教师进行算法课程的演示教学
- 对AI和路径规划感兴趣的普通用户探索学习
## 🔧 使用方法
1. 环境准备:安装Python和Pygame库
```
pip install pygame
```
2. 运行程序:
```
python a_star_maze.py
```
import pygame
import heapq
import random
import time
import math
import sys
# 初始化pygame
pygame.init()
# 定义颜色
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
PURPLE = (128, 0, 128)
ORANGE = (255, 165, 0)
GRAY = (128, 128, 128)
LIGHT_BLUE = (173, 216, 230)
LIGHT_GREEN = (144, 238, 144)
DARK_GRAY = (50, 50, 50)
BG_COLOR = (240, 240, 240)
# 设置屏幕大小和网格参数
SCREEN_WIDTH = 1000 # 增加宽度,为信息面板留出空间
SCREEN_HEIGHT = 700 # 增加高度,为按钮和统计信息留出空间
GRID_SIZE = 20
ROWS = 30 # 固定行数
COLS = 40 # 固定列数
GRID_HEIGHT = ROWS * GRID_SIZE
GRID_WIDTH = COLS * GRID_SIZE
BUTTON_HEIGHT = 50
INFO_PANEL_WIDTH = 200 # 信息面板宽度
# 设置窗口
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("A*算法路径规划演示")
# 设置字体
pygame.font.init()
title_font = pygame.font.SysFont('SimHei', 36)
font = pygame.font.SysFont('SimHei', 24)
small_font = pygame.font.SysFont('SimHei', 20)
class Button:
def __init__(self, x, y, width, height, text, color, hover_color):
self.rect = pygame.Rect(x, y, width, height)
self.text = text
self.color = color
self.hover_color = hover_color
self.current_color = color
def draw(self, screen):
# 绘制按钮
pygame.draw.rect(screen, self.current_color, self.rect)
pygame.draw.rect(screen, BLACK, self.rect, 2)
# 绘制文本
text_surface = font.render(self.text, True, BLACK)
text_rect = text_surface.get_rect(center=self.rect.center)
screen.blit(text_surface, text_rect)
def is_over(self, pos):
return self.rect.collidepoint(pos)
def update(self, mouse_pos):
if self.is_over(mouse_pos):
self.current_color = self.hover_color
else:
self.current_color = self.color
class Node:
def __init__(self, row, col):
self.row = row
self.col = col
self.x = col * GRID_SIZE
self.y = row * GRID_SIZE
self.color = WHITE
self.neighbors = []
self.g_score = float('inf')
self.f_score = float('inf')
self.parent = None
self.is_wall = False
def get_pos(self):
return self.row, self.col
def is_closed(self):
return self.color == RED
def is_open(self):
return self.color == GREEN
def is_barrier(self):
return self.is_wall
def is_start(self):
return self.color == ORANGE
def is_end(self):
return self.color == BLUE
def reset(self):
self.color = WHITE
self.is_wall = False
def make_start(self):
self.color = ORANGE
def make_end(self):
self.color = BLUE
def make_barrier(self):
self.color = BLACK
self.is_wall = True
def make_open(self):
self.color = GREEN
def make_closed(self):
self.color = RED
def make_path(self):
self.color = PURPLE
def draw(self, screen):
pygame.draw.rect(screen, self.color, (self.x, self.y, GRID_SIZE, GRID_SIZE))
pygame.draw.rect(screen, GRAY, (self.x, self.y, GRID_SIZE, GRID_SIZE), 1)
def update_neighbors(self, grid):
self.neighbors = []
# 检查下方的节点
if self.row < ROWS - 1 and not grid[self.row + 1][self.col].is_barrier():
self.neighbors.append(grid[self.row + 1][self.col])
# 检查上方的节点
if self.row > 0 and not grid[self.row - 1][self.col].is_barrier():
self.neighbors.append(grid[self.row - 1][self.col])
# 检查右方的节点
if self.col < COLS - 1 and not grid[self.row][self.col + 1].is_barrier():
self.neighbors.append(grid[self.row][self.col + 1])
# 检查左方的节点
if self.col > 0 and not grid[self.row][self.col - 1].is_barrier():
self.neighbors.append(grid[self.row][self.col - 1])
def __lt__(self, other):
return False
# 创建网格
def make_grid():
grid = []
for i in range(ROWS):
grid.append([])
for j in range(COLS):
node = Node(i, j)
grid[i].append(node)
return grid
# 绘制网格
def draw_grid(screen, grid, buttons=None, stats=None):
screen.fill(BG_COLOR)
# 绘制网格背景
grid_rect = pygame.Rect(0, 0, GRID_WIDTH, GRID_HEIGHT)
pygame.draw.rect(screen, WHITE, grid_rect)
# 绘制网格节点
for row in grid:
for node in row:
node.draw(screen)
# 绘制按钮
if buttons:
for button in buttons:
button.draw(screen)
# 绘制信息面板背景
info_panel_rect = pygame.Rect(GRID_WIDTH, 0, INFO_PANEL_WIDTH, SCREEN_HEIGHT)
pygame.draw.rect(screen, LIGHT_BLUE, info_panel_rect)
pygame.draw.rect(screen, DARK_GRAY, info_panel_rect, 2)
# 绘制标题
title_text = title_font.render("A*算法演示", True, BLACK)
screen.blit(title_text, (GRID_WIDTH + 20, 20))
# 绘制图例
legend_y = 80
legend_items = [
("起点", ORANGE),
("终点", BLUE),
("墙壁", BLACK),
("待探索", GREEN),
("已探索", RED),
("最短路径", PURPLE)
]
for text, color in legend_items:
# 绘制颜色方块
pygame.draw.rect(screen, color, (GRID_WIDTH + 20, legend_y, 20, 20))
pygame.draw.rect(screen, BLACK, (GRID_WIDTH + 20, legend_y, 20, 20), 1)
# 绘制文字
legend_text = font.render(text, True, BLACK)
screen.blit(legend_text, (GRID_WIDTH + 50, legend_y))
legend_y += 30
# 绘制统计信息
if stats:
stats_y = 280
stats_title = font.render("搜索统计", True, BLACK)
screen.blit(stats_title, (GRID_WIDTH + 20, stats_y))
stats_y += 30
for label, value in stats.items():
stat_text = small_font.render(f"{label}: {value}", True, BLACK)
screen.blit(stat_text, (GRID_WIDTH + 20, stats_y))
stats_y += 25
pygame.display.update()
# 启发式函数 - 曼哈顿距离
def h(p1, p2):
x1, y1 = p1
x2, y2 = p2
return abs(x1 - x2) + abs(y1 - y2)
# A*算法
def a_star(draw, grid, start, end, stop_event):
count = 0
open_set = []
heapq.heappush(open_set, (0, count, start))
start.g_score = 0
start.f_score = h(start.get_pos(), end.get_pos())
open_set_hash = {start}
while len(open_set) > 0 and not stop_event[0]:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
current = heapq.heappop(open_set)[2]
open_set_hash.remove(current)
if current == end:
# 重建路径
reconstruct_path(current, draw, stop_event)
end.make_end()
start.make_start()
return True
for neighbor in current.neighbors:
temp_g_score = current.g_score + 1
if temp_g_score < neighbor.g_score:
neighbor.parent = current
neighbor.g_score = temp_g_score
neighbor.f_score = temp_g_score + h(neighbor.get_pos(), end.get_pos())
if neighbor not in open_set_hash:
count += 1
heapq.heappush(open_set, (neighbor.f_score, count, neighbor))
open_set_hash.add(neighbor)
neighbor.make_open()
draw()
if current != start:
current.make_closed()
return False
# 重建路径
def reconstruct_path(current, draw, stop_event):
while current.parent and not stop_event[0]:
current = current.parent
current.make_path()
draw()
# 随机生成迷宫
def generate_maze(grid, start, end):
# 先将所有节点设为墙
for row in grid:
for node in row:
if node != start and node != end:
node.make_barrier()
# 使用深度优先搜索生成迷宫
stack = [(start.row, start.col)]
visited = {(start.row, start.col)}
while stack:
current_row, current_col = stack[-1]
# 获取所有可能的邻居(间隔一个格子)
neighbors = []
directions = [(0, 2), (2, 0), (0, -2), (-2, 0)] # 右、下、左、上
for dr, dc in directions:
nr, nc = current_row + dr, current_col + dc
if 0 <= nr < ROWS and 0 <= nc < COLS and (nr, nc) not in visited:
neighbors.append((nr, nc, (current_row + nr) // 2, (current_col + nc) // 2))
if not neighbors:
stack.pop()
continue
# 随机选择一个邻居
next_row, next_col, wall_row, wall_col = random.choice(neighbors)
# 移除中间的墙
grid[wall_row][wall_col].reset()
grid[next_row][next_col].reset()
visited.add((next_row, next_col))
stack.append((next_row, next_col))
# 重置函数
def reset_grid():
grid = make_grid()
# 重新设置起点和终点
start = grid[1][1]
start.make_start()
end = grid[ROWS-2][COLS-2]
end.make_end()
# 重新生成迷宫
generate_maze(grid, start, end)
return grid, start, end
# 主函数
def main():
# 创建按钮
start_button = Button(50, GRID_HEIGHT + 10, 150, 40, "开始搜索", LIGHT_GREEN, GREEN)
reset_button = Button(220, GRID_HEIGHT + 10, 150, 40, "重置迷宫", LIGHT_BLUE, BLUE)
speed_up_button = Button(390, GRID_HEIGHT + 10, 80, 40, "加速", YELLOW, (255, 215, 0))
speed_down_button = Button(490, GRID_HEIGHT + 10, 80, 40, "减速", ORANGE, (255, 140, 0))
buttons = [start_button, reset_button, speed_up_button, speed_down_button]
grid, start, end = reset_grid()
running = True
algorithm_running = False
stop_event = [False] # 使用列表,这样可以在函数间共享状态
# 设置算法运行速度(以毫秒为单位的延迟)
delay = 30 # 默认延迟
min_delay = 1 # 最小延迟(最快)
max_delay = 100 # 最大延迟(最慢)
# 统计信息
stats = {
"搜索状态": "未开始",
"搜索时间": "0.00 秒",
"已探索节点": "0",
"待探索节点": "0",
"路径长度": "0",
"当前速度": f"{100-delay}%"
}
# 初始更新一次所有节点的邻居
for row in grid:
for node in row:
node.update_neighbors(grid)
while running:
# 获取鼠标位置
mouse_pos = pygame.mouse.get_pos()
# 更新按钮状态
for button in buttons:
button.update(mouse_pos)
# 绘制网格和按钮
draw_grid(screen, grid, buttons, stats)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 鼠标按下事件
if event.type == pygame.MOUSEBUTTONDOWN:
if start_button.is_over(mouse_pos) and not algorithm_running:
print("开始搜索按钮被点击")
algorithm_running = True
stats["搜索状态"] = "搜索中..."
# 清除旧的搜索状态
for row in grid:
for node in row:
if not node.is_barrier() and not node.is_start() and not node.is_end():
node.reset()
# 更新所有节点的邻居
for row in grid:
for node in row:
node.update_neighbors(grid)
# 重置停止事件和统计信息
stop_event[0] = False
stats["已探索节点"] = "0"
stats["待探索节点"] = "0"
stats["路径长度"] = "0"
# 运行A*算法并计时
start_time = time.time()
result = a_star_with_delay(lambda: draw_grid(screen, grid, buttons, stats),
grid, start, end, stop_event, delay, stats)
end_time = time.time()
algorithm_running = False
if result:
stats["搜索状态"] = "搜索完成"
else:
stats["搜索状态"] = "无法到达"
stats["搜索时间"] = f"{end_time - start_time:.2f} 秒"
elif reset_button.is_over(mouse_pos):
print("重置迷宫按钮被点击")
# 停止当前算法
stop_event[0] = True
algorithm_running = False
# 重置网格和统计信息
grid, start, end = reset_grid()
stats["搜索状态"] = "未开始"
stats["搜索时间"] = "0.00 秒"
stats["已探索节点"] = "0"
stats["待探索节点"] = "0"
stats["路径长度"] = "0"
draw_grid(screen, grid, buttons, stats)
elif speed_up_button.is_over(mouse_pos):
print("加速按钮被点击")
delay = max(min_delay, delay - 5)
stats["当前速度"] = f"{100-delay}%"
elif speed_down_button.is_over(mouse_pos):
print("减速按钮被点击")
delay = min(max_delay, delay + 5)
stats["当前速度"] = f"{100-delay}%"
# 键盘事件
if event.type == pygame.KEYDOWN:
print(f"按键被按下: {event.key}")
# 空格键开始搜索
if event.key == pygame.K_SPACE and not algorithm_running:
print("空格键被按下,开始搜索")
algorithm_running = True
stats["搜索状态"] = "搜索中..."
# 清除旧的搜索状态
for row in grid:
for node in row:
if not node.is_barrier() and not node.is_start() and not node.is_end():
node.reset()
# 更新所有节点的邻居
for row in grid:
for node in row:
node.update_neighbors(grid)
# 重置停止事件和统计信息
stop_event[0] = False
stats["已探索节点"] = "0"
stats["待探索节点"] = "0"
stats["路径长度"] = "0"
# 运行A*算法并计时
start_time = time.time()
result = a_star_with_delay(lambda: draw_grid(screen, grid, buttons, stats),
grid, start, end, stop_event, delay, stats)
end_time = time.time()
algorithm_running = False
if result:
stats["搜索状态"] = "搜索完成"
else:
stats["搜索状态"] = "无法到达"
stats["搜索时间"] = f"{end_time - start_time:.2f} 秒"
# r键重置迷宫
elif event.key == pygame.K_r:
print("r键被按下,重置迷宫")
# 停止当前算法
stop_event[0] = True
algorithm_running = False
# 重置网格和统计信息
grid, start, end = reset_grid()
stats["搜索状态"] = "未开始"
stats["搜索时间"] = "0.00 秒"
stats["已探索节点"] = "0"
stats["待探索节点"] = "0"
stats["路径长度"] = "0"
draw_grid(screen, grid, buttons, stats)
# 加速键
elif event.key == pygame.K_UP:
delay = max(min_delay, delay - 5)
stats["当前速度"] = f"{100-delay}%"
# 减速键
elif event.key == pygame.K_DOWN:
delay = min(max_delay, delay + 5)
stats["当前速度"] = f"{100-delay}%"
pygame.quit()
sys.exit()
# 带延迟的A*算法
def a_star_with_delay(draw, grid, start, end, stop_event, delay, stats):
count = 0
open_set = []
heapq.heappush(open_set, (0, count, start))
start.g_score = 0
start.f_score = h(start.get_pos(), end.get_pos())
open_set_hash = {start}
closed_count = 0
while len(open_set) > 0 and not stop_event[0]:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
current = heapq.heappop(open_set)[2]
open_set_hash.remove(current)
if current == end:
# 计算路径长度
path_length = 0
path_node = current
while path_node.parent:
path_length += 1
path_node = path_node.parent
stats["路径长度"] = str(path_length)
# 重建路径
reconstruct_path_with_delay(current, draw, stop_event, delay)
end.make_end()
start.make_start()
return True
for neighbor in current.neighbors:
temp_g_score = current.g_score + 1
if temp_g_score < neighbor.g_score:
neighbor.parent = current
neighbor.g_score = temp_g_score
neighbor.f_score = temp_g_score + h(neighbor.get_pos(), end.get_pos())
if neighbor not in open_set_hash:
count += 1
heapq.heappush(open_set, (neighbor.f_score, count, neighbor))
open_set_hash.add(neighbor)
neighbor.make_open()
# 更新统计信息
if current != start:
current.make_closed()
closed_count += 1
stats["已探索节点"] = str(closed_count)
stats["待探索节点"] = str(len(open_set))
draw()
# 添加延迟以控制算法运行速度
pygame.time.delay(delay)
return False
# 带延迟的路径重建
def reconstruct_path_with_delay(current, draw, stop_event, delay):
while current.parent and not stop_event[0]:
current = current.parent
current.make_path()
draw()
# 添加延迟以控制路径显示速度
pygame.time.delay(delay)
if __name__ == "__main__":
main()
3. 交互控制:
- 点击"开始搜索"按钮或按空格键:启动A*算法搜索
- 点击"重置迷宫"按钮或按R键:生成新的随机迷宫
- 点击"加速"/"减速"按钮或按上/下方向键:调整算法运行速度
- 右侧面板实时显示搜索统计数据
## 🎨 界面图例
- 🟠 橙色:起点位置
- 🔵 蓝色:终点位置
- ⬛ 黑色:墙壁/障碍物
- 🟢 绿色:待探索的节点(开放列表)
- 🔴 红色:已探索过的节点(关闭列表)
- 🟣 紫色:找到的最优路径
## 📚 A*算法原理解析
A*算法是一种启发式搜索算法,它通过评估函数f(n)来确定搜索方向:
```
f(n) = g(n) + h(n)
```
其中:
- **g(n)**:从起点到当前节点n的实际代价
- **h(n)**:从节点n到目标的估计代价(启发式函数)
A*算法的巧妙之处在于它的启发式函数。在本实现中,我们使用了**曼哈顿距离**作为启发式函数,它计算从当前位置到目标的水平和垂直距离之和,非常适合我们的网格迷宫场景。
每一步,A*算法都会:
1. 从开放列表中选择f值最小的节点
2. 将该节点移至关闭列表
3. 检查该节点的所有邻居
4. 对每个邻居计算新的g值和f值
5. 如果找到更优的路径,更新邻居节点的父节点
这个过程会一直持续,直到找到目标或确定无解。
## 🧠 与其他算法的比较
A*算法相比其他路径规划算法有什么优势?
- **vs. 广度优先搜索(BFS)**:BFS保证找到最短路径,但会探索所有方向,效率较低
- **vs. Dijkstra算法**:当启发式函数h(n)=0时,A*等同于Dijkstra算法
- **vs. 贪婪最佳优先**:贪婪算法只考虑h(n),速度快但可能找不到最优解
- **vs. 双向搜索**:A*可以扩展为双向搜索,进一步提高效率
## 🧩 迷宫生成算法
本项目使用改进的深度优先搜索(DFS)算法生成迷宫,确保每个迷宫都有解。这个算法从起点开始,随机选择未访问的相邻单元格,并"打通"它们之间的墙壁,直到所有单元格都被访问过。
## 🔍 进一步探索
如果你对路径规划算法感兴趣,可以尝试:
- 修改启发式函数,如尝试欧几里得距离或切比雪夫距离
- 实现其他路径规划算法(如D*、JPS+)进行比较
- 添加对角线移动,观察算法行为变化
- 设计更复杂的地图场景,测试算法性能
希望这个可视化工具能帮助你更好地理解A*算法的魅力,体验人工智能寻路的智慧!
---
*探索更多算法知识,欢迎关注我的CSDN博客,一起探讨路径规划的奥秘!*