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

427 lines
16 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
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()