2026-rff_mp/stepinim/lab2_oop/poisk.py
2026-05-21 13:40:02 +03:00

571 lines
21 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})"
# Хеш по координатам — чтобы класть клетки в set и dict
def __hash__(self):
return hash((self.x, self.y))
# Сравнение двух клеток (нужно для set и dict)
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 = [] # Двумерный список: cells[y][x]
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. ЗАГРУЗКА ЛАБИРИНТА ИЗ ФАЙЛА
# ============================================================
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
# Идём от выхода к старту по цепочке parents
while current is not None:
path.append(current)
current = parents[current]
path.reverse() # Разворачиваем — получаем путь от старта к выходу
return path
# ============================================================
# ЭТАП 3. АЛГОРИТМЫ ПОИСКА ПУТИ
# ============================================================
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
# ============================================================
# ДЕЙКСТРА — поиск с учётом весов клеток
# ============================================================
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
# Здесь используем вес клеток, а не просто +1
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. РЕШАТЕЛЬ И СТАТИСТИКА
# ============================================================
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] = '#'
# Прокладываем гарантированную дорожку от (1,1) до (width-2, height-2)
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, nx = y + dy, 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
# Запускаем 5 раз и считаем среднее время
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} | {avg_time:>8.4f} мс | {int(avg_visited):>5} клеток | путь {status}")
# Сохраняем всё в CSV
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()
# ============================================================
# ГЛАВНАЯ ФУНКЦИЯ
# ============================================================
def main():
results = run_experiments()
build_charts(results)
if __name__ == "__main__":
main()