425 lines
14 KiB
Python
425 lines
14 KiB
Python
import sys
|
||
import os
|
||
import time
|
||
import csv
|
||
from collections import deque
|
||
import heapq
|
||
import matplotlib.pyplot as plt
|
||
import numpy as np
|
||
|
||
|
||
# ========== Модель данных ==========
|
||
class Tile:
|
||
def __init__(self, x, y):
|
||
self._x = x
|
||
self._y = y
|
||
self._wall = False
|
||
self._entry = False
|
||
self._goal = False
|
||
|
||
@property
|
||
def x(self): return self._x
|
||
@property
|
||
def y(self): return self._y
|
||
@property
|
||
def is_wall(self): return self._wall
|
||
@is_wall.setter
|
||
def is_wall(self, value): self._wall = value
|
||
@property
|
||
def is_entry(self): return self._entry
|
||
@is_entry.setter
|
||
def is_entry(self, value): self._entry = value
|
||
@property
|
||
def is_goal(self): return self._goal
|
||
@is_goal.setter
|
||
def is_goal(self, value): self._goal = value
|
||
|
||
def can_walk(self):
|
||
return not self._wall
|
||
|
||
|
||
class Labyrinth:
|
||
def __init__(self, width, height):
|
||
self._width = width
|
||
self._height = height
|
||
self._grid = [[Tile(x, y) for x in range(width)] for y in range(height)]
|
||
self._start = None
|
||
self._exit = None
|
||
|
||
@property
|
||
def width(self): return self._width
|
||
@property
|
||
def height(self): return self._height
|
||
@property
|
||
def start(self): return self._start
|
||
@property
|
||
def exit(self): return self._exit
|
||
|
||
def tile_at(self, x, y):
|
||
if 0 <= x < self._width and 0 <= y < self._height:
|
||
return self._grid[y][x]
|
||
return None
|
||
|
||
def configure_tile(self, x, y, kind):
|
||
tile = self.tile_at(x, y)
|
||
if tile is None:
|
||
return
|
||
if kind == 'wall':
|
||
tile.is_wall = True
|
||
elif kind == 'entry':
|
||
if self._start:
|
||
self._start.is_entry = False
|
||
tile.is_entry = True
|
||
tile.is_wall = False
|
||
self._start = tile
|
||
elif kind == 'goal':
|
||
if self._exit:
|
||
self._exit.is_goal = False
|
||
tile.is_goal = True
|
||
tile.is_wall = False
|
||
self._exit = tile
|
||
elif kind == 'floor':
|
||
tile.is_wall = False
|
||
|
||
def neighbours(self, tile):
|
||
res = []
|
||
for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
|
||
nb = self.tile_at(tile.x + dx, tile.y + dy)
|
||
if nb and nb.can_walk():
|
||
res.append(nb)
|
||
return res
|
||
|
||
|
||
# ========== Загрузка из файла ==========
|
||
class TextLabyrinthBuilder:
|
||
def build(self, filename):
|
||
with open(filename, 'r', encoding='utf-8') as f:
|
||
lines = [line.rstrip('\n') for line in f]
|
||
h = len(lines)
|
||
w = max(len(l) for l in lines) if h else 0
|
||
lab = Labyrinth(w, h)
|
||
for y, row in enumerate(lines):
|
||
for x, ch in enumerate(row):
|
||
if ch == '#':
|
||
lab.configure_tile(x, y, 'wall')
|
||
elif ch == 'S':
|
||
lab.configure_tile(x, y, 'entry')
|
||
elif ch == 'E':
|
||
lab.configure_tile(x, y, 'goal')
|
||
elif ch == ' ':
|
||
lab.configure_tile(x, y, 'floor')
|
||
return lab
|
||
|
||
|
||
# ========== Алгоритмы поиска ==========
|
||
class BFS_Pathfinder:
|
||
def find_path(self, lab, start, goal):
|
||
if goal is None:
|
||
self._visited = 0
|
||
return []
|
||
q = deque([start])
|
||
preds = {start: None}
|
||
seen = {start}
|
||
while q:
|
||
cur = q.popleft()
|
||
if cur == goal:
|
||
self._visited = len(seen)
|
||
return self._build_path(preds, start, goal)
|
||
for nb in lab.neighbours(cur):
|
||
if nb not in seen:
|
||
seen.add(nb)
|
||
preds[nb] = cur
|
||
q.append(nb)
|
||
self._visited = len(seen)
|
||
return []
|
||
|
||
def _build_path(self, preds, start, goal):
|
||
path = []
|
||
cur = goal
|
||
while cur is not None:
|
||
path.append(cur)
|
||
cur = preds.get(cur)
|
||
path.reverse()
|
||
return path
|
||
|
||
@property
|
||
def visited_count(self):
|
||
return getattr(self, '_visited', 0)
|
||
|
||
|
||
class DFS_Pathfinder:
|
||
def find_path(self, lab, start, goal):
|
||
if goal is None:
|
||
self._visited = 0
|
||
return []
|
||
stack = [start]
|
||
preds = {start: None}
|
||
seen = {start}
|
||
while stack:
|
||
cur = stack.pop()
|
||
if cur == goal:
|
||
self._visited = len(seen)
|
||
return self._build_path(preds, start, goal)
|
||
for nb in lab.neighbours(cur):
|
||
if nb not in seen:
|
||
seen.add(nb)
|
||
preds[nb] = cur
|
||
stack.append(nb)
|
||
self._visited = len(seen)
|
||
return []
|
||
|
||
def _build_path(self, preds, start, goal):
|
||
path = []
|
||
cur = goal
|
||
while cur is not None:
|
||
path.append(cur)
|
||
cur = preds.get(cur)
|
||
path.reverse()
|
||
return path
|
||
|
||
@property
|
||
def visited_count(self):
|
||
return getattr(self, '_visited', 0)
|
||
|
||
|
||
class AStar_Pathfinder:
|
||
def _heuristic(self, a, b):
|
||
return abs(a.x - b.x) + abs(a.y - b.y)
|
||
|
||
def find_path(self, lab, start, goal):
|
||
if goal is None:
|
||
self._visited = 0
|
||
return []
|
||
heap = []
|
||
cnt = 0
|
||
f_start = self._heuristic(start, goal)
|
||
heapq.heappush(heap, (f_start, cnt, start))
|
||
cnt += 1
|
||
preds = {}
|
||
g = {start: 0}
|
||
f = {start: f_start}
|
||
seen = set()
|
||
while heap:
|
||
cur_f, _, cur = heapq.heappop(heap)
|
||
seen.add(cur)
|
||
if cur == goal:
|
||
self._visited = len(seen)
|
||
return self._build_path(preds, start, goal)
|
||
if cur_f > f.get(cur, float('inf')):
|
||
continue
|
||
for nb in lab.neighbours(cur):
|
||
tent_g = g[cur] + 1
|
||
if tent_g < g.get(nb, float('inf')):
|
||
preds[nb] = cur
|
||
g[nb] = tent_g
|
||
new_f = tent_g + self._heuristic(nb, goal)
|
||
f[nb] = new_f
|
||
heapq.heappush(heap, (new_f, cnt, nb))
|
||
cnt += 1
|
||
self._visited = len(seen)
|
||
return []
|
||
|
||
def _build_path(self, preds, start, goal):
|
||
path = []
|
||
cur = goal
|
||
while cur is not None:
|
||
path.append(cur)
|
||
cur = preds.get(cur)
|
||
path.reverse()
|
||
return path
|
||
|
||
@property
|
||
def visited_count(self):
|
||
return getattr(self, '_visited', 0)
|
||
|
||
|
||
class LabyrinthSolver:
|
||
def __init__(self, lab):
|
||
self._lab = lab
|
||
self._strategy = None
|
||
|
||
def set_strategy(self, strategy):
|
||
self._strategy = strategy
|
||
|
||
def solve(self):
|
||
start_t = time.perf_counter()
|
||
path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit)
|
||
elapsed = (time.perf_counter() - start_t) * 1000
|
||
return {
|
||
'time_ms': elapsed,
|
||
'visited': self._strategy.visited_count,
|
||
'length': len(path)
|
||
}
|
||
|
||
|
||
# ========== Графики ==========
|
||
def create_charts(results, save_filename='maze_performance.png'):
|
||
mazes = list(set([r['maze'] for r in results]))
|
||
strategies = ['BFS', 'DFS', 'AStar']
|
||
|
||
data = {s: {m: None for m in mazes} for s in strategies}
|
||
for r in results:
|
||
data[r['strategy']][r['maze']] = r
|
||
|
||
times = {s: [data[s][m]['time_ms'] for m in mazes] for s in strategies}
|
||
visited = {s: [data[s][m]['visited_cells'] for m in mazes] for s in strategies}
|
||
lengths = {s: [data[s][m]['path_length'] for m in mazes] for s in strategies}
|
||
|
||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||
x = np.arange(len(mazes))
|
||
width = 0.25
|
||
|
||
# Время
|
||
axes[0].bar(x - width, times['BFS'], width, label='BFS', color='blue', alpha=0.7)
|
||
axes[0].bar(x, times['DFS'], width, label='DFS', color='green', alpha=0.7)
|
||
axes[0].bar(x + width, times['AStar'], width, label='A*', color='red', alpha=0.7)
|
||
axes[0].set_xlabel('Лабиринты')
|
||
axes[0].set_ylabel('Время (мс)')
|
||
axes[0].set_title('Время выполнения алгоритмов')
|
||
axes[0].set_xticks(x)
|
||
axes[0].set_xticklabels(mazes, rotation=15, ha='right', fontsize=8)
|
||
axes[0].legend()
|
||
axes[0].grid(True, alpha=0.3)
|
||
|
||
# Посещённые клетки
|
||
axes[1].bar(x - width, visited['BFS'], width, label='BFS', color='blue', alpha=0.7)
|
||
axes[1].bar(x, visited['DFS'], width, label='DFS', color='green', alpha=0.7)
|
||
axes[1].bar(x + width, visited['AStar'], width, label='A*', color='red', alpha=0.7)
|
||
axes[1].set_xlabel('Лабиринты')
|
||
axes[1].set_ylabel('Количество клеток')
|
||
axes[1].set_title('Посещённые клетки')
|
||
axes[1].set_xticks(x)
|
||
axes[1].set_xticklabels(mazes, rotation=15, ha='right', fontsize=8)
|
||
axes[1].legend()
|
||
axes[1].grid(True, alpha=0.3)
|
||
|
||
# Длина пути
|
||
axes[2].bar(x - width, lengths['BFS'], width, label='BFS', color='blue', alpha=0.7)
|
||
axes[2].bar(x, lengths['DFS'], width, label='DFS', color='green', alpha=0.7)
|
||
axes[2].bar(x + width, lengths['AStar'], width, label='A*', color='red', alpha=0.7)
|
||
axes[2].set_xlabel('Лабиринты')
|
||
axes[2].set_ylabel('Длина пути')
|
||
axes[2].set_title('Длина найденного пути')
|
||
axes[2].set_xticks(x)
|
||
axes[2].set_xticklabels(mazes, rotation=15, ha='right', fontsize=8)
|
||
axes[2].legend()
|
||
axes[2].grid(True, alpha=0.3)
|
||
|
||
plt.suptitle('Сравнение алгоритмов поиска пути', fontsize=14, fontweight='bold')
|
||
plt.tight_layout()
|
||
plt.savefig(save_filename, dpi=300, bbox_inches='tight')
|
||
print(f"\nГрафик сохранён в файл: {save_filename}")
|
||
plt.show()
|
||
|
||
|
||
# ========== Основная программа ==========
|
||
def main():
|
||
print('\n' + '=' * 60)
|
||
print('ПОИСК ВЫХОДА ИЗ ЛАБИРИНТА')
|
||
print('Сравнение алгоритмов BFS, DFS и A*')
|
||
print('=' * 60)
|
||
|
||
# Находим все txt файлы в текущей директории
|
||
maze_files = [f for f in os.listdir('.') if f.endswith('.txt') and f.startswith('maze_')]
|
||
|
||
if not maze_files:
|
||
print("\nНет файлов с лабиринтами (maze_*.txt)")
|
||
return
|
||
|
||
print(f"\nНайдено лабиринтов: {len(maze_files)}")
|
||
for f in maze_files:
|
||
print(f" - {f}")
|
||
|
||
strategies = [
|
||
('BFS', BFS_Pathfinder()),
|
||
('DFS', DFS_Pathfinder()),
|
||
('AStar', AStar_Pathfinder())
|
||
]
|
||
|
||
all_results = []
|
||
builder = TextLabyrinthBuilder()
|
||
|
||
for maze_file in sorted(maze_files):
|
||
maze_name = maze_file.replace('.txt', '').replace('maze_', '').replace('_', ' ').title()
|
||
print(f"\nОбработка лабиринта: {maze_name}")
|
||
|
||
lab = builder.build(maze_file)
|
||
|
||
for sname, strat in strategies:
|
||
solver = LabyrinthSolver(lab)
|
||
solver.set_strategy(strat)
|
||
stats = solver.solve()
|
||
|
||
all_results.append({
|
||
'maze': maze_name,
|
||
'strategy': sname,
|
||
'time_ms': stats['time_ms'],
|
||
'visited_cells': stats['visited'],
|
||
'path_length': stats['length']
|
||
})
|
||
|
||
status = "Найден" if stats['length'] > 0 else "Не найден"
|
||
print(f" {sname}: {status} (время: {stats['time_ms']:.3f} мс, посещено: {stats['visited']}, длина: {stats['length']})")
|
||
|
||
# Сохранение в CSV
|
||
print('\n' + '=' * 60)
|
||
print('СОХРАНЕНИЕ РЕЗУЛЬТАТОВ В CSV')
|
||
print('=' * 60)
|
||
|
||
csv_filename = 'experiment_results.csv'
|
||
with open(csv_filename, 'w', newline='', encoding='utf-8') as csvfile:
|
||
fieldnames = ['лабиринт', 'стратегия', 'время_мс', 'посещено_клеток', 'длина_пути']
|
||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||
|
||
writer.writeheader()
|
||
for result in all_results:
|
||
writer.writerow({
|
||
'лабиринт': result['maze'],
|
||
'стратегия': result['strategy'],
|
||
'время_мс': f"{result['time_ms']:.3f}",
|
||
'посещено_клеток': result['visited_cells'],
|
||
'длина_пути': result['path_length']
|
||
})
|
||
|
||
print(f"\nРезультаты сохранены в файл: {csv_filename}")
|
||
|
||
# Вывод таблицы в консоль
|
||
print('\n' + '=' * 100)
|
||
print('ТАБЛИЦА РЕЗУЛЬТАТОВ')
|
||
print('=' * 100)
|
||
print(f"{'Лабиринт':<20} {'Стратегия':<10} {'Время (мс)':<12} {'Посещено':<12} {'Длина пути':<12}")
|
||
print('-' * 100)
|
||
for r in all_results:
|
||
print(f"{r['maze']:<20} {r['strategy']:<10} {r['time_ms']:<12.3f} {r['visited_cells']:<12.0f} {r['path_length']:<12.0f}")
|
||
print('=' * 100)
|
||
|
||
# Создание графика
|
||
print('\nСОЗДАНИЕ ГРАФИКА...')
|
||
create_charts(all_results, 'maze_performance.png')
|
||
|
||
# Статистика
|
||
print('\n' + '=' * 60)
|
||
print('СТАТИСТИКА')
|
||
print('=' * 60)
|
||
|
||
for strategy in ['BFS', 'DFS', 'AStar']:
|
||
strategy_results = [r for r in all_results if r['strategy'] == strategy and r['path_length'] > 0]
|
||
if strategy_results:
|
||
avg_time = sum(r['time_ms'] for r in strategy_results) / len(strategy_results)
|
||
avg_visited = sum(r['visited_cells'] for r in strategy_results) / len(strategy_results)
|
||
avg_length = sum(r['path_length'] for r in strategy_results) / len(strategy_results)
|
||
print(f'\n{strategy}:')
|
||
print(f' Среднее время: {avg_time:.3f} мс')
|
||
print(f' Среднее посещено: {avg_visited:.0f} клеток')
|
||
print(f' Средняя длина пути: {avg_length:.1f}')
|
||
|
||
print('\n' + '=' * 60)
|
||
print('ГОТОВО')
|
||
print(f'Результаты сохранены в:')
|
||
print(f' - {csv_filename} (таблица)')
|
||
print(f' - maze_performance.png (график)')
|
||
print('=' * 60)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main() |