A*算法迷宫路径规划可视化

## 为什么选择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博客,一起探讨路径规划的奥秘!*

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值