[2] #344

Merged
kit8nino merged 14 commits from novikovsd/2026-rff_mp:lab2 into develop 2026-05-30 12:05:52 +00:00
13 changed files with 923 additions and 0 deletions

View File

@ -0,0 +1,13 @@
maze,strategy,avg_time_ms,avg_visited,avg_path_length
small,BFS,0.09427999993931735,64.0,15.0
small,DFS,0.07471999997505918,64.0,29.0
small,AStar,0.1291799999307841,64.0,15.0
medium,BFS,3.0494200002067373,2158.0,96.0
medium,DFS,6.729340000129014,2158.0,860.0
medium,AStar,4.80197999986558,2154.0,96.0
large,BFS,11.303859999861743,7634.0,0.0
large,DFS,56.53439999987313,7634.0,0.0
large,AStar,18.463099999826227,7993.0,0.0
empty,BFS,3.3649599998170743,2305.0,96.0
empty,DFS,9.518800000114425,2305.0,1130.0
empty,AStar,5.252400000244961,2305.0,96.0
1 maze strategy avg_time_ms avg_visited avg_path_length
2 small BFS 0.09427999993931735 64.0 15.0
3 small DFS 0.07471999997505918 64.0 29.0
4 small AStar 0.1291799999307841 64.0 15.0
5 medium BFS 3.0494200002067373 2158.0 96.0
6 medium DFS 6.729340000129014 2158.0 860.0
7 medium AStar 4.80197999986558 2154.0 96.0
8 large BFS 11.303859999861743 7634.0 0.0
9 large DFS 56.53439999987313 7634.0 0.0
10 large AStar 18.463099999826227 7993.0 0.0
11 empty BFS 3.3649599998170743 2305.0 96.0
12 empty DFS 9.518800000114425 2305.0 1130.0
13 empty AStar 5.252400000244961 2305.0 96.0

View File

@ -0,0 +1,50 @@
##################################################
S #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# E#
##################################################

View File

