2026-rff_mp/rybakovaa/lab2/docs/data/laba2.py

429 lines
13 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.

from abc import ABC, abstractmethod
from collections import deque
import heapq
import time
import csv
import random
import os
BASE = os.path.dirname(os.path.abspath(__file__))
class Cell:
def __init__(self, x, y):
self.x = x
self.y = y
self.isWall = False
self.isStart = False
self.isExit = False
def isPassable(self):
return not self.isWall
def __eq__(self, other):
if other is None:
return False
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
def __lt__(self, other):
return (self.x, self.y) < (other.x, other.y)
class Maze:
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = [[Cell(x, y) for y in range(height)] for x in range(width)]
self.start = None
self.exit = None
def getCell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.grid[x][y]
return None
def getNeighbors(self, cell):
neighbors = []
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
n = self.getCell(cell.x + dx, cell.y + dy)
if n and n.isPassable():
neighbors.append(n)
return neighbors
class MazeBuilder(ABC):
@abstractmethod
def buildFromFile(self, filename):
pass
class TextFileMazeBuilder(MazeBuilder):
def buildFromFile(self, filename):
with open(filename, "r", encoding="utf-8") as f:
lines = [line.rstrip("\n\r") for line in f.readlines()]
height = len(lines)
width = len(lines[0]) if height > 0 else 0
for line in lines:
if len(line) != width:
raise ValueError("все строки должны быть одной длины")
maze = Maze(width, height)
for y in range(height):
for x in range(width):
ch = lines[y][x]
cell = maze.getCell(x, y)
if ch == "#":
cell.isWall = True
elif ch == " ":
cell.isWall = False
elif ch == "S":
cell.isWall = False
cell.isStart = True
maze.start = cell
elif ch == "E":
cell.isWall = False
cell.isExit = True
maze.exit = cell
else:
raise ValueError(f"неизвестный символ: {ch}")
if maze.start is None:
raise ValueError("нет старта (S)")
if maze.exit is None:
raise ValueError("нет выхода (E)")
return maze
class PathFindingStrategy(ABC):
@abstractmethod
def findPath(self, maze, start, exit_cell):
pass
def _reconstruct(self, parent, exit_cell):
path = []
curr = exit_cell
while curr is not None:
path.append(curr)
curr = parent.get(curr)
path.reverse()
return path
class BFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit_cell):
if exit_cell is None:
return []
queue = deque([start])
visited = {start}
parent = {start: None}
while queue:
curr = queue.popleft()
if curr == exit_cell:
return self._reconstruct(parent, exit_cell)
for n in maze.getNeighbors(curr):
if n not in visited:
visited.add(n)
parent[n] = curr
queue.append(n)
return []
class DFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit_cell):
if exit_cell is None:
return []
stack = [start]
visited = {start}
parent = {start: None}
while stack:
curr = stack.pop()
if curr == exit_cell:
return self._reconstruct(parent, exit_cell)
for n in maze.getNeighbors(curr):
if n not in visited:
visited.add(n)
parent[n] = curr
stack.append(n)
return []
class AStarStrategy(PathFindingStrategy):
def _heuristic(self, cell, exit_cell):
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
def findPath(self, maze, start, exit_cell):
if exit_cell is None:
return []
open_set = []
heapq.heappush(open_set, (0, start))
parent = {start: None}
g_score = {start: 0}
while open_set:
curr = heapq.heappop(open_set)[1]
if curr == exit_cell:
return self._reconstruct(parent, exit_cell)
for n in maze.getNeighbors(curr):
new_g = g_score[curr] + 1
if n not in g_score or new_g < g_score[n]:
g_score[n] = new_g
parent[n] = curr
f = new_g + self._heuristic(n, exit_cell)
heapq.heappush(open_set, (f, n))
return []
class SearchStats:
def __init__(self, time_ms, visited, path_len):
self.time_ms = time_ms
self.visited_cells = visited
self.path_length = path_len
class MazeSolver:
def __init__(self, maze):
self.maze = maze
self.strategy = None
self.observers = []
def setStrategy(self, strategy):
self.strategy = strategy
def attach(self, observer):
self.observers.append(observer)
def notify(self, event):
for obs in self.observers:
obs.update(event)
def solve(self):
if self.strategy is None:
raise ValueError("стратегия не выбрана")
start_time = time.perf_counter()
path = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit)
elapsed_ms = (time.perf_counter() - start_time) * 1000
stats = SearchStats(elapsed_ms, len(path), len(path))
self.notify({"type": "path_found", "maze": self.maze, "path": path, "stats": stats})
return path, stats
class Observer(ABC):
@abstractmethod
def update(self, event):
pass
class ConsoleView(Observer):
def update(self, event):
if event["type"] == "path_found":
stats = event["stats"]
print(f"длина пути {stats.path_length}, время {stats.time_ms:.2f} мс")
def save_maze(maze, filename):
path = os.path.join(BASE, filename)
with open(path, "w", encoding="utf-8") as f:
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.getCell(x, y)
if cell == maze.start:
line += "S"
elif cell == maze.exit:
line += "E"
elif cell.isWall:
line += "#"
else:
line += " "
f.write(line + "\n")
def generate_with_walls(w, h, prob=0.3):
maze = Maze(w, h)
for x in range(w):
for y in range(h):
if random.random() < prob:
maze.getCell(x, y).isWall = True
maze.getCell(0, 0).isWall = False
maze.getCell(w - 1, h - 1).isWall = False
for x in range(w):
maze.getCell(x, 0).isWall = False
for y in range(h):
maze.getCell(w - 1, y).isWall = False
maze.getCell(0, 0).isStart = True
maze.start = maze.getCell(0, 0)
maze.getCell(w - 1, h - 1).isExit = True
maze.exit = maze.getCell(w - 1, h - 1)
return maze
def generate_empty(w, h):
maze = Maze(w, h)
for x in range(w):
for y in range(h):
maze.getCell(x, y).isWall = False
maze.getCell(0, 0).isStart = True
maze.start = maze.getCell(0, 0)
maze.getCell(w - 1, h - 1).isExit = True
maze.exit = maze.getCell(w - 1, h - 1)
return maze
def generate_no_exit(w, h):
maze = generate_with_walls(w, h, 0.3)
exit_cell = maze.getCell(w - 1, h - 1)
exit_cell.isWall = True
exit_cell.isExit = False
maze.exit = None
return maze
def run_experiment(maze, strategy_class, maze_name, repeats=5):
times = []
path_lens = []
for _ in range(repeats):
solver = MazeSolver(maze)
solver.setStrategy(strategy_class())
path, stats = solver.solve()
times.append(stats.time_ms)
path_lens.append(len(path))
raw = strategy_class.__name__
strat_name = "A" if raw == "AStarStrategy" else raw.replace("Strategy", "")
return {
"лабиринт": maze_name,
"стратегия": strat_name,
"время_ср": sum(times) / repeats,
"длина_пути_ср": sum(path_lens) / repeats,
"путь_найден": any(l > 0 for l in path_lens),
}
def main():
mazes = []
small = generate_with_walls(10, 10, 0.2)
save_maze(small, "maze_small.txt")
mazes.append(("маленький 10x10", small))
medium = generate_with_walls(50, 50, 0.3)
save_maze(medium, "maze_medium.txt")
mazes.append(("средний 50x50", medium))
large = generate_with_walls(100, 100, 0.3)
save_maze(large, "maze_large.txt")
mazes.append(("большой 100x100", large))
empty = generate_empty(50, 50)
save_maze(empty, "maze_empty.txt")
mazes.append(("пустой 50x50", empty))
no_exit = generate_no_exit(20, 20)
save_maze(no_exit, "maze_no_exit.txt")
mazes.append(("без выхода 20x20", no_exit))
strategies = [BFSStrategy, DFSStrategy, AStarStrategy]
results = []
for maze_name, maze in mazes:
print(maze_name)
for strat in strategies:
res = run_experiment(maze, strat, maze_name)
results.append(res)
print(f" {strat.__name__}: {res['время_ср']:.2f} мс")
csv_path = os.path.join(BASE, "resultslab.csv")
with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(
f,
fieldnames=["лабиринт", "стратегия", "время_ср", "длина_пути_ср", "путь_найден"],
delimiter=";",
)
writer.writeheader()
for row in results:
row_ru = row.copy()
row_ru["путь_найден"] = "да" if row["путь_найден"] else "нет"
writer.writerow(row_ru)
try:
import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["Segoe UI", "Arial", "Tahoma", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
labyrinths = []
for r in results:
if r["лабиринт"] not in labyrinths:
labyrinths.append(r["лабиринт"])
fig, axes = plt.subplots(1, len(labyrinths), figsize=(4 * len(labyrinths), 4))
if len(labyrinths) == 1:
axes = [axes]
for idx, lab in enumerate(labyrinths):
times = []
for s in ["BFS", "DFS", "A"]:
for r in results:
if r["лабиринт"] == lab and r["стратегия"] == s:
times.append(r["время_ср"])
break
axes[idx].bar(["BFS", "DFS", "A"], times, color=["#1a5632", "#0e5fb4", "#e67e22"])
axes[idx].set_title(lab)
axes[idx].set_ylabel("мс")
plt.tight_layout()
plt.savefig(os.path.join(BASE, "maze_time_comparison.png"))
plt.close()
except ImportError:
pass
report_path = os.path.join(os.path.dirname(BASE), "report.md")
with open(report_path, "w", encoding="utf-8-sig") as f:
f.write("# Отчёт: поиск пути в лабиринте\n\n")
f.write("Паттерны: Builder, Strategy, Observer\n\n")
f.write("```mermaid\nclassDiagram\n")
f.write("class MazeBuilder\nclass TextFileMazeBuilder\n")
f.write("class PathFindingStrategy\nclass BFSStrategy\n")
f.write("class DFSStrategy\nclass AStarStrategy\n")
f.write("class MazeSolver\nclass Observer\nclass ConsoleView\n")
f.write("MazeBuilder <|-- TextFileMazeBuilder\n")
f.write("PathFindingStrategy <|-- BFSStrategy\n")
f.write("PathFindingStrategy <|-- DFSStrategy\n")
f.write("PathFindingStrategy <|-- AStarStrategy\n")
f.write("Observer <|-- ConsoleView\n")
f.write("MazeSolver --> PathFindingStrategy\n")
f.write("```\n\n")
f.write("| Лабиринт | Стратегия | Время (мс) | Длина пути | Найден |\n")
f.write("| --- | --- | --- | --- | --- |\n")
for r in results:
found = "да" if r["путь_найден"] else "нет"
f.write(
f"| {r['лабиринт']} | {r['стратегия']} | {r['время_ср']:.2f} | "
f"{r['длина_пути_ср']:.0f} | {found} |\n"
)
f.write("\n![График](data/maze_time_comparison.png)\n\n")
f.write("## Выводы\n\n")
f.write("- BFS и A* находят кратчайший путь.\n")
f.write("- DFS путь может быть длиннее.\n")
f.write("- На пустом лабиринте алгоритмы работают быстрее всего.\n")
f.write("- Без выхода все стратегии возвращают пустой путь.\n")
print("Готово:", report_path)
if __name__ == "__main__":
main()