diff --git a/BudakovIS/docs/data/2-nd-exercize/main.py b/BudakovIS/docs/data/2-nd-exercize/main.py index 5543e25..08dac90 100644 --- a/BudakovIS/docs/data/2-nd-exercize/main.py +++ b/BudakovIS/docs/data/2-nd-exercize/main.py @@ -1,11 +1,12 @@ import sys from collections import deque import heapq -from typing import List, Dict, Optional +import time +from typing import List, Optional class Cell: - def __init__(self, x, y): + def __init__(self, x: int, y: int): self._x = x self._y = y self._is_wall = False @@ -13,43 +14,46 @@ class Cell: self._is_exit = False @property - def x(self): + def x(self) -> int: return self._x @property - def y(self): + def y(self) -> int: return self._y @property - def is_wall(self): + def is_wall(self) -> bool: return self._is_wall @is_wall.setter - def is_wall(self, value): + def is_wall(self, value: bool) -> None: self._is_wall = value @property - def is_start(self): + def is_start(self) -> bool: return self._is_start @is_start.setter - def is_start(self, value): + def is_start(self, value: bool) -> None: self._is_start = value @property - def is_exit(self): + def is_exit(self) -> bool: return self._is_exit @is_exit.setter - def is_exit(self, value): + def is_exit(self, value: bool) -> None: self._is_exit = value - def is_passable(self): + 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, height): + def __init__(self, width: int, height: int): self._width = width self._height = height self._cells = [[Cell(x, y) for x in range(width)] for y in range(height)] @@ -57,27 +61,27 @@ class Maze: self._exit = None @property - def width(self): + def width(self) -> int: return self._width @property - def height(self): + def height(self) -> int: return self._height @property - def start(self): + def start(self) -> Optional[Cell]: return self._start @property - def exit(self): + def exit(self) -> Optional[Cell]: return self._exit - def get_cell(self, x, y): + def get_cell(self, x: int, y: int) -> Optional[Cell]: if 0 <= x < self._width and 0 <= y < self._height: return self._cells[y][x] return None - def set_cell(self, x, y, cell_type): + def set_cell(self, x: int, y: int, cell_type: str) -> None: cell = self.get_cell(x, y) if cell is None: return @@ -99,7 +103,7 @@ class Maze: elif cell_type == 'path': cell.is_wall = False - def get_neighbors(self, cell): + def get_neighbors(self, cell: Cell) -> List[Cell]: neighbors = [] directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] for dx, dy in directions: @@ -111,118 +115,136 @@ class Maze: class MazeBuilder: - def build_from_file(self, filename): - raise NotImplementedError("Need to realise in calss") + def build_from_file(self, filename: str) -> Maze: + raise NotImplementedError("Need to implement in subclass") + class TextFileMazeBuilder(MazeBuilder): - def build_from_file(self, filename): - with open(filename, 'r') as f: - lines = [line.rstrip('\n')for line in f.readlines()] + def build_from_file(self, filename: str) -> Maze: + with open(filename, 'r', encoding='utf-8') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + + if not lines: + raise ValueError("Файл пуст") + height = len(lines) - width = max(len(line) for line in lines) if height > 0 else 0 - start_en = 0 - exit_en = 0 + width = max(len(line) for line in lines) + + start_count = 0 + exit_count = 0 maze = Maze(width, height) - - for y,line in enumerate(lines): + + for y, line in enumerate(lines): for x, ch in enumerate(line): - if ch == "#": - maze.set_cell(x,y,"wall") - elif ch == "S": - maze.set_cell(x,y,"start") - start_en+=1 - elif ch == "E": - maze.set_cell(x,y,"exit") - exit_en+=1 + if x >= width: + continue + if ch == '#': + maze.set_cell(x, y, "wall") + elif ch == 'S': + maze.set_cell(x, y, "start") + start_count += 1 + elif ch == 'E': + maze.set_cell(x, y, "exit") + exit_count += 1 else: maze.set_cell(x, y, 'path') - if start_en > 1 or exit_en > 1 or start_en==0 or exit_en ==0: - sys.exit("Error while reading file(you have too many or no match start and exits)") + + if start_count != 1 or exit_count != 1: + raise ValueError( + f"Лабиринт должен иметь ровно один S и один E. " + f"Найдено: S={start_count}, E={exit_count}" + ) + return maze +class PathFindingStrategy: + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + raise NotImplementedError("Subclasses must implement find_path") + + def _reconstruct_path(self, came_from: dict, start: Cell, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = came_from.get(current) + path.reverse() + return path -class BFSStrategy: - def find_path(self, maze, start, exit): +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: queue = deque() queue.append(start) - came_from={} - came_from[start]=None - visited = set() - visited.add(start) + + came_from = {start: None} + visited = {start} + while queue: current = queue.popleft() - if current == exit: - return self._reconstruct_path(came_from,start,exit) - for neighbors in maze.get_neighbors(current): - if neighbors not in visited: - visited.add(neighbors) - came_from[neighbors] = current - queue.append(neighbors) + + if current == exit_cell: + return self._reconstruct_path(came_from, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + came_from[neighbor] = current + queue.append(neighbor) + return [] - def _reconstruct_path(self, came_from, start, exit_cell): - path = [] - current = exit_cell - while current is not None: - path.append(current) - current = came_from.get(current) - path.reverse() - return path -class DFSStrategy: - def find_path(self,maze,start,exit): - stack =[] - stack.append(start) - came_from={} - came_from[start]=None - visited = set() - visited.add(start) + +class DFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + stack = [start] + + came_from = {start: None} + visited = {start} + while stack: current = stack.pop() - if current == exit: - return self._reconstruct_path(came_from,start,exit) - for neighbors in maze.get_neighbors(current): - if neighbors not in visited: - visited.add(neighbors) - came_from[neighbors]= current - stack.append(neighbors) + + if current == exit_cell: + return self._reconstruct_path(came_from, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + came_from[neighbor] = current + stack.append(neighbor) + return [] - def _reconstruct_path(self, came_from, start, exit_cell): - path = [] - current = exit_cell - while current is not None: - path.append(current) - current = came_from.get(current) - - path.reverse() - return path - - -class AStarStrategy: - def _heuristic(self, cell,exit_cell): - return abs(cell.x -exit_cell.x)+abs(cell.y-exit_cell.y) - def find_path(self,maze, start, exit_cell): +class AStarStrategy(PathFindingStrategy): + def _heuristic(self, cell: Cell, exit_cell: Cell) -> int: + return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: heap = [] counter = 0 - start_f = 0 + self._heuristic(start,exit_cell) + + start_f = self._heuristic(start, exit_cell) heapq.heappush(heap, (start_f, counter, start)) - counter +=1 - + counter += 1 + came_from = {} g_score = {start: 0} f_score = {start: start_f} - + while heap: current_f, _, current = heapq.heappop(heap) - if current==exit_cell: + + if current == exit_cell: return self._reconstruct_path(came_from, start, exit_cell) - if current_f>f_score.get(current, float("inf")): + + if current_f > f_score.get(current, float('inf')): continue + for neighbor in maze.get_neighbors(current): tentative_g = g_score[current] + 1 + if tentative_g < g_score.get(neighbor, float('inf')): came_from[neighbor] = current g_score[neighbor] = tentative_g @@ -230,28 +252,65 @@ class AStarStrategy: f_score[neighbor] = new_f heapq.heappush(heap, (new_f, counter, neighbor)) counter += 1 + return [] - def _reconstruct_path(self, came_from, start, exit_cell): - path = [] - current = exit_cell - while current is not None: - path.append(current) - current = came_from.get(current) - path.reverse() - return path + + +class SearchStats: + def __init__(self, time_ms: float, visited_cells: int, path_length: int): + self.time_ms = time_ms + self.visited_cells = visited_cells + self.path_length = path_length + + def __repr__(self) -> str: + return f"SearchStats(time={self.time_ms:.3f}ms, visited={self.visited_cells}, path_length={self.path_length})" + + +class MazeSolver: + + def __init__(self, maze: Maze): + self._maze = maze + self._strategy = None + + def set_strategy(self, strategy: PathFindingStrategy) -> None: + self._strategy = strategy + + def solve(self) -> Optional[SearchStats]: + if self._strategy is None: + print("Ошибка: стратегия не установлена") + return None + + start_time = time.perf_counter() + + path = self._strategy.find_path(self._maze, self._maze.start, self._maze.exit) + + end_time = time.perf_counter() + time_ms = (end_time - start_time) * 1000 + + visited_cells = 0 + + return SearchStats(time_ms, visited_cells, len(path)) + if __name__ == "__main__": builder = TextFileMazeBuilder() maze = builder.build_from_file("maze1.txt") + print(f"Лабиринт {maze.width}x{maze.height}") print(f"Старт: ({maze.start.x}, {maze.start.y})") print(f"Выход: ({maze.exit.x}, {maze.exit.y})") - bfs = BFSStrategy() - path = bfs.find_path(maze, maze.start, maze.exit) - print(f"BFS: путь найден, длина = {len(path)}") - dfs = DFSStrategy() - path = dfs.find_path(maze, maze.start, maze.exit) - print(f"DFS: путь найден, длинна = {len(path)}") - astar = AStarStrategy() - path = astar.find_path(maze, maze.start, maze.exit) - print(f"A*: путь найден, длина = {len(path)}") + print("-" * 40) + + solver = MazeSolver(maze) + + solver.set_strategy(BFSStrategy()) + stats = solver.solve() + print(f"BFS: {stats}") + + solver.set_strategy(DFSStrategy()) + stats = solver.solve() + print(f"DFS: {stats}") + + solver.set_strategy(AStarStrategy()) + stats = solver.solve() + print(f"A*: {stats}")