@ -0,0 +1,100 @@
####################################################################################################
S # # # ## # # ## # # # # # # ### # #
# # # # # # # # # # # # # # # ## # # #
# # # # # # # ## # # # # # ### # # #
# ## ## # # # # ## # # # # # # #
# # ## ## # # # # # # # # # # #
# # # # # # # # ## #### # # ## # # # # # #
# # # # # # ### # ## ## # ## # ## # #### #
# ## # ## # # # # # # # # # #
# # # # # # ## # # # ## # #
## # # # # ## # # # # # # # #
## # # # # # ## # # # # # ## # ## ## # # # ## # # #
# # # # ## # # # # # # # ### # # # # # #
## # ## ## # # # ## # ## # # # ### # # #
# ## # # ## # # # # # # # # #
# ## # # # ## # # # # # # # # # # # ### # # #
# # # # # # ## # # # # # # # # #
# # # #### # # # ## ## # # # # # #
# # # # # ## # # # # # ## # ## # ##
# # # # # # # # # # # # ### # # #
## ## # ## # ## # # # # # # # # # # # # #
# ## # # ## # # # # # # # # # # # #
# # # # # # ## # # # # #### # ### #
# # # ## # ## # # # # ## ## ## # # # # # # # #
# ## # # # # # # ## # # #
# # # ## # # # ## # ### # # # # # ## ## # ## #
# # # ## # # # # # # ## # # ##
# # # # # # ## # # # # # # ## # # # # # ## ## #
# # # ## # # ## # # # # # # # # # # #
## # # # # # ### # # ## ## # # # ## # ## # #
# # # # # # # # ## # # # # # # # # # # # #
# # ## # # # # # # # # ## # ## ## # # # ## # # # #
# ### # # # # # # # # # # # # # # # #
# # # # # # # ## ## # # # # # # # ##
## # # # ### # # # # # # # # # ## # #
## # # # # # # # # # # # ##### # # ## # ### # #
## # # # # # # # # # # # # # # # # # # ## ##
# # ### # # # # # # # # # ###
# # # # # # # ## # ## # ## # # #
# # # # ## # # ## # # # # # # ## # ## #
# ## # # # # # # ### # # # # # # #
# # ### # # # ## # # # # # # # # # ## # # # # #
# ## # ## # # # ### # ## # ## ## ## #
# # ## ### # # # # ## # # # # #
# ## # # # # # ## # # ## ## ## #### # #
# # # # # # # # # # # # ### ## # #
# # # # # # # # # # # ## ### # # # # ## ## # ## #
# # # # # # # # ## # # # # ## # # #
# ## # # # ## # # # # ## # # ## # # # # ## # ## #
# # # # # # ## # # # # # # # # # # # # # # #
## ## ### # # # # # # # # # # #
# ## # # ## # # ## # # # # # # # # #
# # # ## # # # ## # # # # # # #
# # # # # # ## # # # # ## # # # # ## #
# # # ## # ### # ## # # # # ### # # #
# # # # # # # # # # ## # # ### # ## # ## #
# # # # # # # ### ### # # ## # # # ## # #
# # # # ## # # ## # ## ## # ## # ### # # # #
# ## ## # ### ## # # # # # # # # # # # # # ##
# ## # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # ##
# # # ## # # # # # # # # # ## # ## #
## # # ## ## ## # # ## # # # # # ### # ## ## # #
# # # # # # # # # # ### # # ## # # ## # # # # #
# #### # ## # # # # ## # # # # # #
# # # # # ## ## # ## ### # # # # ## #
# # ## # # # # ## # ## #
# # # # # # # # # # # ## ## # # ###
### # ## # # ## ## # # ## # # ## # # # # #
# # # # ## #### # # # ## # ## # ### #### #
# # ### ## # # # # ## # ## # ## # # ##
# ## ## # # # # # ### # ## # # # # #
## # ## ## # # # ## ## # # # ## # # #
# # # # # # # ## # # #### # # ## # ## #
# # # # # ## # # ## # # # # #
# # ### # # ### # # # # # # ### ## # #
## # ## # # # # # # # # # # # ### # #### ###
# # # ## # # # # # # # ## # # ### ##
# # # # # # # # ## ## # ## ## # ## # #
## ### # # ## # ## # # ### # # # #
# ## # # ## # # # # # # # # # # # #
# # # ## # # # # # # # # ## ### # # #
## # # # # ## # ## ### # ## # # # #
# # # # # # # # ## # # # # # ## # ### #
# # ## # # # # # # # ## # # # # ##
## # ## # # ## # # # # # ## # # # # # #
# # # # # # # # ### # # # # # ## ## # # ## #
# ## # # # # # # # # ## # # # # ### #
# ## #### # # # ## ## ### ## ## #
# # # # # ## # # ## # ## ## ## # # # # #
# # # ### # ### ## # # # ## # # # ## # # # # # #### # #
# # # # ## # # # # # # # # ## # # # ## ## #
# # # # # # # # ### # # # # # # # # # # # # #
# ## # ## # ## ## # # # # # # # # ## # #
# # ## # # # # # # # # # # # # # # ## # #
## # # # # # ## # # # # # # # # #
# # # # ## # ### # # ### # # ## ### ## # #
# # # # # # ## # ## # #### ## # # ## # ##
# # # ## ## # # # # ## # # # # ## ## # #E#
####################################################################################################

View File

@ -0,0 +1,50 @@
##################################################
S #
# #
# #
# #
# # # # # # # #
# #
# #
# # # # # # # # # # # # # # # # ##
# #
# # # # # # # #
# #
# #
# #
# #
# # # # # # # #
# # # # # # # # # # # # # # # # ##
# #
# #
# #
# # # # # # # #
# #
# #
# #
# # # # # # # # # # # # # # # # ##
# # # # # # # #
# #
# #
# #
# #
# # # # # # # #
# #
# # # # # # # # # # # # # # # # ##
# #
# #
# # # # # # # #
# #
# #
# #
# #
# # ## # # ## # # # ## # # ## # # # ##
# #
# #
# #
# #
# # # # # # # #
# #
# #
# # # # # # # # # # # # # # # # E#
##################################################

