import abc import heapq import time from collections import deque from dataclasses import dataclass from typing import List, Optional, Dict, Set, Tuple, Any import csv import os import sys class Cell: #тут что такое клетка def __init__(self, x: int, y: int, is_wall: bool = False, is_exit: bool = False, is_start: bool = False): self.x = x self.y = y self.is_wall = is_wall self.is_exit = is_exit self.is_start = is_start def __eq__(self, other): return isinstance(other, Cell) and self.x == other.x and self.y == other.y def __hash__(self): return hash((self.x, self.y)) def is_passable(self) -> bool: return not self.is_wall def __repr__(self) -> str: return f"Cell({self.x},{self.y})" class Maze: def __init__(self, width: int, height: int): #что содержит лабиринт, начало конец и тд self.width = width self.height = height self.grid: List[List[Cell]] = [] self.start_cell: Optional[Cell] = None self.exit_cell: Optional[Cell] = None def set_cell(self, x: int, y: int, cell: Cell) -> None: #ставим клетку куда надо или не ставим если в границы не попала if not (0 <= x < self.width and 0 <= y < self.height): raise IndexError("координаты вне границ лабиринта") self.grid[y][x] = cell def get_cell(self, x: int, y: int) -> Optional[Cell]: #тут уже из коррдинат клетку вытаскиваем if 0 <= x < self.width and 0 <= y < self.height: return self.grid[y][x] return None def get_neighbors(self, cell: Cell) -> List[Cell]: #если соседняя клетка проходима - добавляем neighbors = [] for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]: nx, ny = cell.x + dx, cell.y + dy neighbor = self.get_cell(nx, ny) if neighbor and neighbor.is_passable(): neighbors.append(neighbor) return neighbors class MazeBuilder(abc.ABC): @abc.abstractmethod def build_from_file(self, filename: str) -> Maze: pass class TextFileMazeBuilder(MazeBuilder): def build_from_file(self, filename: str) -> Maze: lines = [] with open(filename, 'r', encoding='utf-8') as f: for line in f: line = line.rstrip('\n') if line: #игнорируем пустые строки lines.append(line) if not lines: raise ValueError("Файл пуст") height = len(lines) width = max(len(line) for line in lines) maze = Maze(width, height) #инициализируем сетку пустыми клетками,по умолчанию стенами maze.grid = [[Cell(x, y, is_wall=True) for x in range(width)] for y in range(height)] start_cell = None exit_cell = None for y, line in enumerate(lines): for x, ch in enumerate(line): if x >= width: continue if ch == '#': continue elif ch == ' ': cell = Cell(x, y, is_wall=False) elif ch == 'S': cell = Cell(x, y, is_wall=False, is_start=True) start_cell = cell elif ch == 'E': cell = Cell(x, y, is_wall=False, is_exit=True) exit_cell = cell else: #любой другой символ считаем проходом cell = Cell(x, y, is_wall=False) maze.set_cell(x, y, cell) if start_cell is None: raise ValueError("отсутствует стартовая клетка (S)") #invalid check if exit_cell is None: raise ValueError("отсутствует выход (E)") maze.start_cell = start_cell maze.exit_cell = exit_cell return maze class PathFindingStrategy(abc.ABC): @abc.abstractmethod def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]: pass #дальше скорее математика, методы вроде ещё в том семаке разбирали class BFSStrategy(PathFindingStrategy): def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]: if start is exit_: return [start], 1 queue = deque([start]) #используйте deque 👍 visited: Set[Cell] = {start} parent: Dict[Cell, Optional[Cell]] = {start: None} while queue: current = queue.popleft() if current is exit_: #восстановление пути path = [] cur = current while cur is not None: path.append(cur) cur = parent[cur] path.reverse() return path, len(visited) for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) parent[neighbor] = current queue.append(neighbor) return [], len(visited) class DFSStrategy(PathFindingStrategy): def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]: if start is exit_: return [start], 1 stack = [start] visited: Set[Cell] = {start} parent: Dict[Cell, Optional[Cell]] = {start: None} while stack: current = stack.pop() if current is exit_: path = [] cur = current while cur is not None: path.append(cur) cur = parent[cur] path.reverse() return path, len(visited) for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) parent[neighbor] = current stack.append(neighbor) return [], len(visited) class AStarStrategy(PathFindingStrategy): @staticmethod def _heuristic(cell: Cell, target: Cell) -> int: return abs(cell.x - target.x) + abs(cell.y - target.y) #самая простая эвристика def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]: if start is exit_: return [start], 1 counter = 0 open_set = [(0, counter, start)] g_score: Dict[Cell, int] = {start: 0} f_score: Dict[Cell, int] = {start: self._heuristic(start, exit_)} parent: Dict[Cell, Optional[Cell]] = {start: None} closed_set: Set[Cell] = set() visited_count = 0 while open_set: _, _, current = heapq.heappop(open_set) if current in closed_set: continue closed_set.add(current) visited_count = len(closed_set) if current is exit_: path = [] cur = current while cur is not None: path.append(cur) cur = parent[cur] path.reverse() return path, visited_count for neighbor in maze.get_neighbors(current): if neighbor in closed_set: continue tentative_g = g_score[current] + 1 if neighbor not in g_score or tentative_g < g_score[neighbor]: parent[neighbor] = current g_score[neighbor] = tentative_g f = tentative_g + self._heuristic(neighbor, exit_) f_score[neighbor] = f counter += 1 heapq.heappush(open_set, (f, counter, neighbor)) return [], visited_count @dataclass class SearchStats: time_ms: float #время выполнения в мс visited_cells: int #количество посещённых клеток path_length: int #длина найденного пути (0 если пути нет) path_found: bool #найден ли путь class Observer(abc.ABC): #я забыл что я там писать хотел после наблюдателя удачи мне завтра разобрать @abc.abstractmethod #а я (гугл + хабр) разобрал снова балбесина!!! def update(self, event_type: str, data: Any = None) -> None: pass class Subject: def __init__(self): self._observers: List[Observer] = [] def attach(self, observer: Observer) -> None: if observer not in self._observers: self._observers.append(observer) def detach(self, observer: Observer) -> None: if observer in self._observers: self._observers.remove(observer) def notify(self, event_type: str, data: Any = None) -> None: for obs in self._observers: obs.update(event_type, data) class MazeSolver(Subject): def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None): super().__init__() self.maze = maze self._strategy = strategy def set_strategy(self, strategy: PathFindingStrategy) -> None: self._strategy = strategy def solve(self) -> Optional[SearchStats]: if self._strategy is None: return None start_time = time.perf_counter() path, visited = self._strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell) end_time = time.perf_counter() time_ms = (end_time - start_time) * 1000.0 return SearchStats(time_ms, visited, len(path), len(path) > 0) class Benchmark: def __init__(self, maze_files: List[str], runs_per_strategy: int = 5): self.maze_files = maze_files self.runs = runs_per_strategy self.strategies = { "BFS": BFSStrategy(), "DFS": DFSStrategy(), "AStar": AStarStrategy() } self.builder = TextFileMazeBuilder() self.results = [] def run(self, output_csv: str): for maze_file in self.maze_files: if not os.path.exists(maze_file): print(f"файл {maze_file} не найден") continue try: maze = self.builder.build_from_file(maze_file) except Exception as e: print(f"ошибка загрузки {maze_file}: {e}") continue print(f"обработка лабиринта: {maze_file} (размер {maze.width}x{maze.height})") for strat_name, strategy in self.strategies.items(): solver = MazeSolver(maze, strategy) times = [] visited_list = [] path_lengths = [] path_found = False for run_idx in range(self.runs): stats = solver.solve() if stats is None: continue times.append(stats.time_ms) visited_list.append(stats.visited_cells) path_lengths.append(stats.path_length) path_found = stats.path_found if times: avg_time = sum(times) / len(times) avg_visited = sum(visited_list) / len(visited_list) avg_length = sum(path_lengths) / len(path_lengths) else: avg_time = avg_visited = avg_length = 0.0 self.results.append({ "лабиринт": os.path.basename(maze_file), "стратегия": strat_name, "время_мс": round(avg_time, 3), "посещено_клеток": round(avg_visited, 1), "длина_пути": round(avg_length, 1), "путь_найден": path_found }) print(f" {strat_name}: {avg_time:.3f} мс, посещено {avg_visited:.1f}, длина {avg_length:.1f}") with open(output_csv, 'w', newline='', encoding='utf-8') as csvfile: fieldnames = ["лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути", "путь_найден"] writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=',') writer.writeheader() for row in self.results: writer.writerow(row) print(f"\nрезультаты сохранены в {output_csv}") # ----------------------------- Консольный режим (для одного лабиринта, с визуализацией) ----------------------------- def interactive_mode(maze_file: str): builder = TextFileMazeBuilder() try: maze = builder.build_from_file(maze_file) except Exception as e: print(f"ошибка загрузки лабиринта: {e}") return strategies = { "1": ("BFS", BFSStrategy()), "2": ("DFS", DFSStrategy()), "3": ("A*", AStarStrategy()) } print("\nвыберите алгоритм поиска:") print("1. BFS") print("2. DFS") print("3. A*") choice = input("введите (1/2/3): ").strip() if choice not in strategies: print("неверный выбор, по умолчанию используется BFS.") strat_name, strategy = strategies["1"] else: strat_name, strategy = strategies[choice] solver = MazeSolver(maze, strategy) stats = solver.solve() if stats is None: print("ошибка с решением") return path, _ = strategy.find_path(maze, maze.start_cell, maze.exit_cell) path_set = set(path) for y in range(maze.height): row = [] for x in range(maze.width): cell = maze.get_cell(x, y) if cell is maze.start_cell: row.append('S') elif cell is maze.exit_cell: row.append('E') elif cell in path_set: row.append('*') elif cell and cell.is_wall: row.append('#') else: row.append(' ') print(''.join(row)) print(f"\nстатистика ({strat_name}):") print(f"время выполнения: {stats.time_ms:.3f} мс") print(f"посещено клеток: {stats.visited_cells}") print(f"длина пути: {stats.path_length}") print(f"путь найден: {'да' if stats.path_found else 'нет'}") def main(): if len(sys.argv) < 2: print("использование:") print("режим визуализации: python main.py <файл_лабиринта>") print("режим замера: python main.py --benchmark <список_лабиринтов> --runs <кол_во_итераций> --output <название_таблицы>.csv") return if sys.argv[1] == "--benchmark": args = sys.argv[2:] maze_files = [] runs = 5 output = "benchmark_results.csv" i = 0 while i < len(args): if args[i] == "--runs" and i+1 < len(args): runs = int(args[i+1]) i += 2 elif args[i] == "--output" and i+1 < len(args): output = args[i+1] i += 2 else: maze_files.append(args[i]) i += 1 if not maze_files: print("Ошибка: не указаны файлы лабиринтов.") return benchmark = Benchmark(maze_files, runs_per_strategy=runs) benchmark.run(output) else: interactive_mode(sys.argv[1]) if __name__ == "__main__": main()