429 lines
13 KiB
Python
429 lines
13 KiB
Python
|
|
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\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()
|