2026-rff_mp/stepinim/lab2_oop/poisk.py

571 lines
21 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
2026-05-21 10:40:02 +00:00
self.weight = 1 # Вес клетки (нужен для Дейкстры)
2026-05-20 12:19:09 +00:00
2026-05-21 10:40:02 +00:00
# Можно ли пройти через клетку
2026-05-20 12:19:09 +00:00
def isPassable(self):
return not self.is_wall
def __repr__(self):
return f"Cell({self.x},{self.y})"
2026-05-21 10:40:02 +00:00
# Хеш по координатам — чтобы класть клетки в set и dict
2026-05-20 12:19:09 +00:00
def __hash__(self):
return hash((self.x, self.y))
2026-05-21 10:40:02 +00:00
# Сравнение двух клеток (нужно для set и dict)
2026-05-20 12:19:09 +00:00
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
2026-05-21 10:40:02 +00:00
self.cells = [] # Двумерный список: cells[y][x]
2026-05-20 12:19:09 +00:00
self.start = None
self.exit = None
2026-05-21 10:40:02 +00:00
# Получить клетку по координатам, если она в границах лабиринта
2026-05-20 12:19:09 +00:00
def getCell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
2026-05-21 10:40:02 +00:00
# Получить всех соседей клетки (вверх, вниз, влево, вправо), кроме стен
2026-05-20 12:19:09 +00:00
def getNeighbors(self, cell):
neighbors = []
2026-05-21 10:40:02 +00:00
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]: # Четыре направления
2026-05-20 12:19:09 +00:00
nx = cell.x + dx
ny = cell.y + dy
neighbor = self.getCell(nx, ny)
if neighbor and neighbor.isPassable():
neighbors.append(neighbor)
return neighbors
2026-05-21 10:40:02 +00:00
# То же самое, но возвращает пары (сосед, вес) — для Дейкстры
2026-05-20 12:19:09 +00:00
def getWeightedNeighbors(self, cell):
return [(n, n.weight) for n in self.getNeighbors(cell)]
# ============================================================
2026-05-21 10:40:02 +00:00
# ЭТАП 2. ЗАГРУЗКА ЛАБИРИНТА ИЗ ФАЙЛА
2026-05-20 12:19:09 +00:00
# ============================================================
class MazeBuilder:
def buildFromFile(self, filename):
raise NotImplementedError
class TextFileMazeBuilder(MazeBuilder):
def buildFromFile(self, filename):
2026-05-21 10:40:02 +00:00
# Читаем файл, убираем переносы строк
2026-05-20 12:19:09 +00:00
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f]
height = len(lines)
2026-05-21 10:40:02 +00:00
width = max(len(line) for line in lines) # Берём самую длинную строку
2026-05-20 12:19:09 +00:00
maze = Maze(width, height)
2026-05-21 10:40:02 +00:00
# Разбираем каждый символ в клетку
2026-05-20 12:19:09 +00:00
for y, line in enumerate(lines):
row = []
for x, char in enumerate(line):
if char == '#':
2026-05-21 10:40:02 +00:00
cell = Cell(x, y, is_wall=True) # Стена
2026-05-20 12:19:09 +00:00
elif char == 'S':
cell = Cell(x, y, is_start=True)
2026-05-21 10:40:02 +00:00
maze.start = cell # Запомнили старт
2026-05-20 12:19:09 +00:00
elif char == 'E':
cell = Cell(x, y, is_exit=True)
2026-05-21 10:40:02 +00:00
maze.exit = cell # Запомнили выход
2026-05-20 12:19:09 +00:00
else:
2026-05-21 10:40:02 +00:00
cell = Cell(x, y) # Пустая клетка
2026-05-20 12:19:09 +00:00
row.append(cell)
2026-05-21 10:40:02 +00:00
# Если строка короче ширины — добиваем стенами
2026-05-20 12:19:09 +00:00
while len(row) < width:
row.append(Cell(len(row), y, is_wall=True))
maze.cells.append(row)
2026-05-21 10:40:02 +00:00
# Проверяем, что старт и выход есть
2026-05-20 12:19:09 +00:00
if maze.start is None or maze.exit is None:
raise ValueError("В лабиринте нет S или E")
return maze
# ============================================================
2026-05-21 10:40:02 +00:00
# ВОССТАНОВЛЕНИЕ ПУТИ ПО СЛОВАРЮ РОДИТЕЛЕЙ
2026-05-20 12:19:09 +00:00
# ============================================================
def reconstruct_path(parents, end_cell):
path = []
current = end_cell
2026-05-21 10:40:02 +00:00
# Идём от выхода к старту по цепочке parents
2026-05-20 12:19:09 +00:00
while current is not None:
path.append(current)
current = parents[current]
2026-05-21 10:40:02 +00:00
path.reverse() # Разворачиваем — получаем путь от старта к выходу
2026-05-20 12:19:09 +00:00
return path
# ============================================================
2026-05-21 10:40:02 +00:00
# ЭТАП 3. АЛГОРИТМЫ ПОИСКА ПУТИ
2026-05-20 12:19:09 +00:00
# ============================================================
class PathFindingStrategy:
@property
def name(self):
return "Unknown"
def findPath(self, maze, start, exit):
raise NotImplementedError
# ============================================================
2026-05-21 10:40:02 +00:00
# BFS — обход в ширину (очередь)
2026-05-20 12:19:09 +00:00
# ============================================================
class BFSStrategy(PathFindingStrategy):
@property
def name(self):
return "BFS"
def findPath(self, maze, start, exit):
2026-05-21 10:40:02 +00:00
queue = deque([start]) # Очередь: кто первый зашёл — первый вышел
2026-05-20 12:19:09 +00:00
visited = {start}
2026-05-21 10:40:02 +00:00
parents = {start: None} # Откуда пришли в клетку
2026-05-20 12:19:09 +00:00
visited_count = 1
while queue:
2026-05-21 10:40:02 +00:00
current = queue.popleft() # Берём из начала очереди
2026-05-20 12:19:09 +00:00
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
2026-05-21 10:40:02 +00:00
queue.append(neighbor) # Кладём в конец очереди
2026-05-20 12:19:09 +00:00
return [], visited_count
# ============================================================
2026-05-21 10:40:02 +00:00
# DFS — обход в глубину (стек)
2026-05-20 12:19:09 +00:00
# ============================================================
class DFSStrategy(PathFindingStrategy):
@property
def name(self):
return "DFS"
def findPath(self, maze, start, exit):
2026-05-21 10:40:02 +00:00
stack = [start] # Стек: кто последний зашёл — первый вышел
2026-05-20 12:19:09 +00:00
visited = {start}
2026-05-21 10:40:02 +00:00
parents = {start: None}
2026-05-20 12:19:09 +00:00
visited_count = 1
while stack:
2026-05-21 10:40:02 +00:00
current = stack.pop() # Берём с вершины стека
2026-05-20 12:19:09 +00:00
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
2026-05-21 10:40:02 +00:00
stack.append(neighbor) # Кладём на вершину стека
2026-05-20 12:19:09 +00:00
return [], visited_count
# ============================================================
2026-05-21 10:40:02 +00:00
# A* — поиск с подсказкой (эвристикой)
2026-05-20 12:19:09 +00:00
# ============================================================
class AStarStrategy(PathFindingStrategy):
@property
def name(self):
return "A*"
2026-05-21 10:40:02 +00:00
# Подсказка: примерное расстояние до выхода (по прямой)
2026-05-20 12:19:09 +00:00
def heuristic(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
def findPath(self, maze, start, exit):
2026-05-21 10:40:02 +00:00
counter = 0 # Чтобы различать клетки с одинаковым приоритетом
open_set = [] # Куча: всегда берём самую перспективную клетку
2026-05-20 12:19:09 +00:00
heapq.heappush(open_set, (0, counter, start))
2026-05-21 10:40:02 +00:00
parents = {start: None}
g_score = {start: 0} # Пройденное расстояние от старта
2026-05-20 12:19:09 +00:00
visited = set()
visited_count = 0
while open_set:
2026-05-21 10:40:02 +00:00
_, _, current = heapq.heappop(open_set) # Достаём клетку с лучшей оценкой
2026-05-20 12:19:09 +00:00
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):
2026-05-21 10:40:02 +00:00
tentative_g = g_score[current] + 1 # Расстояние до соседа через текущую
2026-05-20 12:19:09 +00:00
if neighbor not in g_score or tentative_g < g_score[neighbor]:
g_score[neighbor] = tentative_g
parents[neighbor] = current
2026-05-21 10:40:02 +00:00
# Оценка клетки = пройденный путь + подсказка до выхода
2026-05-20 12:19:09 +00:00
f_score = tentative_g + self.heuristic(neighbor, exit)
counter += 1
2026-05-21 10:40:02 +00:00
heapq.heappush(open_set, (f_score, counter, neighbor))
2026-05-20 12:19:09 +00:00
return [], visited_count
# ============================================================
2026-05-21 10:40:02 +00:00
# ДЕЙКСТРА — поиск с учётом весов клеток
2026-05-20 12:19:09 +00:00
# ============================================================
class DijkstraStrategy(PathFindingStrategy):
@property
def name(self):
return "Dijkstra"
def findPath(self, maze, start, exit):
counter = 0
2026-05-21 10:40:02 +00:00
queue = [] # Куча: всегда берём клетку с кратчайшим путём от старта
2026-05-20 12:19:09 +00:00
heapq.heappush(queue, (0, counter, start))
2026-05-21 10:40:02 +00:00
distances = {start: 0} # Кратчайшее известное расстояние до каждой клетки
parents = {start: None}
2026-05-20 12:19:09 +00:00
visited = set()
visited_count = 0
while queue:
2026-05-21 10:40:02 +00:00
dist, _, current = heapq.heappop(queue) # Достаём ближайшую клетку
2026-05-20 12:19:09 +00:00
if current in visited:
continue
visited.add(current)
visited_count += 1
if current == exit:
path = reconstruct_path(parents, exit)
return path, visited_count
2026-05-21 10:40:02 +00:00
# Здесь используем вес клеток, а не просто +1
2026-05-20 12:19:09 +00:00
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
2026-05-21 10:40:02 +00:00
heapq.heappush(queue, (new_dist, counter, neighbor))
2026-05-20 12:19:09 +00:00
return [], visited_count
# ============================================================
2026-05-21 10:40:02 +00:00
# ЭТАП 4. РЕШАТЕЛЬ И СТАТИСТИКА
2026-05-20 12:19:09 +00:00
# ============================================================
class SearchStats:
2026-05-21 10:40:02 +00:00
def __init__(self, strategy_name, time_ms, visited_cells, path_length, path_found):
2026-05-20 12:19:09 +00:00
self.strategy_name = strategy_name
2026-05-21 10:40:02 +00:00
self.time_ms = time_ms # Время в миллисекундах
self.visited_cells = visited_cells # Сколько клеток посетили
self.path_length = path_length # Длина найденного пути
self.path_found = path_found # Нашли путь или нет
2026-05-20 12:19:09 +00:00
class MazeSolver:
def __init__(self, maze, strategy=None):
self.maze = maze
self.strategy = strategy
2026-05-21 10:40:02 +00:00
# Сменить алгоритм поиска
2026-05-20 12:19:09 +00:00
def setStrategy(self, strategy):
self.strategy = strategy
def solve(self):
if self.strategy is None:
raise ValueError("Стратегия не выбрана")
2026-05-21 10:40:02 +00:00
# Засекаем время и запускаем алгоритм
2026-05-20 12:19:09 +00:00
start_time = time.perf_counter()
2026-05-21 10:40:02 +00:00
path, visited = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit)
2026-05-20 12:19:09 +00:00
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
# ============================================================
2026-05-21 10:40:02 +00:00
# ВЫВОД ЛАБИРИНТА В КОНСОЛЬ
2026-05-20 12:19:09 +00:00
# ============================================================
def render(maze, path=None):
2026-05-21 10:40:02 +00:00
path_set = set(path) if path else set() # Для быстрой проверки "клетка на пути?"
2026-05-20 12:19:09 +00:00
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:
2026-05-21 10:40:02 +00:00
line += "." # Точка — клетка пути
2026-05-20 12:19:09 +00:00
elif cell.is_wall:
line += "#"
else:
line += " "
print(line)
print()
# ============================================================
2026-05-21 10:40:02 +00:00
# ПУТИ ДЛЯ СОХРАНЕНИЯ ФАЙЛОВ
2026-05-20 12:19:09 +00:00
# ============================================================
OUTPUT_DIR = os.path.join("docs", "data")
PREFIX = "_2lab"
2026-05-21 10:40:02 +00:00
os.makedirs(OUTPUT_DIR, exist_ok=True) # Создаём папку, если её нет
2026-05-20 12:19:09 +00:00
def get_path(filename):
name, ext = os.path.splitext(filename)
2026-05-21 10:40:02 +00:00
return os.path.join(OUTPUT_DIR, f"{name}{PREFIX}{ext}")
2026-05-20 12:19:09 +00:00
# ============================================================
2026-05-21 10:40:02 +00:00
# СОЗДАНИЕ ЛАБИРИНТА ИЗ СПИСКА СТРОК
2026-05-20 12:19:09 +00:00
# ============================================================
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
# ============================================================
2026-05-21 10:40:02 +00:00
# ГЕНЕРАЦИЯ ЛАБИРИНТОВ
2026-05-20 12:19:09 +00:00
# ============================================================
2026-05-21 10:40:02 +00:00
# Случайный лабиринт с гарантированным путём
2026-05-20 12:19:09 +00:00
def generate_maze(width, height, wall_density=0.3):
grid = [[' ' for _ in range(width)] for _ in range(height)]
2026-05-21 10:40:02 +00:00
# Ставим стены по краям
2026-05-20 12:19:09 +00:00
for x in range(width):
grid[0][x] = '#'
grid[height - 1][x] = '#'
for y in range(height):
grid[y][0] = '#'
grid[y][width - 1] = '#'
2026-05-21 10:40:02 +00:00
# Прокладываем гарантированную дорожку от (1,1) до (width-2, height-2)
2026-05-20 12:19:09 +00:00
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))
2026-05-21 10:40:02 +00:00
# Случайно расставляем стены, но не на дорожке
2026-05-20 12:19:09 +00:00
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] = '#'
2026-05-21 10:40:02 +00:00
# Ставим старт и выход по углам
2026-05-20 12:19:09 +00:00
grid[1][1] = 'S'
grid[height - 2][width - 2] = 'E'
return [''.join(row) for row in grid]
2026-05-21 10:40:02 +00:00
# Пустой лабиринт без стен
2026-05-20 12:19:09 +00:00
def generate_empty_maze(size):
lines = [" " * size for _ in range(size)]
lines[0] = "S" + " " * (size - 1)
lines[size - 1] = " " * (size - 1) + "E"
return lines
2026-05-21 10:40:02 +00:00
# Лабиринт, где выход замурован со всех сторон
2026-05-20 12:19:09 +00:00
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')
2026-05-21 10:40:02 +00:00
# Окружаем выход стенами
2026-05-20 12:19:09 +00:00
for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
2026-05-21 10:40:02 +00:00
ny, nx = y + dy, x + dx
2026-05-20 12:19:09 +00:00
if 0 <= ny < size and 0 <= nx < size:
if lines[ny][nx] == ' ':
2026-05-21 10:40:02 +00:00
lines[ny] = lines[ny][:nx] + '#' + lines[ny][nx + 1:]
2026-05-20 12:19:09 +00:00
return lines
# ============================================================
2026-05-21 10:40:02 +00:00
# ЗАПУСК ЭКСПЕРИМЕНТОВ
2026-05-20 12:19:09 +00:00
# ============================================================
def run_experiments():
2026-05-21 10:40:02 +00:00
# Набор лабиринтов для тестов
2026-05-20 12:19:09 +00:00
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)
}
2026-05-21 10:40:02 +00:00
# Список алгоритмов
2026-05-20 12:19:09 +00:00
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
2026-05-21 10:40:02 +00:00
# Запускаем 5 раз и считаем среднее время
2026-05-20 12:19:09 +00:00
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 "не найден"
2026-05-21 10:40:02 +00:00
print(f"{strategy.name:<10} | {avg_time:>8.4f} мс | {int(avg_visited):>5} клеток | путь {status}")
2026-05-20 12:19:09 +00:00
2026-05-21 10:40:02 +00:00
# Сохраняем всё в CSV
2026-05-20 12:19:09 +00:00
csv_path = get_path("results.csv")
with open(csv_path, "w", newline="", encoding='utf-8') as f:
2026-05-21 10:40:02 +00:00
writer = csv.DictWriter(f, fieldnames=["maze", "strategy", "time_ms", "visited", "path_length"])
2026-05-20 12:19:09 +00:00
writer.writeheader()
writer.writerows(results)
print(f"\nCSV сохранён: {csv_path}")
return results
# ============================================================
2026-05-21 10:40:02 +00:00
# ПОСТРОЕНИЕ ГРАФИКА
2026-05-20 12:19:09 +00:00
# ============================================================
def build_charts(results):
2026-05-21 10:40:02 +00:00
mazes = list(dict.fromkeys(r["maze"] for r in results)) # Список лабиринтов без повторов
strategies = list(dict.fromkeys(r["strategy"] for r in results)) # Список стратегий без повторов
2026-05-20 12:19:09 +00:00
fig, ax = plt.subplots(figsize=(12, 6))
x = range(len(mazes))
2026-05-21 10:40:02 +00:00
width = 0.2 # Ширина одного столбика
2026-05-20 12:19:09 +00:00
2026-05-21 10:40:02 +00:00
# Цвета для каждого алгоритма
colors = {'BFS': '#3498db', 'DFS': '#e74c3c', 'A*': '#2ecc71', 'Dijkstra': '#f39c12'}
2026-05-20 12:19:09 +00:00
for i, strategy in enumerate(strategies):
2026-05-21 10:40:02 +00:00
# Берём время этой стратегии для всех лабиринтов
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'))
2026-05-20 12:19:09 +00:00
ax.set_xlabel("Лабиринт")
ax.set_ylabel("Время (мс)")
ax.set_title("Сравнение алгоритмов")
2026-05-21 10:40:02 +00:00
ax.set_xticks([j + width * 1.5 for j in x]) # Подписи по центру группы
2026-05-20 12:19:09 +00:00
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()
# ============================================================
2026-05-21 10:40:02 +00:00
# ГЛАВНАЯ ФУНКЦИЯ
2026-05-20 12:19:09 +00:00
# ============================================================
def main():
results = run_experiments()
build_charts(results)
if __name__ == "__main__":
main()