import sys import csv from collections import deque import heapq import time import matplotlib.pyplot as plt import numpy as np # ---------- Модель ---------- class Node: def __init__(self, x, y): self.x = x self.y = y self.wall = False self.start_flag = False self.exit_flag = False @property def is_wall(self): return self.wall @is_wall.setter def is_wall(self, val): self.wall = val @property def is_start(self): return self.start_flag @is_start.setter def is_start(self, val): self.start_flag = val @property def is_exit(self): return self.exit_flag @is_exit.setter def is_exit(self, val): self.exit_flag = val def passable(self): return not self.wall class Grid: def __init__(self, w, h): self.w = w self.h = h self.cells = [[Node(x, y) for x in range(w)] for y in range(h)] self.start_node = None self.exit_node = None def get(self, x, y): if 0 <= x < self.w and 0 <= y < self.h: return self.cells[y][x] return None def set_type(self, x, y, typ): cell = self.get(x, y) if not cell: return if typ == 'wall': cell.is_wall = True elif typ == 'start': if self.start_node: self.start_node.is_start = False cell.is_start = True cell.is_wall = False self.start_node = cell elif typ == 'exit': if self.exit_node: self.exit_node.is_exit = False cell.is_exit = True cell.is_wall = False self.exit_node = cell elif typ == 'path': cell.is_wall = False def neighbors(self, node): res = [] dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)] for dx, dy in dirs: nx, ny = node.x + dx, node.y + dy nb = self.get(nx, ny) if nb and nb.passable(): res.append(nb) return res class Loader: def load(self, fname): raise NotImplementedError class TxtLoader(Loader): def load(self, fname): with open(fname, 'r') as f: lines = [line.rstrip('\n') for line in f.readlines()] h = len(lines) w = max(len(line) for line in lines) if h > 0 else 0 start_cnt = 0 exit_cnt = 0 grid = Grid(w, h) for y, line in enumerate(lines): for x, ch in enumerate(line): if ch == "#": grid.set_type(x, y, "wall") elif ch == "S": grid.set_type(x, y, "start") start_cnt += 1 elif ch == "E": grid.set_type(x, y, "exit") exit_cnt += 1 else: grid.set_type(x, y, 'path') if start_cnt != 1 or exit_cnt != 1: raise ValueError(f"Bad maze: S={start_cnt}, E={exit_cnt}") return grid # ---------- Поисковые стратегии ---------- class SearchAlgo: def search(self, grid, start, goal): raise NotImplementedError def _reconstruct(self, parent, start, goal): path = [] cur = goal while cur: path.append(cur) cur = parent.get(cur) path.reverse() return path def visited_count(self): return getattr(self, '_visited_num', 0) class BFSAlgo(SearchAlgo): def search(self, grid, start, goal): q = deque([start]) parent = {start: None} seen = {start} while q: cur = q.popleft() if cur == goal: self._visited_num = len(seen) return self._reconstruct(parent, start, goal) for nb in grid.neighbors(cur): if nb not in seen: seen.add(nb) parent[nb] = cur q.append(nb) self._visited_num = len(seen) return [] class DFSAlgo(SearchAlgo): def search(self, grid, start, goal): stack = [start] parent = {start: None} seen = {start} while stack: cur = stack.pop() if cur == goal: self._visited_num = len(seen) return self._reconstruct(parent, start, goal) for nb in grid.neighbors(cur): if nb not in seen: seen.add(nb) parent[nb] = cur stack.append(nb) self._visited_num = len(seen) return [] class AStarAlgo(SearchAlgo): def _h(self, a, b): return abs(a.x - b.x) + abs(a.y - b.y) def search(self, grid, start, goal): heap = [] cnt = 0 start_f = self._h(start, goal) heapq.heappush(heap, (start_f, cnt, start)) cnt += 1 parent = {} g_score = {start: 0} f_score = {start: start_f} seen = set() while heap: cur_f, _, cur = heapq.heappop(heap) seen.add(cur) if cur == goal: self._visited_num = len(seen) return self._reconstruct(parent, start, goal) if cur_f > f_score.get(cur, float('inf')): continue for nb in grid.neighbors(cur): tentative_g = g_score[cur] + 1 if tentative_g < g_score.get(nb, float('inf')): parent[nb] = cur g_score[nb] = tentative_g new_f = tentative_g + self._h(nb, goal) f_score[nb] = new_f heapq.heappush(heap, (new_f, cnt, nb)) cnt += 1 self._visited_num = len(seen) return [] class Solver: def __init__(self, grid): self.grid = grid self.algo = None def set_algo(self, algo): self.algo = algo def solve(self): if not self.algo: return None t0 = time.perf_counter() path = self.algo.search(self.grid, self.grid.start_node, self.grid.exit_node) t1 = time.perf_counter() elapsed = (t1 - t0) * 1000 return { 'time_ms': elapsed, 'visited_cells': self.algo.visited_count(), 'path_length': len(path) } def experiment(maze_file, algo, runs=5): loader = TxtLoader() grid = loader.load(maze_file) total_t = 0.0 total_v = 0 total_l = 0 for _ in range(runs): s = Solver(grid) s.set_algo(algo) stats = s.solve() if stats: total_t += stats['time_ms'] total_v += stats['visited_cells'] total_l += stats['path_length'] return { 'time_ms': total_t / runs, 'visited_cells': total_v / runs, 'path_length': total_l / runs } def make_plots(results): mazes = list(set(r['maze'] for r in results)) algos = ['BFS', 'DFS', 'AStar'] fig, axes = plt.subplots(1, 3, figsize=(15, 5)) x = np.arange(len(mazes)) width = 0.25 # Время for i, algo in enumerate(algos): times = [] for m in mazes: val = next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == algo), 0) times.append(val) axes[0].bar(x + i * width, times, width, label=algo) axes[0].set_xlabel('Maze') axes[0].set_ylabel('Time (ms)') axes[0].set_title('Execution time') axes[0].set_xticks(x + width) axes[0].set_xticklabels(mazes, rotation=45, ha='right') axes[0].legend() axes[0].grid(True, alpha=0.3) # Посещённые клетки for i, algo in enumerate(algos): visited = [] for m in mazes: val = next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == algo), 0) visited.append(val) axes[1].bar(x + i * width, visited, width, label=algo) axes[1].set_xlabel('Maze') axes[1].set_ylabel('Visited cells') axes[1].set_title('Visited cells comparison') axes[1].set_xticks(x + width) axes[1].set_xticklabels(mazes, rotation=45, ha='right') axes[1].legend() axes[1].grid(True, alpha=0.3) # Длина пути for i, algo in enumerate(algos): lengths = [] for m in mazes: val = next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == algo), 0) lengths.append(val) axes[2].bar(x + i * width, lengths, width, label=algo) axes[2].set_xlabel('Maze') axes[2].set_ylabel('Path length') axes[2].set_title('Path length comparison') axes[2].set_xticks(x + width) axes[2].set_xticklabels(mazes, rotation=45, ha='right') axes[2].legend() axes[2].grid(True, alpha=0.3) plt.tight_layout() plt.savefig('performance_plot.png', dpi=150, bbox_inches='tight') plt.show() if __name__ == "__main__": test_mazes = [ ("maze1.txt", "Small 10x6"), ("maze10x10.txt", "Medium 10x10"), ("maze20x20.txt", "Large 20x20"), ("maze_empty.txt", "Empty 15x15"), ("maze_no_exit.txt", "No exit 10x10") ] algorithms = [ ("BFS", BFSAlgo()), ("DFS", DFSAlgo()), ("AStar", AStarAlgo()) ] results = [] for fname, name in test_mazes: print(f"Benchmarking {name}...") for algo_name, algo in algorithms: try: stat = experiment(fname, algo, runs=3) results.append({ 'maze': name, 'strategy': algo_name, 'time_ms': stat['time_ms'], 'visited_cells': stat['visited_cells'], 'path_length': stat['path_length'] }) print(f" {algo_name}: time={stat['time_ms']:.3f}ms, visited={stat['visited_cells']:.0f}, length={stat['path_length']:.0f}") except Exception as e: print(f" {algo_name}: failed - {e}") results.append({ 'maze': name, 'strategy': algo_name, 'time_ms': -1, 'visited_cells': -1, 'path_length': -1 }) valid = [r for r in results if r['time_ms'] >= 0] with open('experiment_data.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length']) writer.writeheader() writer.writerows(valid) if valid: make_plots(valid) print("\nData saved to experiment_data.csv") print("Plot saved to performance_plot.png")