forked from UNN/2026-rff_mp
Merge pull request '[2] 2-nd-exercise' (#318) from BoriskovaDV/2026-rff_mp:2-nd-exercise into develop
Reviewed-on: UNN/2026-rff_mp#318
This commit is contained in:
commit
52fbd9c2b1
16
BoriskovaDV/docs/data/2-nd-exercise/experiment_results.csv
Normal file
16
BoriskovaDV/docs/data/2-nd-exercise/experiment_results.csv
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
maze,strategy,time_ms,visited_cells,path_length
|
||||||
|
Small 10x6,BFS,0.05722500009142095,25.0,16.0
|
||||||
|
Small 10x6,DFS,0.05680966667872175,24.0,16.0
|
||||||
|
Small 10x6,AStar,0.04801966664066034,23.0,16.0
|
||||||
|
Medium 10x10,BFS,0.04772166676048073,47.0,16.0
|
||||||
|
Medium 10x10,DFS,0.034641333362136116,44.0,30.0
|
||||||
|
Medium 10x10,AStar,0.0983669999641279,47.0,16.0
|
||||||
|
Large 20x20,BFS,0.09949400002066493,100.0,36.0
|
||||||
|
Large 20x20,DFS,0.07004933331700158,75.0,68.0
|
||||||
|
Large 20x20,AStar,0.16450733316257052,85.0,36.0
|
||||||
|
Empty 15x15,BFS,0.13264433331035738,133.0,17.0
|
||||||
|
Empty 15x15,DFS,0.11371733338213137,161.0,89.0
|
||||||
|
Empty 15x15,AStar,0.1543506666621397,65.0,17.0
|
||||||
|
No exit 10x10,BFS,0.04392100011803753,25.0,0.0
|
||||||
|
No exit 10x10,DFS,0.05871466661725814,25.0,0.0
|
||||||
|
No exit 10x10,AStar,0.046440666665148456,25.0,0.0
|
||||||
|
438
BoriskovaDV/docs/data/2-nd-exercise/main.py
Normal file
438
BoriskovaDV/docs/data/2-nd-exercise/main.py
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from collections import deque
|
||||||
|
import heapq
|
||||||
|
import time
|
||||||
|
import csv
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
class GridPoint:
|
||||||
|
def __init__(self, x, y):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.blocked = False
|
||||||
|
self.is_start = False
|
||||||
|
self.is_exit = False
|
||||||
|
|
||||||
|
def can_step(self):
|
||||||
|
return not self.blocked
|
||||||
|
|
||||||
|
class Labyrinth:
|
||||||
|
def __init__(self, w, h):
|
||||||
|
self.w = w
|
||||||
|
self.h = h
|
||||||
|
self.grid = [[GridPoint(x, y) for x in range(w)] for y in range(h)]
|
||||||
|
self.start_point = None
|
||||||
|
self.exit_point = None
|
||||||
|
|
||||||
|
def get_point(self, x, y):
|
||||||
|
if 0 <= x < self.w and 0 <= y < self.h:
|
||||||
|
return self.grid[y][x]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_point(self, x, y, typ):
|
||||||
|
p = self.get_point(x, y)
|
||||||
|
if not p:
|
||||||
|
return
|
||||||
|
if typ == 'wall':
|
||||||
|
p.blocked = True
|
||||||
|
elif typ == 'start':
|
||||||
|
if self.start_point:
|
||||||
|
self.start_point.is_start = False
|
||||||
|
p.is_start = True
|
||||||
|
p.blocked = False
|
||||||
|
self.start_point = p
|
||||||
|
elif typ == 'exit':
|
||||||
|
if self.exit_point:
|
||||||
|
self.exit_point.is_exit = False
|
||||||
|
p.is_exit = True
|
||||||
|
p.blocked = False
|
||||||
|
self.exit_point = p
|
||||||
|
elif typ == 'path':
|
||||||
|
p.blocked = False
|
||||||
|
|
||||||
|
def neighbors(self, p):
|
||||||
|
dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
||||||
|
res = []
|
||||||
|
for dx, dy in dirs:
|
||||||
|
nx, ny = p.x + dx, p.y + dy
|
||||||
|
nb = self.get_point(nx, ny)
|
||||||
|
if nb and nb.can_step():
|
||||||
|
res.append(nb)
|
||||||
|
return res
|
||||||
|
|
||||||
|
class MazeLoader:
|
||||||
|
def load(self, filename):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
class TextMazeLoader(MazeLoader):
|
||||||
|
def load(self, filename):
|
||||||
|
with open(filename, 'r') as f:
|
||||||
|
lines = [line.rstrip('\n') for line in f]
|
||||||
|
h = len(lines)
|
||||||
|
w = max(len(line) for line in lines) if h > 0 else 0
|
||||||
|
start_cnt = 0
|
||||||
|
exit_cnt = 0
|
||||||
|
lab = Labyrinth(w, h)
|
||||||
|
|
||||||
|
for y, line in enumerate(lines):
|
||||||
|
for x, ch in enumerate(line):
|
||||||
|
if ch == '#':
|
||||||
|
lab.set_point(x, y, 'wall')
|
||||||
|
elif ch == 'S':
|
||||||
|
lab.set_point(x, y, 'start')
|
||||||
|
start_cnt += 1
|
||||||
|
elif ch == 'E':
|
||||||
|
lab.set_point(x, y, 'exit')
|
||||||
|
exit_cnt += 1
|
||||||
|
else:
|
||||||
|
lab.set_point(x, y, 'path')
|
||||||
|
if start_cnt != 1 or exit_cnt != 1:
|
||||||
|
raise ValueError(f"Need exactly one S and one E. Found S={start_cnt}, E={exit_cnt}")
|
||||||
|
return lab
|
||||||
|
|
||||||
|
class SearchAlgorithm:
|
||||||
|
def find_way(self, lab, start, goal):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _build_path(self, prev, start, goal):
|
||||||
|
path = []
|
||||||
|
cur = goal
|
||||||
|
while cur:
|
||||||
|
path.append(cur)
|
||||||
|
cur = prev.get(cur)
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
def get_visited(self):
|
||||||
|
return getattr(self, '_visited', 0)
|
||||||
|
|
||||||
|
class BreadthFirst(SearchAlgorithm):
|
||||||
|
def find_way(self, lab, start, goal):
|
||||||
|
q = deque([start])
|
||||||
|
prev = {start: None}
|
||||||
|
seen = {start}
|
||||||
|
while q:
|
||||||
|
cur = q.popleft()
|
||||||
|
if cur == goal:
|
||||||
|
self._visited = len(seen)
|
||||||
|
return self._build_path(prev, start, goal)
|
||||||
|
for nb in lab.neighbors(cur):
|
||||||
|
if nb not in seen:
|
||||||
|
seen.add(nb)
|
||||||
|
prev[nb] = cur
|
||||||
|
q.append(nb)
|
||||||
|
self._visited = len(seen)
|
||||||
|
return []
|
||||||
|
|
||||||
|
class DepthFirst(SearchAlgorithm):
|
||||||
|
def find_way(self, lab, start, goal):
|
||||||
|
stack = [start]
|
||||||
|
prev = {start: None}
|
||||||
|
seen = {start}
|
||||||
|
while stack:
|
||||||
|
cur = stack.pop()
|
||||||
|
if cur == goal:
|
||||||
|
self._visited = len(seen)
|
||||||
|
return self._build_path(prev, start, goal)
|
||||||
|
for nb in lab.neighbors(cur):
|
||||||
|
if nb not in seen:
|
||||||
|
seen.add(nb)
|
||||||
|
prev[nb] = cur
|
||||||
|
stack.append(nb)
|
||||||
|
self._visited = len(seen)
|
||||||
|
return []
|
||||||
|
|
||||||
|
class AStar(SearchAlgorithm):
|
||||||
|
def _dist(self, a, b):
|
||||||
|
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||||
|
|
||||||
|
def find_way(self, lab, start, goal):
|
||||||
|
heap = []
|
||||||
|
cnt = 0
|
||||||
|
start_f = self._dist(start, goal)
|
||||||
|
heapq.heappush(heap, (start_f, cnt, start))
|
||||||
|
cnt += 1
|
||||||
|
prev = {}
|
||||||
|
g = {start: 0}
|
||||||
|
f = {start: start_f}
|
||||||
|
seen = set()
|
||||||
|
while heap:
|
||||||
|
cur_f, _, cur = heapq.heappop(heap)
|
||||||
|
seen.add(cur)
|
||||||
|
if cur == goal:
|
||||||
|
self._visited = len(seen)
|
||||||
|
return self._build_path(prev, start, goal)
|
||||||
|
if cur_f > f.get(cur, float('inf')):
|
||||||
|
continue
|
||||||
|
for nb in lab.neighbors(cur):
|
||||||
|
new_g = g[cur] + 1
|
||||||
|
if new_g < g.get(nb, float('inf')):
|
||||||
|
prev[nb] = cur
|
||||||
|
g[nb] = new_g
|
||||||
|
new_f = new_g + self._dist(nb, goal)
|
||||||
|
f[nb] = new_f
|
||||||
|
heapq.heappush(heap, (new_f, cnt, nb))
|
||||||
|
cnt += 1
|
||||||
|
self._visited = len(seen)
|
||||||
|
return []
|
||||||
|
|
||||||
|
class LabyrinthSolver:
|
||||||
|
def __init__(self, lab):
|
||||||
|
self.lab = lab
|
||||||
|
self.algorithm = None
|
||||||
|
|
||||||
|
def set_algorithm(self, algo):
|
||||||
|
self.algorithm = algo
|
||||||
|
|
||||||
|
def solve(self):
|
||||||
|
if not self.algorithm:
|
||||||
|
return None
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
path = self.algorithm.find_way(self.lab, self.lab.start_point, self.lab.exit_point)
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
ms = (t1 - t0) * 1000
|
||||||
|
return ms, self.algorithm.get_visited(), len(path)
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
def __init__(self, start, lab):
|
||||||
|
self.current = start
|
||||||
|
self.last = None
|
||||||
|
self.lab = lab
|
||||||
|
|
||||||
|
def move(self, cell):
|
||||||
|
if cell and cell.can_step():
|
||||||
|
self.last = self.current
|
||||||
|
self.current = cell
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self.last:
|
||||||
|
self.current, self.last = self.last, None
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
class Command:
|
||||||
|
def do(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
def revert(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
class MoveCommand(Command):
|
||||||
|
def __init__(self, player, dx, dy, lab):
|
||||||
|
self.player = player
|
||||||
|
self.dx = dx
|
||||||
|
self.dy = dy
|
||||||
|
self.lab = lab
|
||||||
|
self.done = False
|
||||||
|
|
||||||
|
def do(self):
|
||||||
|
nx = self.player.current.x + self.dx
|
||||||
|
ny = self.player.current.y + self.dy
|
||||||
|
target = self.lab.get_point(nx, ny)
|
||||||
|
if target and target.can_step():
|
||||||
|
self.player.move(target)
|
||||||
|
self.done = True
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def revert(self):
|
||||||
|
if self.done:
|
||||||
|
self.player.undo()
|
||||||
|
self.done = False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
class InteractiveView:
|
||||||
|
def __init__(self, lab, player):
|
||||||
|
self.lab = lab
|
||||||
|
self.player = player
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
|
print("=" * (self.lab.w * 2 + 4))
|
||||||
|
print(" LABYRINTH (P = player)")
|
||||||
|
print("=" * (self.lab.w * 2 + 4))
|
||||||
|
for y in range(self.lab.h):
|
||||||
|
print(" ", end='')
|
||||||
|
for x in range(self.lab.w):
|
||||||
|
p = self.lab.get_point(x, y)
|
||||||
|
if self.player.current == p:
|
||||||
|
print('P', end=' ')
|
||||||
|
elif p == self.lab.start_point:
|
||||||
|
print('S', end=' ')
|
||||||
|
elif p == self.lab.exit_point:
|
||||||
|
print('E', end=' ')
|
||||||
|
elif p.blocked:
|
||||||
|
print('#', end=' ')
|
||||||
|
else:
|
||||||
|
print('.', end=' ')
|
||||||
|
print()
|
||||||
|
print("=" * (self.lab.w * 2 + 4))
|
||||||
|
print(f" Position: ({self.player.current.x},{self.player.current.y})")
|
||||||
|
print(" Controls: h(left) j(down) k(up) l(right) u=undo q=quit")
|
||||||
|
print(" Auto-search: b=BFS d=DFS a=A*")
|
||||||
|
|
||||||
|
def run_experiment(maze_file, algo, runs=5):
|
||||||
|
loader = TextMazeLoader()
|
||||||
|
lab = loader.load(maze_file)
|
||||||
|
total_ms = 0
|
||||||
|
total_visited = 0
|
||||||
|
total_len = 0
|
||||||
|
for _ in range(runs):
|
||||||
|
solver = LabyrinthSolver(lab)
|
||||||
|
solver.set_algorithm(algo)
|
||||||
|
stats = solver.solve()
|
||||||
|
if stats:
|
||||||
|
ms, vis, plen = stats
|
||||||
|
total_ms += ms
|
||||||
|
total_visited += vis
|
||||||
|
total_len += plen
|
||||||
|
return total_ms / runs, total_visited / runs, total_len / runs
|
||||||
|
|
||||||
|
def generate_plots(results):
|
||||||
|
mazes = list(set([r['maze'] for r in results]))
|
||||||
|
strategies = ['BFS', 'DFS', 'AStar']
|
||||||
|
|
||||||
|
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||||||
|
x = np.arange(len(mazes))
|
||||||
|
width = 0.25
|
||||||
|
|
||||||
|
for i, strat in enumerate(strategies):
|
||||||
|
times = []
|
||||||
|
for maze in mazes:
|
||||||
|
val = next((r['time_ms'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0)
|
||||||
|
times.append(val)
|
||||||
|
axes[0].bar(x + i*width, times, width, label=strat)
|
||||||
|
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, strat in enumerate(strategies):
|
||||||
|
visited = []
|
||||||
|
for maze in mazes:
|
||||||
|
val = next((r['visited_cells'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0)
|
||||||
|
visited.append(val)
|
||||||
|
axes[1].bar(x + i*width, visited, width, label=strat)
|
||||||
|
axes[1].set_xlabel('Maze')
|
||||||
|
axes[1].set_ylabel('Visited Cells')
|
||||||
|
axes[1].set_title('Visited Cells')
|
||||||
|
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, strat in enumerate(strategies):
|
||||||
|
lengths = []
|
||||||
|
for maze in mazes:
|
||||||
|
val = next((r['path_length'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0)
|
||||||
|
lengths.append(val)
|
||||||
|
axes[2].bar(x + i*width, lengths, width, label=strat)
|
||||||
|
axes[2].set_xlabel('Maze')
|
||||||
|
axes[2].set_ylabel('Path Length')
|
||||||
|
axes[2].set_title('Path Length')
|
||||||
|
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_comparison.png', dpi=150, bbox_inches='tight')
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
|
||||||
|
print("Running experiments on all mazes...")
|
||||||
|
maze_files = [
|
||||||
|
("maze/maze1.txt", "Small 10x6"),
|
||||||
|
("maze/maze10x10.txt", "Medium 10x10"),
|
||||||
|
("maze/maze20x20.txt", "Large 20x20"),
|
||||||
|
("maze/maze_empty.txt", "Empty 15x15"),
|
||||||
|
("maze/maze_no_exit.txt", "No exit 10x10")
|
||||||
|
]
|
||||||
|
algorithms = [
|
||||||
|
("BFS", BreadthFirst()),
|
||||||
|
("DFS", DepthFirst()),
|
||||||
|
("AStar", AStar())
|
||||||
|
]
|
||||||
|
results = []
|
||||||
|
for fname, label in maze_files:
|
||||||
|
print(f"Testing {label}...")
|
||||||
|
for aname, algo in algorithms:
|
||||||
|
try:
|
||||||
|
avg_t, avg_v, avg_l = run_experiment(fname, algo, runs=3)
|
||||||
|
results.append({
|
||||||
|
'maze': label,
|
||||||
|
'strategy': aname,
|
||||||
|
'time_ms': avg_t,
|
||||||
|
'visited_cells': avg_v,
|
||||||
|
'path_length': avg_l
|
||||||
|
})
|
||||||
|
print(f" {aname}: time={avg_t:.3f}ms visited={avg_v:.0f} length={avg_l:.0f}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {aname}: ERROR {e}")
|
||||||
|
# save csv
|
||||||
|
with open('experiment_results.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(results)
|
||||||
|
generate_plots(results)
|
||||||
|
print("Done. Results saved to experiment_results.csv and performance_comparison.png")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# else interactive mode
|
||||||
|
loader = TextMazeLoader()
|
||||||
|
lab = loader.load("maze/maze1.txt")
|
||||||
|
player = Player(lab.start_point, lab)
|
||||||
|
view = InteractiveView(lab, player)
|
||||||
|
view.render()
|
||||||
|
|
||||||
|
solver = LabyrinthSolver(lab)
|
||||||
|
history = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
key = input("\n > ").lower()
|
||||||
|
if key == 'q':
|
||||||
|
print("Goodbye!")
|
||||||
|
break
|
||||||
|
elif key == 'b':
|
||||||
|
solver.set_algorithm(BreadthFirst())
|
||||||
|
ms, vis, plen = solver.solve()
|
||||||
|
print(f"BFS: {ms:.3f}ms, visited={vis}, length={plen}")
|
||||||
|
elif key == 'd':
|
||||||
|
solver.set_algorithm(DepthFirst())
|
||||||
|
ms, vis, plen = solver.solve()
|
||||||
|
print(f"DFS: {ms:.3f}ms, visited={vis}, length={plen}")
|
||||||
|
elif key == 'a':
|
||||||
|
solver.set_algorithm(AStar())
|
||||||
|
ms, vis, plen = solver.solve()
|
||||||
|
print(f"A*: {ms:.3f}ms, visited={vis}, length={plen}")
|
||||||
|
elif key in ('h','j','k','l'):
|
||||||
|
moves = {'h': (-1,0), 'l': (1,0), 'k': (0,-1), 'j': (0,1)}
|
||||||
|
dx, dy = moves[key]
|
||||||
|
cmd = MoveCommand(player, dx, dy, lab)
|
||||||
|
if cmd.do():
|
||||||
|
history.append(cmd)
|
||||||
|
view.render()
|
||||||
|
if player.current == lab.exit_point:
|
||||||
|
print("\n*** YOU REACHED THE EXIT! ***")
|
||||||
|
print(f"Total moves: {len(history)}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Can't go there - wall!")
|
||||||
|
elif key == 'u':
|
||||||
|
if history:
|
||||||
|
cmd = history.pop()
|
||||||
|
cmd.revert()
|
||||||
|
view.render()
|
||||||
|
print("Undo last move")
|
||||||
|
else:
|
||||||
|
print("Nothing to undo")
|
||||||
|
else:
|
||||||
|
print("Unknown command")
|
||||||
7
BoriskovaDV/docs/data/2-nd-exercise/maze/maze1.txt
Normal file
7
BoriskovaDV/docs/data/2-nd-exercise/maze/maze1.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
##########
|
||||||
|
#S #
|
||||||
|
# ####### #
|
||||||
|
# # # #
|
||||||
|
# # ### # #
|
||||||
|
# # E #
|
||||||
|
##########
|
||||||
10
BoriskovaDV/docs/data/2-nd-exercise/maze/maze10x10.txt
Normal file
10
BoriskovaDV/docs/data/2-nd-exercise/maze/maze10x10.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
##########
|
||||||
|
#S #
|
||||||
|
# # #### #
|
||||||
|
# # #
|
||||||
|
# #### # #
|
||||||
|
# # #
|
||||||
|
# #### # #
|
||||||
|
# # #
|
||||||
|
# #
|
||||||
|
########E#
|
||||||
21
BoriskovaDV/docs/data/2-nd-exercise/maze/maze20x20.txt
Normal file
21
BoriskovaDV/docs/data/2-nd-exercise/maze/maze20x20.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
####################
|
||||||
|
#S #
|
||||||
|
# ############### #
|
||||||
|
# # # #
|
||||||
|
# # ######### # # #
|
||||||
|
# # # # # # #
|
||||||
|
# # # ##### # # # #
|
||||||
|
# # # # # # # # #
|
||||||
|
# # # # # # # # # #
|
||||||
|
# # # # # # # # #
|
||||||
|
# # # ##### # # # #
|
||||||
|
# # # # # # #
|
||||||
|
# # ######### # # #
|
||||||
|
# # # #
|
||||||
|
# ############### #
|
||||||
|
# #
|
||||||
|
# ############### #
|
||||||
|
# # # #
|
||||||
|
# # ########### # #
|
||||||
|
# E#
|
||||||
|
####################
|
||||||
15
BoriskovaDV/docs/data/2-nd-exercise/maze/maze_empty.txt
Normal file
15
BoriskovaDV/docs/data/2-nd-exercise/maze/maze_empty.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
###############
|
||||||
|
#S #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# E #
|
||||||
|
###############
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
##########
|
||||||
|
#S #
|
||||||
|
# # #
|
||||||
|
# # #### #
|
||||||
|
# # #
|
||||||
|
##########
|
||||||
|
E#########
|
||||||
BIN
BoriskovaDV/docs/data/2-nd-exercise/performance_comparison.png
Normal file
BIN
BoriskovaDV/docs/data/2-nd-exercise/performance_comparison.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
BIN
BoriskovaDV/docs/performance_comparison-2-nd-exercise.png
Normal file
BIN
BoriskovaDV/docs/performance_comparison-2-nd-exercise.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
92
BoriskovaDV/docs/report2.md
Normal file
92
BoriskovaDV/docs/report2.md
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# Отчёт по лабораторной работе: Алгоритмы поиска пути в лабиринте
|
||||||
|
|
||||||
|
## 1. Цель работы
|
||||||
|
|
||||||
|
Разработка программы для загрузки лабиринта из текстового файла, реализации трёх алгоритмов поиска пути (BFS, DFS, A\*) и проведения экспериментального сравнения их эффективности на лабиринтах различной сложности.
|
||||||
|
|
||||||
|
## 2. Структура программы
|
||||||
|
|
||||||
|
Программа написана на Python 3 и состоит из следующих основных классов:
|
||||||
|
|
||||||
|
- `GridPoint` – представление клетки лабиринта (координаты, проходимость, флаги старта/выхода);
|
||||||
|
- `Labyrinth` – модель лабиринта (сетка клеток, методы получения соседей);
|
||||||
|
- `TextMazeLoader` – загрузка лабиринта из файла с символами `#` (стена), `S` (старт), `E` (выход);
|
||||||
|
- `SearchAlgorithm` (и наследники `BreadthFirst`, `DepthFirst`, `AStar`) – реализация алгоритмов поиска;
|
||||||
|
- `LabyrinthSolver` – класс-оркестратор, позволяющий сменить стратегию и измеряющий время выполнения;
|
||||||
|
- `Player`, `Command`, `MoveCommand`, `InteractiveView` – для интерактивного режима с отменой ходов;
|
||||||
|
- функции `run_experiment` и `generate_plots` – для многократных запусков и построения графиков.
|
||||||
|
|
||||||
|
## 3. Описание алгоритмов
|
||||||
|
|
||||||
|
### 3.1 BFS (поиск в ширину)
|
||||||
|
Использует очередь. Гарантирует нахождение кратчайшего пути (по числу шагов). Обходит клетки в порядке увеличения расстояния от старта.
|
||||||
|
|
||||||
|
### 3.2 DFS (поиск в глубину)
|
||||||
|
Использует стек. Идёт «вглубь» по одному пути, не гарантирует кратчайший путь. Обычно быстрее по времени и памяти на больших лабиринтах.
|
||||||
|
|
||||||
|
### 3.3 A* (звездочка)
|
||||||
|
Использует приоритетную очередь и эвристику (манхэттенское расстояние). Оценивает клетку по формуле `f = g + h`, где `g` – пройденное расстояние, `h` – эвристика. Находит оптимальный путь, если эвристика допустима.
|
||||||
|
|
||||||
|
## 4. Методика эксперимента
|
||||||
|
|
||||||
|
Для каждого лабиринта каждый алгоритм запускался 3 раза, результаты усреднялись. Измерялись:
|
||||||
|
- время выполнения (в миллисекундах);
|
||||||
|
- количество посещённых клеток;
|
||||||
|
- длина найденного пути.
|
||||||
|
|
||||||
|
Тестовые лабиринты:
|
||||||
|
|
||||||
|
| Название | Размер | Описание |
|
||||||
|
|----------|--------|-----------|
|
||||||
|
| Small 10x6 | 10×6 | Простой лабиринт с извилистым коридором |
|
||||||
|
| Medium 10x10 | 10×10 | Лабиринт среднего размера с несколькими тупиками |
|
||||||
|
| Large 20x20 | 20×20 | Большой запутанный лабиринт |
|
||||||
|
| Empty 15x15 | 15×15 | Пустой лабиринт без стен (прямая линия от S до E) |
|
||||||
|
| No exit 10x10 | 10×10 | Лабиринт без буквы E (путь отсутствует) |
|
||||||
|
|
||||||
|
## 5. Результаты экспериментов
|
||||||
|
|
||||||
|
| Лабиринт | Алгоритм | Время, мс | Посещено клеток | Длина пути |
|
||||||
|
|----------------|----------|-----------|-----------------|------------|
|
||||||
|
| Small 10x6 | BFS | 0.057 | 25 | 16 |
|
||||||
|
| Small 10x6 | DFS | 0.057 | 24 | 16 |
|
||||||
|
| Small 10x6 | A* | 0.048 | 23 | 16 |
|
||||||
|
| Medium 10x10 | BFS | 0.048 | 47 | 16 |
|
||||||
|
| Medium 10x10 | DFS | 0.035 | 44 | 30 |
|
||||||
|
| Medium 10x10 | A* | 0.098 | 47 | 16 |
|
||||||
|
| Large 20x20 | BFS | 0.099 | 100 | 36 |
|
||||||
|
| Large 20x20 | DFS | 0.070 | 75 | 68 |
|
||||||
|
| Large 20x20 | A* | 0.165 | 85 | 36 |
|
||||||
|
| Empty 15x15 | BFS | 0.133 | 133 | 17 |
|
||||||
|
| Empty 15x15 | DFS | 0.114 | 161 | 89 |
|
||||||
|
| Empty 15x15 | A* | 0.154 | 65 | 17 |
|
||||||
|
| No exit 10x10 | BFS | 0.044 | 25 | 0 |
|
||||||
|
| No exit 10x10 | DFS | 0.059 | 25 | 0 |
|
||||||
|
| No exit 10x10 | A* | 0.046 | 25 | 0 |
|
||||||
|
|
||||||
|
## 6. Анализ результатов
|
||||||
|
|
||||||
|
### 6.1. Нахождение кратчайшего пути
|
||||||
|
- **BFS** и **A*** нашли оптимальные пути во всех лабиринтах, где выход существовал (длина пути совпадает для них в каждом случае).
|
||||||
|
- **DFS** в лабиринтах Medium, Large и Empty дал существенно более длинные пути (30 против 16, 68 против 36, 89 против 17), что характерно для глубинного обхода без эвристики.
|
||||||
|
|
||||||
|
### 6.2. Время выполнения
|
||||||
|
- На малых лабиринтах все алгоритмы работают сопоставимо (0.035–0.099 мс).
|
||||||
|
- На лабиринте Large 20×20 BFS выполнился за 0.099 мс, A* – 0.165 мс (медленнее из-за сложности поддержки очереди с приоритетом), DFS – быстрее всех (0.070 мс).
|
||||||
|
- В пустом лабиринте BFS и A* обошли почти все клетки (133 и 65 посещённых соответственно), но A* за счёт эвристики посетил вдвое меньше клеток, хотя время оказалось чуть выше, чем у BFS (0.154 против 0.133 мс). Это объясняется накладными расходами на вычисление эвристики и управление кучей.
|
||||||
|
|
||||||
|
### 6.3. Количество посещённых клеток
|
||||||
|
- **A*** показал лучшую эффективность в пустом лабиринте (65 посещённых против 133 у BFS и 161 у DFS). В лабиринтах со стенами разница не столь заметна, но A* почти всегда посещал меньше клеток, чем BFS.
|
||||||
|
- **DFS** в среднем посещает меньше клеток, чем BFS, но при этом путь часто неоптимален.
|
||||||
|
- **BFS** вынужден обходить всю область равных расстояний, поэтому посещённых клеток обычно больше.
|
||||||
|
|
||||||
|
### 6.4. Поведение при отсутствии выхода
|
||||||
|
Все алгоритмы корректно завершились, вернув пустой путь (длина 0). В лабиринте без выхода BFS, DFS и A* посетили 25 клеток – это все доступные клетки.
|
||||||
|
|
||||||
|
## 7. Выводы
|
||||||
|
|
||||||
|
1. **BFS** надёжен для поиска кратчайшего пути, но может быть медленнее на больших открытых пространствах из-за широкого обхода.
|
||||||
|
2. **DFS** – самый быстрый по времени и экономный по памяти, но не гарантирует оптимальность пути. Его применение оправдано, когда любой путь подходит.
|
||||||
|
3. **A*** демонстрирует лучший баланс: находит кратчайший путь и при этом посещает меньше клеток, чем BFS. Небольшое замедление на сложных лабиринтах компенсируется меньшим числом обработанных клеток.
|
||||||
|
4. Программа успешно справляется с лабиринтами разного размера и конфигурации, включая отсутствие выхода.
|
||||||
|
5. Интерактивный режим с отменой ходов (паттерн Command) и выбором алгоритма (паттерн Strategy) реализован и работает корректно.
|
||||||
Loading…
Reference in New Issue
Block a user