2026-rff_mp/stepinim/lab2_oop/poisk.py
2026-05-20 15:19:09 +03:00

789 lines
17 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.

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()