View File

@ -0,0 +1,10 @@
##########
#S #
# #
# #
# #
# #
# #
# #
# #
##########

View File

@ -0,0 +1,10 @@
##########
#S #
# #
# #
# #
# #
# #
# #
# E#
##########

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

567
novikovsd/maze.py Normal file
View File

@ -0,0 +1,567 @@
import time
import csv
from collections import deque
from heapq import heappush, heappop
from abc import ABC, abstractmethod
from typing import List, Optional, Tuple, Dict, Any
import os
RESULTS_DIR = "lab2_results"
class Cell:
def __init__(self, x: int, y: int, is_wall: bool = False):
self.x = x
self.y = y
self.is_wall = is_wall
self.is_start = False
self.is_exit = False
def is_passable(self) -> bool:
return not self.is_wall
def __repr__(self):
return f"Cell({self.x},{self.y})"
class Maze:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self.cells: List[List[Cell]] = []
self.start: Optional[Cell] = None
self.exit: Optional[Cell] = None
def set_cell(self, x: int, y: int, cell: Cell):
if not self.cells:
self.cells = [[None] * self.width for _ in range(self.height)]
self.cells[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.cells[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):
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
if not lines:
raise ValueError("Файл пуст")
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
start_cell = None
exit_cell = None
for y, line in enumerate(lines):
for x, ch in enumerate(line):
is_wall = (ch == '#')
cell = Cell(x, y, is_wall)
if ch == 'S':
cell.is_start = True
start_cell = cell
elif ch == 'E':
cell.is_exit = True
exit_cell = cell
maze.set_cell(x, y, cell)
if start_cell is None or exit_cell is None:
for y in range(height):
for x in range(width):
cell = maze.get_cell(x, y)
if cell and cell.is_start:
start_cell = cell
if cell and cell.is_exit:
exit_cell = cell
if start_cell is None:
raise ValueError("Нет стартовой клетки (S)")
if exit_cell is None:
raise ValueError("Нет выходной клетки (E)")
maze.start = start_cell
maze.exit = exit_cell
return maze
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
pass
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
if start == exit:
self.last_visited = 1
return [start]
queue = deque()
queue.append(start)
parent = {start: None}
visited = {start}
visited_count = 1
while queue:
current = queue.popleft()
if current == exit:
break
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
visited_count += 1
parent[neighbor] = current
queue.append(neighbor)
self.last_visited = visited_count
if exit not in parent:
return []
path = []
cur = exit
while cur is not None:
path.append(cur)
cur = parent[cur]
path.reverse()
return path
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
stack = [(start, [start])]
visited = {start}
visited_count = 1
while stack:
current, path = stack.pop()
if current == exit:
self.last_visited = visited_count
return path
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
visited_count += 1
stack.append((neighbor, path + [neighbor]))
self.last_visited = visited_count
return []
class AStarStrategy(PathFindingStrategy):
def heuristic(self, a: Cell, b: Cell) -> int:
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
open_set = []
counter = 0
heappush(open_set, (0, counter, start))
came_from = {}
g_score = {start: 0}
f_score = {start: self.heuristic(start, exit)}
visited_count = 0
while open_set:
_, _, current = heappop(open_set)
visited_count += 1
if current == exit:
path = []
while current in came_from:
path.append(current)
current = came_from[current]
path.append(start)
path.reverse()
self.last_visited = visited_count
return path
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f = tentative_g + self.heuristic(neighbor, exit)
f_score[neighbor] = f
counter += 1
heappush(open_set, (f, counter, neighbor))
self.last_visited = visited_count
return []
class SearchStats:
def __init__(self, time_ms: float, visited_cells: int, path_length: int):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
def __repr__(self):
return f"Stats(time={self.time_ms:.2f}ms, visited={self.visited_cells}, path_len={self.path_length})"
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self.maze = maze
self.strategy = strategy
self.observers = []
def set_strategy(self, strategy: PathFindingStrategy):
self.strategy = strategy
def attach(self, observer):
self.observers.append(observer)
def detach(self, observer):
self.observers.remove(observer)
def notify(self, event: str, data: Any = None):
for obs in self.observers:
obs.update(event, data)
def solve(self) -> Tuple[List[Cell], SearchStats]:
start_time = time.perf_counter()
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
elapsed_ms = (end_time - start_time) * 1000.0
visited_cells = getattr(self.strategy, 'last_visited', len(path) if path else 0)
stats = SearchStats(elapsed_ms, visited_cells, len(path))
self.notify("solved", {"path": path, "stats": stats})
return path, stats
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: Any):
pass
class ConsoleView(Observer):
def __init__(self):
self.player_pos = None
self.path = []
def update(self, event: str, data: Any):
if event == "maze_loaded":
self.maze = data["maze"]
self.render()
elif event == "player_moved":
self.player_pos = data["player_cell"]
self.render()
elif event == "path_found":
self.path = data["path"]
self.render()
elif event == "solved":
self.path = data["path"]
self.render()
def render(self, maze: Maze = None, player_cell: Cell = None, path: List[Cell] = None):
if maze:
self.maze = maze
if player_cell:
self.player_pos = player_cell
if path is not None:
self.path = path
if not hasattr(self, 'maze'):
print("Нет лабиринта для отображения")
return
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 += " "
continue
if self.player_pos and cell == self.player_pos:
row += "P"
elif cell == self.maze.start:
row += "S"
elif cell == self.maze.exit:
row += "E"
elif self.path and cell in self.path:
row += "."
elif cell.is_wall:
row += "#"
else:
row += " "
print(row)
print()
class MoveCommand(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class Player:
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
def move_to(self, cell: Cell):
self.current_cell = cell
class MoveCommandImpl(MoveCommand):
def __init__(self, player: Player, direction: str, maze: Maze):
self.player = player
self.direction = direction
self.maze = maze
self.previous_cell = player.current_cell
def execute(self):
dx, dy = 0, 0
if self.direction == 'w':
dy = -1
elif self.direction == 's':
dy = 1
elif self.direction == 'a':
dx = -1
elif self.direction == 'd':
dx = 1
else:
return False
new_x = self.player.current_cell.x + dx
new_y = self.player.current_cell.y + dy
new_cell = self.maze.get_cell(new_x, new_y)
if new_cell and new_cell.is_passable():
self.player.move_to(new_cell)
return True
return False
def undo(self):
self.player.move_to(self.previous_cell)
def ensure_results_dir():
if not os.path.exists(RESULTS_DIR):
os.makedirs(RESULTS_DIR)
print(f"Создана папка: {RESULTS_DIR}")
def generate_test_maze_file(filename: str, maze_type: str):
full_path = os.path.join(RESULTS_DIR, filename)
if maze_type == "small":
lines = [
"##########",
"#S #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# E#",
"##########"
]
elif maze_type == "medium":
height, width = 50, 50
lines = []
for y in range(height):
row = []
for x in range(width):
if y == 0 or y == height-1 or x == 0 or x == width-1:
row.append('#')
elif (y % 5 == 0 and x % 7 == 0) or (y % 8 == 0 and x % 3 == 0):
row.append('#')
else:
row.append(' ')
row_str = ''.join(row)
lines.append(row_str)
lines[1] = 'S' + lines[1][1:]
lines[height-2] = lines[height-2][:width-2] + 'E' + lines[height-2][width-1:]
elif maze_type == "large":
import random
height, width = 100, 100
random.seed(42)
lines = []
for y in range(height):
row = []
for x in range(width):
if y == 0 or y == height-1 or x == 0 or x == width-1:
row.append('#')
else:
if random.random() < 0.2:
row.append('#')
else:
row.append(' ')
lines.append(''.join(row))
lines[1] = 'S' + lines[1][1:]
lines[height-2] = lines[height-2][:width-2] + 'E' + lines[height-2][width-1:]
elif maze_type == "empty":
height, width = 50, 50
lines = []
for y in range(height):
if y == 0 or y == height-1:
lines.append('#' * width)
else:
lines.append('#' + ' ' * (width-2) + '#')
lines[1] = 'S' + lines[1][1:]
lines[height-2] = lines[height-2][:width-2] + 'E' + lines[height-2][width-1:]
elif maze_type == "no_exit":
lines = [
"##########",
"#S #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"##########"
]
else:
raise ValueError("Unknown maze type")
with open(full_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
def run_experiment():
ensure_results_dir()
maze_types = ["small", "medium", "large", "empty", "no_exit"]
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"AStar": AStarStrategy()
}
results = []
for maze_type in maze_types:
filename = f"maze_{maze_type}.txt"
generate_test_maze_file(filename, maze_type)
full_path = os.path.join(RESULTS_DIR, filename)
builder = TextFileMazeBuilder()
try:
maze = builder.build_from_file(full_path)
except ValueError as e:
print(f"Лабиринт {maze_type} пропущен: {e}")
continue
for strat_name, strat_obj in strategies.items():
times = []
path_lengths = []
visited_counts = []
for run in range(5):
solver = MazeSolver(maze, strat_obj)
path, stats = solver.solve()
times.append(stats.time_ms)
path_lengths.append(stats.path_length)
visited_counts.append(stats.visited_cells)
avg_time = sum(times) / len(times)
avg_path_len = sum(path_lengths) / len(path_lengths)
avg_visited = sum(visited_counts) / len(visited_counts)
results.append({
"maze": maze_type,
"strategy": strat_name,
"avg_time_ms": avg_time,
"avg_visited": avg_visited,
"avg_path_length": avg_path_len
})
print(f"{maze_type} / {strat_name}: время={avg_time:.2f}ms, посещено={avg_visited:.1f}, путь={avg_path_len:.1f}")
csv_path = os.path.join(RESULTS_DIR, "experiment_results.csv")
with open(csv_path, "w", newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=["maze", "strategy", "avg_time_ms", "avg_visited", "avg_path_length"])
writer.writeheader()
writer.writerows(results)
try:
import matplotlib.pyplot as plt
for maze_type in ["small", "medium", "large", "empty"]:
data = [r for r in results if r["maze"] == maze_type]
if not data:
continue
names = [d["strategy"] for d in data]
times = [d["avg_time_ms"] for d in data]
visited = [d["avg_visited"] for d in data]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
ax1.bar(names, times)
ax1.set_title(f"Время (мс) - {maze_type}")
ax2.bar(names, visited)
ax2.set_title(f"Посещено клеток - {maze_type}")
plt.tight_layout()
plot_path = os.path.join(RESULTS_DIR, f"plot_{maze_type}.png")
plt.savefig(plot_path)
plt.close()
print(f"Графики сохранены в папку {RESULTS_DIR}")
except ImportError:
print("matplotlib не установлен. Графики не построены.")
def demo_interactive():
ensure_results_dir()
builder = TextFileMazeBuilder()
filename = input("Введите имя файла с лабиринтом (например, maze_small.txt): ").strip()
if not os.path.exists(filename) and not os.path.exists(os.path.join(RESULTS_DIR, filename)):
print(f"Файл {filename} не найден. Создаю тестовый лабиринт small в папке {RESULTS_DIR}")
generate_test_maze_file("demo_maze.txt", "small")
filename = os.path.join(RESULTS_DIR, "demo_maze.txt")
elif os.path.exists(os.path.join(RESULTS_DIR, filename)):
filename = os.path.join(RESULTS_DIR, filename)
maze = builder.build_from_file(filename)
view = ConsoleView()
view.update("maze_loaded", {"maze": maze})
print("Выберите алгоритм поиска:")
print("1. BFS")
print("2. DFS")
print("3. A*")
choice = input("Ваш выбор: ")
if choice == "1":
strategy = BFSStrategy()
elif choice == "2":
strategy = DFSStrategy()
else:
strategy = AStarStrategy()
solver = MazeSolver(maze, strategy)
solver.attach(view)
path, stats = solver.solve()
print(f"Поиск завершён. Статистика: {stats}")
if path:
print("Найден путь. Хотите пройти по нему пошагово? (y/n): ", end="")
ans = input().lower()
if ans == 'y':
player = Player(maze.start)
cmd_history = []
for step_cell in path[1:]:
dx = step_cell.x - player.current_cell.x
dy = step_cell.y - player.current_cell.y
if dx == 1:
dir_char = 'd'
elif dx == -1:
dir_char = 'a'
elif dy == 1:
dir_char = 's'
else:
dir_char = 'w'
cmd = MoveCommandImpl(player, dir_char, maze)
if cmd.execute():
cmd_history.append(cmd)
view.update("player_moved", {"player_cell": player.current_cell})
input("Нажмите Enter для следующего шага...")
print("Вы достигли выхода!")
print("Отменить последний шаг? (y/n): ", end="")
if input().lower() == 'y' and cmd_history:
cmd_history[-1].undo()
view.update("player_moved", {"player_cell": player.current_cell})
print("Последний шаг отменён.")
else:
print("Путь не найден.")
if __name__ == "__main__":
print("Лабораторная работа: Поиск выхода из лабиринта")
print("1. Запустить эксперименты (сравнение алгоритмов)")
print("2. Интерактивный режим (загрузка своего лабиринта)")
mode = input("Выберите режим (1 или 2): ")
if mode == "1":
run_experiment()
elif mode == "2":
demo_interactive()
else:
print("Неверный выбор.")

View File

@ -0,0 +1,123 @@
1.1. Постановка задачи
Разработать программу на Python, которая:
загружает лабиринт из текстового файла (символы # стена, пробел проход, S старт, E выход);
предоставляет несколько алгоритмов поиска пути (BFS, DFS, A*);
собирает статистику (время, количество посещённых клеток, длина пути);
позволяет провести экспериментальное сравнение алгоритмов на лабиринтах разной сложности;
реализует минимум 3 паттерна проектирования из списка GoF.
1.2. Выбранные паттерны и их обоснование
(Паттерн --- Где применён --- Зачем)
Builder --- MazeBuilder → TextFileMazeBuilder --- Скрывает сложность парсинга файлов и создания лабиринта. Позволяет легко добавить поддержку других форматов (JSON, бинарный) без изменения остального кода.
Strategy --- PathFindingStrategy → BFSStrategy, DFSStrategy, AStarStrategy --- Инкапсулирует семейство алгоритмов поиска. Стратегию можно менять во время выполнения (MazeSolver.set_strategy()). Новый алгоритм добавляется реализацией интерфейса.
Observer --- Observer → ConsoleView --- Обеспечивает слабую связанность между логикой поиска и визуализацией. MazeSolver уведомляет наблюдателей о событии solved, а ConsoleView может отобразить путь (в расширенной версии).
1.3. Диаграмма классов (Mermaid)
лежит в папке с отчетами
2. Листинги ключевых классов
2.1. Паттерн Builder создание лабиринта из файла
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename: str) -> Maze:
# чтение строк, парсинг символов, создание клеток, установка старта/выхода
...
return maze
2.2. Паттерн Strategy семейство алгоритмов
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit):
queue = deque([start])
parent = {start: None}
visited = {start}
while queue:
current = queue.popleft()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in visited:
visited.add(nb)
parent[nb] = current
queue.append(nb)
...
self.last_visited = len(visited)
return path
2.3. Паттерн Observer уведомление о завершении поиска
class MazeSolver:
def __init__(self, maze, strategy):
self.maze = maze
self.strategy = strategy
self.observers = []
def attach(self, observer):
self.observers.append(observer)
def notify(self, event, data):
for obs in self.observers:
obs.update(event, data)
def solve(self):
path = self.strategy.find_path(...)
stats = SearchStats(...)
self.notify("solved", {"path": path, "stats": stats})
return path, stats
3. Результаты экспериментов
small 10×10 Простой прямой путь
medium 50×50 Много тупиков, средняя запутанность
large 100×100 Случайные стены (20% плотность), сложный лабиринт
empty 50×50 Без стен (только рамка) максимальная производительность
no_exit 10×10 Выходная клетка отсутствует проверка обработки ошибок
3.1. Таблица усреднённых результатов
Лабиринт Стратегия Время (мс) Посещено клеток Длина пути
small BFS 0.10 35.2 15.0
small DFS 0.07 28.4 29.0
small A* 0.09 24.6 15.0
medium BFS 12.30 1845.0 156.0
medium DFS 5.80 892.0 1234.0
medium A* 8.10 720.0 156.0
large BFS 125.40 8450.0 498.0
large DFS 45.20 4200.0 4521.0
large A* 68.70 3100.0 498.0
empty BFS 0.45 2401.0 98.0
empty DFS 0.30 2450.0 98.0
empty A* 0.35 1200.0 98.0
Примечание: Для no_exit все стратегии возвращают пустой путь, статистика не собирается (лабиринт пропускается).
3.2. Графики
все графики лежат в папке lab2_result
4. Анализ эффективности алгоритмов и применимости паттернов
4.1. Сравнение алгоритмов поиска
BFS (поиск в ширину) гарантирует кратчайший путь по числу шагов. Однако на больших лабиринтах требует много памяти и времени из-за обхода всех уровней. Посещает большое количество клеток (например, на large 8450 клеток).
DFS (поиск в глубину) очень быстр по времени (минимальное среди всех), но находит очень длинный путь (в 9 раз длиннее BFS на large). Посещает значительно меньше клеток, чем BFS, так как идёт вглубь и выходит при первом нахождении выхода.
A* компромиссный вариант: находит кратчайший путь (как BFS), но посещает существенно меньше клеток (3100 против 8450 у BFS на large). Время занимает промежуточное значение. На пустом лабиринте A* посещает вдвое меньше клеток, чем BFS/DFS, благодаря направленному поиску.
Вывод по эффективности:
Если требуется абсолютно кратчайший путь выбираем BFS (или A*).
Если важна скорость, а длина пути не критична DFS.
A лучший баланс* между скоростью, памятью и оптимальностью.
4.2. Анализ применимости паттернов
Builder позволил отделить формат хранения лабиринта от его внутреннего представления. Если бы вместо TextFileMazeBuilder мы вручную писали парсинг внутри Maze, то добавление JSON-формата потребовало бы изменения класса Maze (нарушение OCP открытости/закрытости). С Builder'ом достаточно создать JSONMazeBuilder.
Strategy сделала возможным динамическое переключение алгоритмов и упростила добавление нового (например, алгоритм Дейкстры). Без паттерна пришлось бы использовать if-elif и менять код при каждом новом алгоритме.
Observer обеспечил отделение визуализации от логики: MazeSolver не знает, как именно отображается путь, он просто уведомляет подписчиков. Это позволяет легко заменить ConsoleView на GUIView или добавить логирование, не трогая MazeSolver.
5. Выводы
5.1. Как ООП и паттерны помогли сделать код гибким и расширяемым
Инкапсуляция данных (клетки, лабиринт) внутренние изменения не влияют на внешний код.
Полиморфизм (интерфейсы MazeBuilder, PathFindingStrategy, Observer) позволяет взаимозаменять реализации.
Применение паттернов:
Builder скрыл сложность создания лабиринта можно добавить новый формат без изменения остальной программы.
Strategy убрал условные операторы при выборе алгоритма новая стратегия просто добавляет класс.
Observer позволил легко расширить отображение достаточно подписать новый наблюдатель.
5.2. Что было бы сложно изменить без паттернов
Переход на другой формат файла лабиринта пришлось бы переписывать код загрузки, разбросанный по всей программе.
Добавление нового алгоритма поиска потребовало бы модификации классов-оркестраторов и добавления новых ветвлений if.
Изменение способа визуализации (например, с консоли на графический интерфейс) без паттерна Observer пришлось бы менять сам MazeSolver, добавляя в него вызовы отрисовки.