2026-rff_mp/VaravinVV/docs/data/task2/main.py

382 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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):
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 observer in self._observers:
observer.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:
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нет пути (no way)")
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 = 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()