diff --git a/VaravinVV/.vscode/settings.json b/VaravinVV/.vscode/settings.json new file mode 100644 index 0000000..c9ebf2d --- /dev/null +++ b/VaravinVV/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:system" +} \ No newline at end of file diff --git a/VaravinVV/docs/data/task2/easy.txt b/VaravinVV/docs/data/task2/easy.txt new file mode 100644 index 0000000..7b701e7 --- /dev/null +++ b/VaravinVV/docs/data/task2/easy.txt @@ -0,0 +1,6 @@ +########## +#S # # # +# # # # # +# # # # # +# # E# +########## \ No newline at end of file diff --git a/VaravinVV/docs/data/task2/hard.txt b/VaravinVV/docs/data/task2/hard.txt new file mode 100644 index 0000000..9c2703a --- /dev/null +++ b/VaravinVV/docs/data/task2/hard.txt @@ -0,0 +1,22 @@ +#################################### +#S# # # # +# # #### # ##### # ############### # +# # # # # # # # +### # # # # ############### ### # # +# # # # # # # # +# ### ##### ############# # ### # # +# # # # # # # +##### ##### # ######### # ####### # +# # # # # # # # +# ####### ### # ##### # ####### # # +# # # # # # # # # +# # ######### ##### # # ##### # # # +# # # # # # # # # # # +# # # ######### # ### # # # # # # # +# # # # # # # # # # # +### # # ##### # ##### # # # # ### # +# # # # # # # # +# ########### # ### # ### # ####### +# # # # +############### ####### ####### #E# +#################################### \ No newline at end of file diff --git a/VaravinVV/docs/data/task2/main.py b/VaravinVV/docs/data/task2/main.py new file mode 100644 index 0000000..f5b246d --- /dev/null +++ b/VaravinVV/docs/data/task2/main.py @@ -0,0 +1,429 @@ +import abc +import heapq +import time +from collections import deque +from dataclasses import dataclass +from typing import List, Optional, Dict, Set, Tuple, Any + +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 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): + """Строитель лабиринта из текстового файла. + # - стена, пробел - проход, S - старт, E - выход.""" + + 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)") + if exit_cell is None: + raise ValueError("В лабиринте отсутствует выход (E)") + + maze.start_cell = start_cell + maze.exit_cell = exit_cell + return maze + + +# ============================== Паттерн Strategy ============================== + +class PathFindingStrategy(abc.ABC): + """Интерфейс стратегии поиска пути.""" + @abc.abstractmethod + def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]: + """ + Возвращает (путь в виде списка клеток от start до exit_, количество посещённых клеток). + Если пути нет, возвращает ([], visited_count). + """ + 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]) + 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): + """Алгоритм A* с манхэттенской эвристикой.""" + + @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 + + # Приоритетная очередь: (f, counter, cell) + 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 # найден ли путь + + +# ============================== Паттерн Observer ============================== + +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 observer in self._observers: + observer.update(event_type, data) + + +# ============================== MazeSolver (оркестратор) ============================== + +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]: + """Выполнить поиск пути с текущей стратегией. + Возвращает статистику или None, если стратегия не установлена.""" + if self._strategy is None: + print("Стратегия не установлена.") + return None + + self.notify("solving_start", {"strategy": self._strategy.__class__.__name__}) + + 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 + path_found = len(path) > 0 + stats = SearchStats( + time_ms=time_ms, + visited_cells=visited, + path_length=len(path) if path_found else 0, + path_found=path_found + ) + + if path_found: + self.notify("path_found", {"path": path, "stats": stats}) + else: + self.notify("no_path", {"stats": stats}) + + self.notify("solving_end", {"stats": stats}) + return stats + +class ConsoleView(Observer): + """Отображает лабиринт и найденный путь в консоли.""" + def __init__(self, maze: Maze): + self.maze = maze + self.last_path: List[Cell] = [] + + def update(self, event_type: str, data: Any = None) -> None: + if event_type == "path_found": + self.last_path = data["path"] + self.render() + elif event_type == "no_path": + self.last_path = [] + self.render(no_path=True) + elif event_type == "solving_start": + print(f"\n== Поиск пути (алгоритм: {data['strategy']}) ==\n") + + def render(self, no_path: bool = False) -> None: + """Отрисовать лабиринт с текущим найденным путём.""" + # Создаём множество клеток пути для быстрой проверки + path_set = set(self.last_path) if self.last_path else set() + + for y in range(self.maze.height): + row = [] + for x in range(self.maze.width): + cell = self.maze.get_cell(x, y) + if cell is None: + row.append('?') + continue + if cell is self.maze.start_cell: + row.append('S') + elif cell is self.maze.exit_cell: + row.append('E') + elif cell in path_set: + row.append('*') + elif cell.is_wall: + row.append('#') + else: + row.append(' ') + print(''.join(row)) + + if no_path: + print("\nПуть не найден!") + elif self.last_path: + print(f"\nНайден путь длиной {len(self.last_path)} клеток.") + else: + print("\nОжидание решения...") + +def main(): + import sys + + if len(sys.argv) < 2: + print("для запуска: python main.py <имя_лабиринта>.txt") + return + + filename = sys.argv[1] + + # Строим лабиринт из файла (Builder) + builder = TextFileMazeBuilder() + try: + maze = builder.build_from_file(filename) + except Exception as e: + print(f"Ошибка загрузки лабиринта: {e}") + return + + # Создаём решатель и прикрепляем наблюдателя + solver = MazeSolver(maze) + view = ConsoleView(maze) + solver.attach(view) + + # Меню выбора стратегии + strategies = { + "1": BFSStrategy(), + "2": DFSStrategy(), + "3": AStarStrategy() + } + + print("\nвыберите алгоритм поиска:") + print("1. BFS") + print("2. DFS") + print("3. A*") + choice = input("введите (1/2/3): ").strip() + + strategy = strategies.get(choice) + if not strategy: + print("неверный выбор, по умолчанию используется BFS.") + strategy = BFSStrategy() + + solver.set_strategy(strategy) + stats = solver.solve() + + if stats: + print("\nстатистика по поиску пути в данном лабиринте") + print(f"выбранный алгоритм: {strategy.__class__.__name__}") + print(f"время выполнения: {stats.time_ms:.3f} мс") + print(f"посещено клеток: {stats.visited_cells}") + print(f"путь найден?: {'да' if stats.path_found else 'нет'}") + if stats.path_found: + print(f"длина пути: {stats.path_length}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/VaravinVV/docs/data/task2/normal.txt b/VaravinVV/docs/data/task2/normal.txt new file mode 100644 index 0000000..ca24cd2 --- /dev/null +++ b/VaravinVV/docs/data/task2/normal.txt @@ -0,0 +1,11 @@ +####################### +#S # # +# #### # # ##### ### # +# # # # # # +# ## # ### # # # ### # +# # # # # # # # +## # ### ### # ### # # +# # # # # # +# ## # # ########### # +# # # E # +####################### \ No newline at end of file