[3]
This commit is contained in:
parent
b5f5372e4f
commit
0fe5753b41
64
SmirnovVS/docs/data/2-nd-exercize/experiment.py
Normal file
64
SmirnovVS/docs/data/2-nd-exercize/experiment.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import argparse
|
||||
import csv
|
||||
from pathlib import Path
|
||||
from statistics import mean
|
||||
|
||||
from generate_mazes import generate_all
|
||||
from maze_app import AStarStrategy, BFSStrategy, DFSStrategy, MazeSolver, TextFileMazeBuilder
|
||||
|
||||
|
||||
STRATEGIES = (BFSStrategy, DFSStrategy, AStarStrategy)
|
||||
|
||||
|
||||
def run_experiment(repeats=7, maze_dir="mazes", output_dir="docs/data"):
|
||||
generate_all(maze_dir)
|
||||
builder = TextFileMazeBuilder()
|
||||
rows = []
|
||||
for maze_path in sorted(Path(maze_dir).glob("*.txt")):
|
||||
maze = builder.build_from_file(maze_path)
|
||||
for strategy_type in STRATEGIES:
|
||||
for run in range(1, repeats + 1):
|
||||
stats = MazeSolver(maze, strategy_type()).solve()
|
||||
rows.append({
|
||||
"maze": maze_path.stem,
|
||||
"strategy": stats.strategy,
|
||||
"run": run,
|
||||
"time_ms": stats.time_ms,
|
||||
"visited_cells": stats.visited_cells,
|
||||
"path_length": stats.path_length,
|
||||
"path_found": bool(stats.path),
|
||||
})
|
||||
output = Path(output_dir)
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
raw_path = output / "maze_results_raw.csv"
|
||||
with raw_path.open("w", newline="", encoding="utf-8-sig") as file:
|
||||
writer = csv.DictWriter(file, fieldnames=rows[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
groups = {}
|
||||
for row in rows:
|
||||
groups.setdefault((row["maze"], row["strategy"]), []).append(row)
|
||||
summary = []
|
||||
for (maze_name, strategy), values in groups.items():
|
||||
summary.append({
|
||||
"maze": maze_name,
|
||||
"strategy": strategy,
|
||||
"mean_time_ms": mean(row["time_ms"] for row in values),
|
||||
"mean_visited_cells": mean(row["visited_cells"] for row in values),
|
||||
"path_length": values[0]["path_length"],
|
||||
"path_found": values[0]["path_found"],
|
||||
})
|
||||
summary_path = output / "maze_results_summary.csv"
|
||||
with summary_path.open("w", newline="", encoding="utf-8-sig") as file:
|
||||
writer = csv.DictWriter(file, fieldnames=summary[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(summary)
|
||||
return raw_path, summary_path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Сравнение алгоритмов поиска пути")
|
||||
parser.add_argument("--repeats", type=int, default=7)
|
||||
args = parser.parse_args()
|
||||
print("Результаты:", *run_experiment(args.repeats), sep="\n")
|
||||
52
SmirnovVS/docs/data/2-nd-exercize/generate_mazes.py
Normal file
52
SmirnovVS/docs/data/2-nd-exercize/generate_mazes.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""Генерация воспроизводимых тестовых лабиринтов."""
|
||||
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def obstacle_maze(width, height, wall_probability, seed):
|
||||
rng = random.Random(seed)
|
||||
grid = [["#" if x in (0, width - 1) or y in (0, height - 1) else " " for x in range(width)] for y in range(height)]
|
||||
for y in range(1, height - 1):
|
||||
for x in range(1, width - 1):
|
||||
if rng.random() < wall_probability:
|
||||
grid[y][x] = "#"
|
||||
# Оставляем гарантированный путь по верхней и правой внутренним границам.
|
||||
for x in range(1, width - 1):
|
||||
grid[1][x] = " "
|
||||
for y in range(1, height - 1):
|
||||
grid[y][width - 2] = " "
|
||||
grid[1][1], grid[height - 2][width - 2] = "S", "E"
|
||||
return "\n".join("".join(row) for row in grid) + "\n"
|
||||
|
||||
|
||||
def empty_maze(width=50, height=50):
|
||||
return obstacle_maze(width, height, 0, 1)
|
||||
|
||||
|
||||
def blocked_maze(width=30, height=30):
|
||||
lines = empty_maze(width, height).splitlines()
|
||||
grid = [list(line) for line in lines]
|
||||
exit_y, exit_x = height - 2, width - 2
|
||||
grid[exit_y - 1][exit_x] = "#"
|
||||
grid[exit_y][exit_x - 1] = "#"
|
||||
return "\n".join("".join(row) for row in grid) + "\n"
|
||||
|
||||
|
||||
def generate_all(output_dir="mazes"):
|
||||
output = Path(output_dir)
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
maps = {
|
||||
"small_10x10.txt": obstacle_maze(10, 10, 0.12, 10),
|
||||
"medium_50x50.txt": obstacle_maze(50, 50, 0.28, 50),
|
||||
"large_100x100.txt": obstacle_maze(100, 100, 0.32, 100),
|
||||
"empty_50x50.txt": empty_maze(),
|
||||
"no_path_30x30.txt": blocked_maze(),
|
||||
}
|
||||
for filename, content in maps.items():
|
||||
(output / filename).write_text(content, encoding="utf-8")
|
||||
return list(maps)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Созданы файлы:", ", ".join(generate_all()))
|
||||
24
SmirnovVS/docs/data/2-nd-exercize/main.py
Normal file
24
SmirnovVS/docs/data/2-nd-exercize/main.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import argparse
|
||||
|
||||
from maze_app import AStarStrategy, BFSStrategy, ConsoleView, DFSStrategy, MazeSolver, TextFileMazeBuilder
|
||||
|
||||
|
||||
STRATEGIES = {"bfs": BFSStrategy, "dfs": DFSStrategy, "astar": AStarStrategy}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Поиск выхода из лабиринта")
|
||||
parser.add_argument("maze", nargs="?", default="mazes/small_10x10.txt")
|
||||
parser.add_argument("--algorithm", choices=STRATEGIES, default="bfs")
|
||||
args = parser.parse_args()
|
||||
maze = TextFileMazeBuilder().build_from_file(args.maze)
|
||||
view = ConsoleView()
|
||||
solver = MazeSolver(maze, STRATEGIES[args.algorithm]())
|
||||
solver.attach(view)
|
||||
stats = solver.solve()
|
||||
print(view.render(maze, stats.path))
|
||||
print(f"Время: {stats.time_ms:.4f} мс; посещено: {stats.visited_cells}; длина пути: {stats.path_length}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
246
SmirnovVS/docs/data/2-nd-exercize/maze_app.py
Normal file
246
SmirnovVS/docs/data/2-nd-exercize/maze_app.py
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
"""Модель лабиринта и алгоритмы поиска с паттернами Builder, Strategy, Observer."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from heapq import heappop, heappush
|
||||
from itertools import count
|
||||
from pathlib import Path
|
||||
from time import perf_counter
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Cell:
|
||||
x: int
|
||||
y: int
|
||||
is_wall: bool = False
|
||||
is_start: bool = False
|
||||
is_exit: bool = False
|
||||
|
||||
def is_passable(self):
|
||||
return not self.is_wall
|
||||
|
||||
|
||||
class Maze:
|
||||
def __init__(self, cells):
|
||||
if not cells or not cells[0]:
|
||||
raise ValueError("Лабиринт не может быть пустым")
|
||||
self.cells = cells
|
||||
self.height = len(cells)
|
||||
self.width = len(cells[0])
|
||||
self.start = next((cell for row in cells for cell in row if cell.is_start), None)
|
||||
self.exit = next((cell for row in cells for cell in row if cell.is_exit), None)
|
||||
|
||||
def get_cell(self, x, y):
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
return self.cells[y][x]
|
||||
return None
|
||||
|
||||
def get_neighbors(self, cell):
|
||||
neighbors = []
|
||||
for dx, dy in ((0, -1), (1, 0), (0, 1), (-1, 0)):
|
||||
neighbor = self.get_cell(cell.x + dx, cell.y + dy)
|
||||
if neighbor is not None and neighbor.is_passable():
|
||||
neighbors.append(neighbor)
|
||||
return neighbors
|
||||
|
||||
|
||||
class MazeBuilder(ABC):
|
||||
@abstractmethod
|
||||
def build_from_file(self, filename):
|
||||
pass
|
||||
|
||||
|
||||
class TextFileMazeBuilder(MazeBuilder):
|
||||
SYMBOLS = {"#", " ", "S", "E"}
|
||||
|
||||
def build_from_file(self, filename):
|
||||
lines = Path(filename).read_text(encoding="utf-8").splitlines()
|
||||
if not lines or not lines[0] or any(len(line) != len(lines[0]) for line in lines):
|
||||
raise ValueError("Строки лабиринта должны иметь одинаковую ненулевую длину")
|
||||
unknown = {character for line in lines for character in line} - self.SYMBOLS
|
||||
if unknown:
|
||||
raise ValueError(f"Недопустимые символы: {sorted(unknown)}")
|
||||
if sum(line.count("S") for line in lines) != 1 or sum(line.count("E") for line in lines) != 1:
|
||||
raise ValueError("Лабиринт должен содержать ровно один старт S и один выход E")
|
||||
|
||||
cells = []
|
||||
for y, line in enumerate(lines):
|
||||
cells.append([
|
||||
Cell(x, y, symbol == "#", symbol == "S", symbol == "E")
|
||||
for x, symbol in enumerate(line)
|
||||
])
|
||||
return Maze(cells)
|
||||
|
||||
|
||||
class PathFindingStrategy(ABC):
|
||||
name = "Неизвестно"
|
||||
|
||||
def __init__(self):
|
||||
self.visited_count = 0
|
||||
|
||||
@abstractmethod
|
||||
def find_path(self, maze, start, exit_cell):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def restore_path(parent, start, exit_cell):
|
||||
if exit_cell not in parent:
|
||||
return []
|
||||
path, current = [], exit_cell
|
||||
while current is not None:
|
||||
path.append(current)
|
||||
current = parent[current]
|
||||
path.reverse()
|
||||
return path if path and path[0] == start else []
|
||||
|
||||
|
||||
class BFSStrategy(PathFindingStrategy):
|
||||
name = "BFS"
|
||||
|
||||
def find_path(self, maze, start, exit_cell):
|
||||
queue = deque([start])
|
||||
parent = {start: None}
|
||||
visited = 0
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
visited += 1
|
||||
if current == exit_cell:
|
||||
break
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor not in parent:
|
||||
parent[neighbor] = current
|
||||
queue.append(neighbor)
|
||||
self.visited_count = visited
|
||||
return self.restore_path(parent, start, exit_cell)
|
||||
|
||||
|
||||
class DFSStrategy(PathFindingStrategy):
|
||||
name = "DFS"
|
||||
|
||||
def find_path(self, maze, start, exit_cell):
|
||||
stack = [start]
|
||||
parent = {start: None}
|
||||
visited = 0
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
visited += 1
|
||||
if current == exit_cell:
|
||||
break
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor not in parent:
|
||||
parent[neighbor] = current
|
||||
stack.append(neighbor)
|
||||
self.visited_count = visited
|
||||
return self.restore_path(parent, start, exit_cell)
|
||||
|
||||
|
||||
class AStarStrategy(PathFindingStrategy):
|
||||
name = "A*"
|
||||
|
||||
@staticmethod
|
||||
def heuristic(first, second):
|
||||
return abs(first.x - second.x) + abs(first.y - second.y)
|
||||
|
||||
def find_path(self, maze, start, exit_cell):
|
||||
order = count()
|
||||
queue = [(self.heuristic(start, exit_cell), next(order), start)]
|
||||
distance = {start: 0}
|
||||
parent = {start: None}
|
||||
closed = set()
|
||||
while queue:
|
||||
_, _, current = heappop(queue)
|
||||
if current in closed:
|
||||
continue
|
||||
closed.add(current)
|
||||
if current == exit_cell:
|
||||
break
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
new_distance = distance[current] + 1
|
||||
if new_distance < distance.get(neighbor, float("inf")):
|
||||
distance[neighbor] = new_distance
|
||||
parent[neighbor] = current
|
||||
priority = new_distance + self.heuristic(neighbor, exit_cell)
|
||||
heappush(queue, (priority, next(order), neighbor))
|
||||
self.visited_count = len(closed)
|
||||
return self.restore_path(parent, start, exit_cell)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchStats:
|
||||
strategy: str
|
||||
time_ms: float
|
||||
visited_cells: int
|
||||
path_length: int
|
||||
path: list = field(repr=False)
|
||||
|
||||
|
||||
class Observer(ABC):
|
||||
@abstractmethod
|
||||
def update(self, event):
|
||||
pass
|
||||
|
||||
|
||||
class ConsoleView(Observer):
|
||||
def __init__(self, verbose=True):
|
||||
self.verbose = verbose
|
||||
self.events = []
|
||||
|
||||
def update(self, event):
|
||||
self.events.append(event)
|
||||
if self.verbose:
|
||||
print(event["message"])
|
||||
|
||||
def render(self, maze, path=None):
|
||||
path_cells = set(path or [])
|
||||
rows = []
|
||||
for row in maze.cells:
|
||||
symbols = []
|
||||
for cell in row:
|
||||
if cell.is_start:
|
||||
symbols.append("S")
|
||||
elif cell.is_exit:
|
||||
symbols.append("E")
|
||||
elif cell.is_wall:
|
||||
symbols.append("#")
|
||||
elif cell in path_cells:
|
||||
symbols.append(".")
|
||||
else:
|
||||
symbols.append(" ")
|
||||
rows.append("".join(symbols))
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
class MazeSolver:
|
||||
def __init__(self, maze, strategy):
|
||||
self.maze = maze
|
||||
self.strategy = strategy
|
||||
self.observers = []
|
||||
|
||||
def set_strategy(self, strategy):
|
||||
self.strategy = strategy
|
||||
|
||||
def attach(self, observer):
|
||||
if observer not in self.observers:
|
||||
self.observers.append(observer)
|
||||
|
||||
def notify(self, event):
|
||||
for observer in self.observers:
|
||||
observer.update(event)
|
||||
|
||||
def solve(self):
|
||||
self.notify({"type": "search_started", "message": f"Запущен алгоритм {self.strategy.name}"})
|
||||
started = perf_counter()
|
||||
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
|
||||
elapsed_ms = max((perf_counter() - started) * 1000, 1e-9)
|
||||
stats = SearchStats(
|
||||
strategy=self.strategy.name,
|
||||
time_ms=elapsed_ms,
|
||||
visited_cells=self.strategy.visited_count,
|
||||
path_length=max(len(path) - 1, 0),
|
||||
path=path,
|
||||
)
|
||||
event_type = "path_found" if path else "path_not_found"
|
||||
message = f"{self.strategy.name}: путь длиной {stats.path_length}" if path else f"{self.strategy.name}: путь не найден"
|
||||
self.notify({"type": event_type, "message": message, "stats": stats})
|
||||
return stats
|
||||
35
SmirnovVS/docs/data/2-nd-exercize/plot_results.py
Normal file
35
SmirnovVS/docs/data/2-nd-exercize/plot_results.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import csv
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
def create_plot(csv_path="docs/data/maze_results_summary.csv", output_path="docs/data/maze_performance.png"):
|
||||
with Path(csv_path).open(encoding="utf-8-sig") as file:
|
||||
rows = list(csv.DictReader(file))
|
||||
mazes = sorted({row["maze"] for row in rows})
|
||||
strategies = ["BFS", "DFS", "A*"]
|
||||
colors = {"BFS": "#4472C4", "DFS": "#ED7D31", "A*": "#70AD47"}
|
||||
figure, axes = plt.subplots(2, 1, figsize=(12, 9))
|
||||
positions = range(len(mazes))
|
||||
width = 0.24
|
||||
for index, strategy in enumerate(strategies):
|
||||
selected = [next(row for row in rows if row["maze"] == maze and row["strategy"] == strategy) for maze in mazes]
|
||||
offsets = [position + (index - 1) * width for position in positions]
|
||||
axes[0].bar(offsets, [float(row["mean_time_ms"]) for row in selected], width, label=strategy, color=colors[strategy])
|
||||
axes[1].bar(offsets, [float(row["mean_visited_cells"]) for row in selected], width, label=strategy, color=colors[strategy])
|
||||
for axis, ylabel, title in zip(axes, ["Время, мс", "Посещено клеток"], ["Среднее время поиска", "Объём исследования лабиринта"]):
|
||||
axis.set_xticks(list(positions), mazes, rotation=20, ha="right")
|
||||
axis.set_ylabel(ylabel)
|
||||
axis.set_title(title)
|
||||
axis.grid(axis="y", alpha=0.25)
|
||||
axis.legend()
|
||||
figure.suptitle("Сравнение стратегий поиска (средние значения)")
|
||||
figure.tight_layout()
|
||||
figure.savefig(output_path, dpi=180)
|
||||
plt.close(figure)
|
||||
return Path(output_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"График сохранён: {create_plot()}")
|
||||
Loading…
Reference in New Issue
Block a user