2026-rff_mp/stepinim/lab2_oop/poisk.py

789 lines
17 KiB
Python
Raw Normal View History

2026-05-20 12:19:09 +00:00
import time
from collections import deque
import heapq
import csv
import os
import random
import matplotlib.pyplot as plt
# ============================================================
# ЭТАП 1. МОДЕЛЬ ЛАБИРИНТА
# ============================================================
class Cell:
def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False):
self.x = x
self.y = y
self.is_wall = is_wall
self.is_start = is_start
self.is_exit = is_exit
self.weight = 1
def isPassable(self):
return not self.is_wall
def __repr__(self):
return f"Cell({self.x},{self.y})"
def __hash__(self):
return hash((self.x, self.y))
def __eq__(self, other):
return isinstance(other, Cell) and self.x == other.x and self.y == other.y
class Maze:
def __init__(self, width, height):
self.width = width
self.height = height
self.cells = []
self.start = None
self.exit = None
def getCell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def getNeighbors(self, cell):
neighbors = []
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
nx = cell.x + dx
ny = cell.y + dy
neighbor = self.getCell(nx, ny)
if neighbor and neighbor.isPassable():
neighbors.append(neighbor)
return neighbors
def getWeightedNeighbors(self, cell):
return [(n, n.weight) for n in self.getNeighbors(cell)]
# ============================================================
# ЭТАП 2. BUILDER
# ============================================================
class MazeBuilder:
def buildFromFile(self, filename):
raise NotImplementedError
class TextFileMazeBuilder(MazeBuilder):
def buildFromFile(self, filename):
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f]
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
for y, line in enumerate(lines):
row = []
for x, char in enumerate(line):
if char == '#':
cell = Cell(x, y, is_wall=True)
elif char == 'S':
cell = Cell(x, y, is_start=True)
maze.start = cell
elif char == 'E':
cell = Cell(x, y, is_exit=True)
maze.exit = cell
else:
cell = Cell(x, y)
row.append(cell)
while len(row) < width:
row.append(Cell(len(row), y, is_wall=True))
maze.cells.append(row)
if maze.start is None or maze.exit is None:
raise ValueError("В лабиринте нет S или E")
return maze
# ============================================================
# ВОССТАНОВЛЕНИЕ ПУТИ
# ============================================================
def reconstruct_path(parents, end_cell):
path = []
current = end_cell
while current is not None:
path.append(current)
current = parents[current]
path.reverse()
return path
# ============================================================
# ЭТАП 3. STRATEGY
# ============================================================
class PathFindingStrategy:
@property
def name(self):
return "Unknown"
def findPath(self, maze, start, exit):
raise NotImplementedError
# ============================================================
# BFS
# ============================================================
class BFSStrategy(PathFindingStrategy):
@property
def name(self):
return "BFS"
def findPath(self, maze, start, exit):
queue = deque([start])
visited = {start}
parents = {
start: None
}
visited_count = 1
while queue:
current = queue.popleft()
if current == exit:
path = reconstruct_path(parents, exit)
return path, visited_count
for neighbor in maze.getNeighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parents[neighbor] = current
visited_count += 1
queue.append(neighbor)
return [], visited_count
# ============================================================
# DFS
# ============================================================
class DFSStrategy(PathFindingStrategy):
@property
def name(self):
return "DFS"
def findPath(self, maze, start, exit):
stack = [start]
visited = {start}
parents = {
start: None
}
visited_count = 1
while stack:
current = stack.pop()
if current == exit:
path = reconstruct_path(parents, exit)
return path, visited_count
for neighbor in maze.getNeighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parents[neighbor] = current
visited_count += 1
stack.append(neighbor)
return [], visited_count
# ============================================================
# A*
# ============================================================
class AStarStrategy(PathFindingStrategy):
@property
def name(self):
return "A*"
def heuristic(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
def findPath(self, maze, start, exit):
counter = 0
open_set = []
heapq.heappush(open_set, (0, counter, start))
parents = {
start: None
}
g_score = {
start: 0
}
visited = set()
visited_count = 0
while open_set:
_, _, current = heapq.heappop(open_set)
if current in visited:
continue
visited.add(current)
visited_count += 1
if current == exit:
path = reconstruct_path(parents, exit)
return path, visited_count
for neighbor in maze.getNeighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
g_score[neighbor] = tentative_g
parents[neighbor] = current
f_score = tentative_g + self.heuristic(neighbor, exit)
counter += 1
heapq.heappush(
open_set,
(f_score, counter, neighbor)
)
return [], visited_count
# ============================================================
# DIJKSTRA
# ============================================================
class DijkstraStrategy(PathFindingStrategy):
@property
def name(self):
return "Dijkstra"
def findPath(self, maze, start, exit):
counter = 0
queue = []
heapq.heappush(queue, (0, counter, start))
distances = {
start: 0
}
parents = {
start: None
}
visited = set()
visited_count = 0
while queue:
dist, _, current = heapq.heappop(queue)
if current in visited:
continue
visited.add(current)
visited_count += 1
if current == exit:
path = reconstruct_path(parents, exit)
return path, visited_count
for neighbor, weight in maze.getWeightedNeighbors(current):
new_dist = dist + weight
if neighbor not in distances or new_dist < distances[neighbor]:
distances[neighbor] = new_dist
parents[neighbor] = current
counter += 1
heapq.heappush(
queue,
(new_dist, counter, neighbor)
)
return [], visited_count
# ============================================================
# ЭТАП 4. STATS + SOLVER
# ============================================================
class SearchStats:
def __init__(
self,
strategy_name,
time_ms,
visited_cells,
path_length,
path_found
):
self.strategy_name = strategy_name
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
self.path_found = path_found
class MazeSolver:
def __init__(self, maze, strategy=None):
self.maze = maze
self.strategy = strategy
def setStrategy(self, strategy):
self.strategy = strategy
def solve(self):
if self.strategy is None:
raise ValueError("Стратегия не выбрана")
start_time = time.perf_counter()
path, visited = self.strategy.findPath(
self.maze,
self.maze.start,
self.maze.exit
)
end_time = time.perf_counter()
elapsed_ms = (end_time - start_time) * 1000
return SearchStats(
self.strategy.name,
elapsed_ms,
visited,
len(path),
len(path) > 0
), path
# ============================================================
# ВИЗУАЛИЗАЦИЯ
# ============================================================
def render(maze, path=None):
path_set = set(path) if path else set()
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 in path_set:
line += "."
elif cell.is_wall:
line += "#"
else:
line += " "
print(line)
print()
# ============================================================
# ФАЙЛЫ И ПУТИ
# ============================================================
OUTPUT_DIR = os.path.join("docs", "data")
PREFIX = "_2lab"
os.makedirs(OUTPUT_DIR, exist_ok=True)
def get_path(filename):
name, ext = os.path.splitext(filename)
return os.path.join(
OUTPUT_DIR,
f"{name}{PREFIX}{ext}"
)
# ============================================================
# СОЗДАНИЕ ЛАБИРИНТА
# ============================================================
def create_test_maze(filename, lines):
with open(filename, 'w', encoding='utf-8') as f:
for line in lines:
f.write(line + '\n')
return filename
# ============================================================
# ГЕНЕРАЦИЯ
# ============================================================
def generate_maze(width, height, wall_density=0.3):
grid = [[' ' for _ in range(width)] for _ in range(height)]
for x in range(width):
grid[0][x] = '#'
grid[height - 1][x] = '#'
for y in range(height):
grid[y][0] = '#'
grid[y][width - 1] = '#'
x, y = 1, 1
path_cells = {(x, y)}
while x < width - 2 or y < height - 2:
if x < width - 2 and random.random() > 0.3:
x += 1
elif y < height - 2:
y += 1
else:
x += 1
path_cells.add((x, y))
for yy in range(1, height - 1):
for xx in range(1, width - 1):
if (xx, yy) not in path_cells:
if random.random() < wall_density:
grid[yy][xx] = '#'
grid[1][1] = 'S'
grid[height - 2][width - 2] = 'E'
return [''.join(row) for row in grid]
def generate_empty_maze(size):
lines = [" " * size for _ in range(size)]
lines[0] = "S" + " " * (size - 1)
lines[size - 1] = " " * (size - 1) + "E"
return lines
def generate_no_exit_maze(size):
lines = generate_maze(size, size, wall_density=0.2)
for y, line in enumerate(lines):
if 'E' in line:
x = line.index('E')
for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
ny = y + dy
nx = x + dx
if 0 <= ny < size and 0 <= nx < size:
if lines[ny][nx] == ' ':
lines[ny] = (
lines[ny][:nx]
+ '#'
+ lines[ny][nx + 1:]
)
return lines
# ============================================================
# ЭКСПЕРИМЕНТЫ
# ============================================================
def run_experiments():
mazes = {
"small": [
"##########",
"#S #",
"# ###### #",
"# # # #",
"# # ## # #",
"# # ## # #",
"# # # #",
"# ###### #",
"# E#",
"##########"
],
"medium": generate_maze(50, 50, 0.35),
"large": generate_maze(100, 100, 0.4),
"empty": generate_empty_maze(20),
"no_exit": generate_no_exit_maze(15)
}
strategies = [
BFSStrategy(),
DFSStrategy(),
AStarStrategy(),
DijkstraStrategy()
]
results = []
print("=" * 60)
print("ЭКСПЕРИМЕНТЫ")
print("=" * 60)
for maze_name, lines in mazes.items():
filename = get_path(f"{maze_name}.txt")
create_test_maze(filename, lines)
maze = TextFileMazeBuilder().buildFromFile(filename)
print(f"\nЛабиринт: {maze_name}")
print("-" * 60)
for strategy in strategies:
times = []
visited_values = []
final_path_len = 0
for _ in range(5):
solver = MazeSolver(maze)
solver.setStrategy(strategy)
stats, path = solver.solve()
times.append(stats.time_ms)
visited_values.append(stats.visited_cells)
final_path_len = stats.path_length
avg_time = sum(times) / len(times)
avg_visited = sum(visited_values) / len(visited_values)
results.append({
"maze": maze_name,
"strategy": strategy.name,
"time_ms": round(avg_time, 4),
"visited": int(avg_visited),
"path_length": final_path_len
})
status = "найден" if final_path_len > 0 else "не найден"
print(
f"{strategy.name:<10} | "
f"{avg_time:>8.4f} мс | "
f"{int(avg_visited):>5} клеток | "
f"путь {status}"
)
csv_path = get_path("results.csv")
with open(csv_path, "w", newline="", encoding='utf-8') as f:
writer = csv.DictWriter(
f,
fieldnames=[
"maze",
"strategy",
"time_ms",
"visited",
"path_length"
]
)
writer.writeheader()
writer.writerows(results)
print(f"\nCSV сохранён: {csv_path}")
return results
# ============================================================
# ГРАФИК
# ============================================================
def build_charts(results):
mazes = list(dict.fromkeys(r["maze"] for r in results))
strategies = list(dict.fromkeys(r["strategy"] for r in results))
fig, ax = plt.subplots(figsize=(12, 6))
x = range(len(mazes))
width = 0.2
colors = {
'BFS': '#3498db',
'DFS': '#e74c3c',
'A*': '#2ecc71',
'Dijkstra': '#f39c12'
}
for i, strategy in enumerate(strategies):
times = [
r["time_ms"]
for r in results
if r["strategy"] == strategy
]
ax.bar(
[j + i * width for j in x],
times,
width,
label=strategy,
color=colors.get(strategy, 'gray')
)
ax.set_xlabel("Лабиринт")
ax.set_ylabel("Время (мс)")
ax.set_title("Сравнение алгоритмов")
ax.set_xticks([j + width * 1.5 for j in x])
ax.set_xticklabels(mazes)
ax.legend()
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
chart_path = get_path("chart_time.png")
plt.savefig(chart_path, dpi=150, bbox_inches='tight')
print(f"График сохранён: {chart_path}")
plt.show()
# ============================================================
# MAIN
# ============================================================
def main():
results = run_experiments()
build_charts(results)
if __name__ == "__main__":
main()