四、求解起点到终点的最短路径
求解程序:
#!/usr/bin/env python3
import argparse
import json
import time
from typing import List, Tuple, Dict, Optional, Set
from collections import deque
import heapq
import sys
class MazeSolver:
""" 迷宫路径求解器 """
def init(self, maze_data: List[List[int]]):
self.maze = maze_data
self.rows = len(maze_data)
self.cols = len(maze_data[0]) if self.rows > 0 else 0
# 默认起点和终点
self.start = self._find_cell(2) or (0, 0)
self.end = self._find_cell(3) or (self.rows - 1, self.cols - 1)
# 统计信息
self.stats = {
'nodes_explored': 0,
'max_queue_size': 0,
'time_taken': 0.0,
'path_length': 0
}
def _find_cell(self, value: int) -> Optional[Tuple[int, int]]:
""" 查找指定值的单元格 """
for y in range(self.rows):
for x in range(self.cols):
if self.maze[y][x] == value:
return (x, y)
return None
def is_valid(self, x: int, y: int) -> bool:
""" 检查坐标是否有效 """
return 0 <= x < self.cols and 0 <= y < self.rows
def is_passable(self, x: int, y: int) -> bool:
""" 检查位置是否可通过 """
return self.is_valid(x, y) and self.maze[y][x] != 1
def set_start_end(self, start: Tuple[int, int], end: Tuple[int, int]):
""" 设置起点和终点 """
if not (self.is_valid(*start) and self.is_passable(*start)):
raise ValueError(f" 起点 {start} 无效 ")
if not (self.is_valid(*end) and self.is_passable(*end)):
raise ValueError(f" 终点 {end} 无效 ")
self.start = start
self.end = end
def bfs(self) -> Optional[List[Tuple[int, int]]]:
"""
BFS 算法求解最短路径
Returns: 路径列表 [(x, y), ...] ,如果无解返回 None
"""
print("\n" + "="*60)
print("BFS ( 广度优先搜索 ) 算法 ")
print("="*60)
start_time = time.time()
queue = deque([(self.start, [self.start])])
visited = {self.start}
self.stats['nodes_explored'] = 0
self.stats['max_queue_size'] = 1
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] # 下、上、右、左
while queue:
self.stats['max_queue_size'] = max(self.stats['max_queue_size'], len(queue))
current, path = queue.popleft()
self.stats['nodes_explored'] += 1
# 到达终点
if current == self.end:
self.stats['time_taken'] = time.time() - start_time
self.stats['path_length'] = len(path)
print(f"[+] 找到最短路径 !")
print(f" 路径长度 : {len(path)} 步 ")
return path
# 探索邻居
for dx, dy in directions:
nx, ny = current[0] + dx, current[1] + dy
neighbor = (nx, ny)
if self.is_passable(nx, ny) and neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
self.stats['time_taken'] = time.time() - start_time
print("[-] 未找到路径 ")
return None
def astar(self, heuristic: str = 'manhattan') -> Optional[List[Tuple[int, int]]]:
"""
A* 算法求解最短路径
Args:
heuristic: 启发式函数 ('manhattan' 或 'euclidean')
Returns: 路径列表
"""
print("\n" + "="*60)
print(f"A* 算法 ( 启发式 : {heuristic})")
print("="*60)
start_time = time.time()
# 优先队列 : (f_score, g_score, position, path)
heap = [(0, 0, self.start, [self.start])]
visited = {self.start: 0} # 记录到达每个点的最小 g_score
self.stats['nodes_explored'] = 0
self.stats['max_queue_size'] = 1
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
while heap:
self.stats['max_queue_size'] = max(self.stats['max_queue_size'], len(heap))
f_score, g_score, current, path = heapq.heappop(heap)
self.stats['nodes_explored'] += 1
# 如果找到更好的路径,跳过
if current in visited and visited[current] < g_score:
continue
# 到达终点
if current == self.end:
self.stats['time_taken'] = time.time() - start_time
self.stats['path_length'] = len(path)
print(f"[+] 找到最短路径 !")
print(f" 路径长度 : {len(path)} 步 ")
print(f" 最终 f_score: {f_score:.2f}")
return path
# 探索邻居
for dx, dy in directions:
nx, ny = current[0] + dx, current[1] + dy
neighbor = (nx, ny)
if not self.is_passable(nx, ny):
continue
new_g_score = g_score + 1
# 如果找到了更好的路径
if neighbor not in visited or new_g_score < visited.get(neighbor, float('inf')):
visited[neighbor] = new_g_score
h_score = self._calculate_heuristic(neighbor, heuristic)
new_f_score = new_g_score + h_score
heapq.heappush(
heap,
(new_f_score, new_g_score, neighbor, path + [neighbor])
)
self.stats['time_taken'] = time.time() - start_time
print("[-] 未找到路径 ")
return None
def _calculate_heuristic(self, pos: Tuple[int, int], method: str) -> float:
""" 计算启发式函数值 """
if method == 'manhattan':
return abs(pos[0] - self.end[0]) + abs(pos[1] - self.end[1])
elif method == 'euclidean':
return ((pos[0] - self.end[0]) ** 2 + (pos[1] - self.end[1]) ** 2) ** 0.5
else:
return 0
def dfs(self, max_depth: int = 10000) -> Optional[List[Tuple[int, int]]]:
"""
DFS 算法求解路径
Args:
max_depth: 最大搜索深度
Returns: 路径列表
"""
print("\n" + "="*60)
print(f"DFS ( 深度优先搜索 ) 算法 ( 最大深度 : {max_depth})")
print("="*60)
start_time = time.time()
self.stats['nodes_explored'] = 0
def dfs_recursive(current: Tuple[int, int], path: List[Tuple[int, int]], visited: Set) -> Optional[List[Tuple[int, int]]]:
if current == self.end:
return path[:]
if len(path) >= max_depth:
return None
self.stats['nodes_explored'] += 1
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
for dx, dy in directions:
nx, ny = current[0] + dx, current[1] + dy
neighbor = (nx, ny)
if self.is_passable(nx, ny) and neighbor not in visited:
visited.add(neighbor)
result = dfs_recursive(neighbor, path + [neighbor], visited)
visited.remove(neighbor)
if result:
return result
return None
path = dfs_recursive(self.start, [self.start], {self.start})
self.stats['time_taken'] = time.time() - start_time
if path:
self.stats['path_length'] = len(path)
print(f"[+] 找到路径 !")
print(f" 路径长度 : {len(path)} 步 ")
else:
print("[-] 未找到路径 ")
return path
def bidirectional_bfs(self) -> Optional[List[Tuple[int, int]]]:
""" 双向 BFS 算法(从起点和终点同时搜索)
Returns: 路径列表
"""
print("\n" + "="*60)
print(" 双向 BFS 算法 ")
print("="*60)
start_time = time.time()
# 从起点开始的搜索
queue_start = deque([(self.start, [self.start])])
visited_start = {self.start: [self.start]}
# 从终点开始的搜索
queue_end = deque([(self.end, [self.end])])
visited_end = {self.end: [self.end]}
self.stats['nodes_explored'] = 0
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
def explore_step(queue, visited, other_visited):
""" 执行一步探索 """
if not queue:
return None
current, path = queue.popleft()
self.stats['nodes_explored'] += 1
# 检查是否在另一个搜索树中找到
if current in other_visited:
# 合并路径
other_path = other_visited[current]
# 注意: other_path 需要反转(如果是从终点搜索的)
if queue == queue_end:
other_path = other_path[::-1]
return path + other_path[1:]
for dx, dy in directions:
nx, ny = current[0] + dx, current[1] + dy
neighbor = (nx, ny)
if self.is_passable(nx, ny) and neighbor not in visited:
visited[neighbor] = path + [neighbor]
queue.append((neighbor, path + [neighbor]))
return None
iteration = 0
while queue_start and queue_end:
iteration += 1
self.stats['max_queue_size'] = max(self.stats['max_queue_size'],
len(queue_start) + len(queue_end))
# 交替执行
if iteration % 2 == 0:
result = explore_step(queue_start, visited_start, visited_end)
else:
result = explore_step(queue_end, visited_end, visited_start)
if result:
self.stats['time_taken'] = time.time() - start_time
self.stats['path_length'] = len(result)
print(f"[+] 找到最短路径 !")
print(f" 路径长度 : {len(result)} 步 ")
print(f" 迭代次数 : {iteration}")
return result
self.stats['time_taken'] = time.time() - start_time
print("[-] 未找到路径 ")
return None
def print_stats(self):
""" 打印统计信息 """
print("\n" + "-"*60)
print(" 统计信息 :")
print("-"*60)
print(f" 探索的节点数 : {self.stats['nodes_explored']}")
print(f" 最大队列大小 : {self.stats['max_queue_size']}")
print(f" 耗时 : {self.stats['time_taken']:.4f} 秒 ")
print(f" 路径长度 : {self.stats['path_length']} 步 ")
print(f" 每秒探索节点数 : {self.stats['nodes_explored'] / self.stats['time_taken']:.2f}")
print("-"*60)
def print_path(self, path: List[Tuple[int, int]]):
""" 打印路径 """
if not path:
print("\n[-] 无路径可显示 ")
return
print("\n" + "-"*60)
print(" 路径详情 :")
print("-"*60)
print(f" 起点 : {path[0]}")
print(f" 终点 : {path[-1]}")
print(f" 总步数 : {len(path) - 1}")
print("\n 路径坐标 ( 每 10 步显示一次 ):")
for i, pos in enumerate(path):
if i == 0 or i == len(path) - 1 or i % 10 == 0:
direction = self._get_direction(path[i-1] if i > 0 else None, pos)
print(f" 步骤 {i:3d}: ({pos[0]:3d}, {pos[1]:3d}) {direction}")
# 计算方向统计
print("\n 方向统计 :")
direction_counts = {' 上 ': 0, ' 下 ': 0, ' 左 ': 0, ' 右 ': 0}
for i in range(len(path) - 1):
direction = self._get_direction(path[i], path[i+1], chinese=False)
if direction:
direction_counts[direction] += 1
for direction, count in direction_counts.items():
if count > 0:
print(f" {direction}: {count} 次 ({count/(len(path)-1)*100:.1f}%)")
print("-"*60)
def _get_direction(self, from_pos: Optional[Tuple[int, int]],
to_pos: Tuple[int, int], chinese: bool = True) -> str:
""" 获取移动方向 """
if from_pos is None:
return " 起点 " if chinese else "Start"
dx = to_pos[0] - from_pos[0]
dy = to_pos[1] - from_pos[1]
if chinese:
if dy > 0:
return "↓ 下 "
elif dy < 0:
return "↑ 上 "
elif dx > 0:
return "→ 右 "
elif dx < 0:
return "← 左 "
else:
if dy > 0:
return " 下 "
elif dy < 0:
return " 上 "
elif dx > 0:
return " 右 "
elif dx < 0:
return " 左 "
return ""
def visualize_path(self, path: List[Tuple[int, int]], filename: str):
""" 可视化路径并保存为图像 """
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
print(" 跳过可视化生成 ")
return
print(f"\n[*] 生成可视化图像 : {filename}")
# 参数设置
cell_size = 30
padding = 10
# 计算图像尺寸
img_width = self.cols * cell_size + 2 * padding
img_height = self.rows * cell_size + 2 * padding
# 创建图像
img = Image.new('RGB', (img_width, img_height), (255, 255, 255))
draw = ImageDraw.Draw(img)
# 颜色定义
colors = {
'wall': (40, 40, 40), # 深灰色
'path': (240, 240, 240), # 浅灰色
'solution': (255, 69, 0), # 红橙色
'start': (34, 139, 34), # 森林绿
'end': (255, 215, 0), # 金色
'grid': (200, 200, 200) # 浅灰色网格
}
# 绘制迷宫
for y in range(self.rows):
for x in range(self.cols):
px = padding + x * cell_size
py = padding + y * cell_size
if self.maze[y][x] == 1:
# 墙壁
*draw.rectangle(
px, py, px + cell_size - 1, py + cell_size - 1\], fill=colors\['wall'\], outline=colors\['grid'
)
else:
#* 通路
*draw.rectangle(
px, py, px + cell_size - 1, py + cell_size - 1\], fill=colors\['path'\], outline=colors\['grid'
)
#* 绘制解路径(使用线条)
if path:
for i in range(len(path) - 1):
x1, y1 = path[i]
x2, y2 = path[i + 1]
px1 = padding + x1 * cell_size + cell_size // 2
py1 = padding + y1 * cell_size + cell_size // 2
px2 = padding + x2 * cell_size + cell_size // 2
py2 = padding + y2 * cell_size + cell_size // 2
draw.line([(px1, py1), (px2, py2)], fill=colors['solution'], width=3)
# 标记起点和终点
if path:
# 起点
*sx, sy = path[0]
px_start = padding + sx * cell_size + cell_size // 2
py_start = padding + sy * cell_size + cell_size // 2
draw.ellipse(
px_start - 8, py_start - 8, px_start + 8, py_start + 8\], fill=colors\['start'\], outline='black', width=2 ) #* *终点* *ex, ey = path\[-1
px_end = padding + ex * cell_size + cell_size // 2
py_end = padding + ey * cell_size + cell_size // 2
draw.ellipse(
px_end - 8, py_end - 8, px_end + 8, py_end + 8\], fill=colors\['end'\], outline='black', width=2 ) #* *添加图例* *legend_y = padding legend_items = \[ ('* *起点* *', colors\['start'\]), ('* *终点* *', colors\['end'\]), ('* *路径* *', colors\['solution'\]), ('* *墙壁* *', colors\['wall'\]), ('* *通路* *', colors\['path'\])
for text, color in legend_items:
px = img_width - 120
draw.rectangle([px, legend_y, px + 15, legend_y + 15], fill=color, outline='black')
draw.text((px + 20, legend_y), text, fill='black')
legend_y += 25
#* 保存图像
img.save(filename)
print(f"[+] 可视化图像已保存 : {filename}")
def load_maze_from_ascii(filename: str) -> List[List[int]]:
""" 从 ASCII 格式文件加载迷宫 """
print(f"[*] 从 ASCII 文件加载迷宫 : {filename}")
maze = []
with open(filename, 'r', encoding='utf-8') as f:
for line in f:
line = line.rstrip('\n\r')
# 跳过空行
if not line:
continue
line_stripped = line.strip()
is_comment = False
if line_stripped.startswith('#'):
has_chinese = any('\u4e00' <= c <= '\u9fff' for c in line)
has_letters = any(c.isalpha() and c not in 'SE' for c in line)
is_short_title = len(line_stripped) < 10 and line_stripped.startswith('#')
if has_chinese or has_letters or is_short_title:
is_comment = True
if is_comment:
continue
row = []
for char in line:
if char == '#':
row.append(1) # 墙壁
elif char == ' ' or char == '.':
row.append(0) # 通路
elif char == 'S':
row.append(2) # 起点
elif char == 'E':
row.append(3) # 终点
elif char == '0':
row.append(0)
elif char == '1':
row.append(1)
elif char == '2':
row.append(2)
elif char == '3':
row.append(3)
# 忽略其他字符(如制表符等)
if row:
maze.append(row)
# 统一行长度(用 0 填充)
if maze:
max_cols = max(len(row) for row in maze)
for row in maze:
while len(row) < max_cols:
row.append(0) # 用通路填充
print(f"[+] 迷宫尺寸 : {len(maze)} 行 x {len(maze[0]) if maze else 0} 列 ")
return maze
def load_maze_from_json(filename: str) -> List[List[int]]:
""" 从 JSON 格式文件加载迷宫 """
print(f"[*] 从 JSON 文件加载迷宫 : {filename}")
with open(filename, 'r', encoding='utf-8') as f:
data = json.load(f)
if isinstance(data, dict) and 'maze' in data:
maze = data['maze']
elif isinstance(data, list):
maze = data
else:
raise ValueError(" 无法识别的 JSON 格式 ")
print(f"[+] 迷宫尺寸 : {len(maze)} 行 x {len(maze[0]) if maze else 0} 列 ")
return maze
def compare_algorithms(solver: MazeSolver) -> Dict[str, any]:
""" 比较多种算法的性能 """
print("\n" + "="*60)
print(" 算法性能比较 ")
print("="*60)
results = {}
algorithms = [
('BFS', lambda: solver.bfs()),
('A* (Manhattan)', lambda: solver.astar('manhattan')),
('A* (Euclidean)', lambda: solver.astar('euclidean')),
(' 双向 BFS', lambda: solver.bidirectional_bfs()),
]
for name, func in algorithms:
print(f"\n 测试 : {name}")
path = func()
if path:
results[name] = {
'path_length': len(path),
'nodes_explored': solver.stats['nodes_explored'],
'time_taken': solver.stats['time_taken'],
'max_queue_size': solver.stats['max_queue_size']
}
# 打印比较结果
print("\n" + "="*60)
print(" 比较结果 ")
print("="*60)
print(f"{' 算法 ':<20} {' 路径长度 ':<10} {' 探索节点 ':<12} {' 耗时 (s)':<12} {' 队列大小 ':<10}")
print("-"*60)
for name, stats in results.items():
print(f"{name:<20} {stats['path_length']:<10} {stats['nodes_explored']:<12} "
f"{stats['time_taken']:<12.4f} {stats['max_queue_size']:<10}")
return results
def main():
""" 主函数 """
parser = argparse.ArgumentParser(
description=' 迷宫最短路径求解程序 ',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例 :
# 使用 BFS 求解
python standalone_path_solver.py maze.txt --algorithm bfs
# 使用 A* 求解并可视化
python standalone_path_solver.py maze.txt --algorithm astar --visualize
# 比较多种算法
python standalone_path_solver.py maze.txt --compare
# 设置自定义起点和终点
python standalone_path_solver.py maze.txt --start 0,0 --end 19,19
'''
)
parser.add_argument('maze_file', help=' 迷宫文件路径 (ASCII 或 JSON 格式 )')
parser.add_argument('--algorithm', '-a',
choices=['bfs', 'astar', 'astar-manhattan', 'astar-euclidean',
'dfs', 'bidirectional-bfs', 'bidirectional'],
default='bfs',
help=' 选择算法 ( 默认 : bfs)')
parser.add_argument('--compare', '-c', action='store_true',
help=' 比较多种算法的性能 ')
parser.add_argument('--visualize', '-v', action='store_true',
help=' 生成可视化图像 ')
parser.add_argument('--output', '-o', default='path_visualization.png',
help=' 可视化输出文件名 ( 默认 : path_visualization.png)')
parser.add_argument('--start', '-s', help=' 起点坐标,格式 : x,y ( 例如 : 0,0)')
parser.add_argument('--end', '-e', help=' 终点坐标,格式 : x,y ( 例如 : 19,19)')
parser.add_argument('--heuristic',
choices=['manhattan', 'euclidean'],
default='manhattan',
help='A* 启发式函数 ( 默认 : manhattan)')
parser.add_argument('--max-depth', type=int, default=10000,
help='DFS 最大搜索深度 ( 默认 : 10000)')
args = parser.parse_args()
print("="*60)
# 加载迷宫
try:
if args.maze_file.endswith('.json'):
maze = load_maze_from_json(args.maze_file)
else:
maze = load_maze_from_ascii(args.maze_file)
except Exception as e:
print(f"[!] 加载迷宫失败 : {e}")
sys.exit(1)
# 创建求解器
solver = MazeSolver(maze)
print(f"[+] 起点 : {solver.start}")
print(f"[+] 终点 : {solver.end}")
# 设置自定义起点和终点
if args.start:
try:
start_x, start_y = map(int, args.start.split(','))
solver.set_start_end((start_x, start_y), solver.end)
print(f"[+] 自定义起点 : ({start_x}, {start_y})")
except Exception as e:
print(f"[!] 无效的起点格式 : {e}")
sys.exit(1)
if args.end:
try:
end_x, end_y = map(int, args.end.split(','))
solver.set_start_end(solver.start, (end_x, end_y))
print(f"[+] 自定义终点 : ({end_x}, {end_y})")
except Exception as e:
print(f"[!] 无效的终点格式 : {e}")
sys.exit(1)
# 比较模式
if args.compare:
results = compare_algorithms(solver)
sys.exit(0)
# 选择算法
path = None
if args.algorithm == 'bfs':
path = solver.bfs()
elif args.algorithm == 'astar' or args.algorithm == 'astar-manhattan':
path = solver.astar('manhattan')
elif args.algorithm == 'astar-euclidean':
path = solver.astar('euclidean')
elif args.algorithm == 'dfs':
path = solver.dfs(args.max_depth)
elif args.algorithm in ['bidirectional-bfs', 'bidirectional']:
path = solver.bidirectional_bfs()
# 打印结果
if path:
solver.print_stats()
solver.print_path(path)
# 可视化
if args.visualize:
solver.visualize_path(path, args.output)
# 保存路径到文件
path_file = args.maze_file.rsplit('.', 1)[0] + '_path.txt'
with open(path_file, 'w', encoding='utf-8') as f:
f.write(f"# 路径求解结果 \n")
f.write(f"# 算法 : {args.algorithm}\n")
f.write(f"# 起点 : {solver.start}\n")
f.write(f"# 终点 : {solver.end}\n")
f.write(f"# 路径长度 : {len(path)} 步 \n\n")
f.write(" 路径坐标 :\n")
for i, pos in enumerate(path):
f.write(f"{i} ({pos[0]}, {pos[1]})\n")
print(f"\n[+] 路径已保存到 : {path_file}")
else:
print("\n[-] 无法找到从起点到终点的路径 ")
if name == 'main':
main()
最短路径搜素计算方法:
- BFS(广度优先搜索)
- 过程:从起点开始,逐层向外扩展,记录访问标记和路径。
- A* 算法
-
代价函数:f(n) = g(n) + h(n)
-
g(n):从起点到当前节点的实际步数。
-
h(n):启发函数,可选曼哈顿距离或欧氏距离。
- DFS(深度优先搜索)
- 递归回溯,可设置最大深度max_depth防止无限递归。
- 双向BFS
-从起点和终点同时开始BFS,每次扩展一层,直到两个搜索树相遇。