forked from UNN/2026-rff_mp
[2] Final
This commit is contained in:
parent
bb1a35103e
commit
b6c595a11a
|
|
@ -0,0 +1,13 @@
|
||||||
|
maze,strategy,time_ms,visited_cells,path_length
|
||||||
|
Small 10x6,BFS,0.04046166759508196,27.0,14.0
|
||||||
|
Small 10x6,DFS,0.02375933339256638,27.0,18.0
|
||||||
|
Small 10x6,AStar,0.051083666524694614,19.0,14.0
|
||||||
|
Medium 10x10,BFS,0.02262299979823486,19.0,12.0
|
||||||
|
Medium 10x10,DFS,0.016091333236545324,18.0,12.0
|
||||||
|
Medium 10x10,AStar,0.03017666616263644,12.0,12.0
|
||||||
|
Large 20x20,BFS,0.015730000086477958,16.0,5.0
|
||||||
|
Large 20x20,DFS,0.014211666590805786,17.0,9.0
|
||||||
|
Large 20x20,AStar,0.020270666330664728,9.0,5.0
|
||||||
|
Empty 15x15,BFS,0.10161799946217798,78.0,15.0
|
||||||
|
Empty 15x15,DFS,0.04646399975172244,76.0,43.0
|
||||||
|
Empty 15x15,AStar,0.13135433376495106,63.0,15.0
|
||||||
|
|
|
@ -119,27 +119,27 @@ class MazeBuilder:
|
||||||
class TextFileMazeBuilder(MazeBuilder):
|
class TextFileMazeBuilder(MazeBuilder):
|
||||||
def build_from_file(self, filename):
|
def build_from_file(self, filename):
|
||||||
with open(filename, 'r') as f:
|
with open(filename, 'r') as f:
|
||||||
lines = [line.rstrip('\n')for line in f.readlines()]
|
lines = [line.rstrip('\n') for line in f.readlines()]
|
||||||
height = len(lines)
|
height = len(lines)
|
||||||
width = max(len(line) for line in lines) if height > 0 else 0
|
width = max(len(line) for line in lines) if height > 0 else 0
|
||||||
start_en = 0
|
start_en = 0
|
||||||
exit_en = 0
|
exit_en = 0
|
||||||
maze = Maze(width, height)
|
maze = Maze(width, height)
|
||||||
|
|
||||||
for y,line in enumerate(lines):
|
for y, line in enumerate(lines):
|
||||||
for x, ch in enumerate(line):
|
for x, ch in enumerate(line):
|
||||||
if ch == "#":
|
if ch == "#":
|
||||||
maze.set_cell(x,y,"wall")
|
maze.set_cell(x, y, "wall")
|
||||||
elif ch == "S":
|
elif ch == "S":
|
||||||
maze.set_cell(x,y,"start")
|
maze.set_cell(x, y, "start")
|
||||||
start_en+=1
|
start_en += 1
|
||||||
elif ch == "E":
|
elif ch == "E":
|
||||||
maze.set_cell(x,y,"exit")
|
maze.set_cell(x, y, "exit")
|
||||||
exit_en+=1
|
exit_en += 1
|
||||||
else:
|
else:
|
||||||
maze.set_cell(x, y, 'path')
|
maze.set_cell(x, y, 'path')
|
||||||
if start_en > 1 or exit_en > 1 or start_en==0 or exit_en ==0:
|
if start_en != 1 or exit_en != 1:
|
||||||
sys.exit("Error while reading file(you have too many or no match start and exits)")
|
raise ValueError(f"Labirint must have one S and one E. Found: S={start_en}, E={exit_en}")
|
||||||
return maze
|
return maze
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -156,6 +156,9 @@ class PathFindingStrategy:
|
||||||
path.reverse()
|
path.reverse()
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
def get_visited_count(self):
|
||||||
|
return getattr(self, '_visited_count', 0)
|
||||||
|
|
||||||
|
|
||||||
class BFSStrategy(PathFindingStrategy):
|
class BFSStrategy(PathFindingStrategy):
|
||||||
def find_path(self, maze, start, exit_cell):
|
def find_path(self, maze, start, exit_cell):
|
||||||
|
|
@ -167,12 +170,14 @@ class BFSStrategy(PathFindingStrategy):
|
||||||
while queue:
|
while queue:
|
||||||
current = queue.popleft()
|
current = queue.popleft()
|
||||||
if current == exit_cell:
|
if current == exit_cell:
|
||||||
|
self._visited_count = len(visited)
|
||||||
return self._reconstruct_path(came_from, start, exit_cell)
|
return self._reconstruct_path(came_from, start, exit_cell)
|
||||||
for neighbor in maze.get_neighbors(current):
|
for neighbor in maze.get_neighbors(current):
|
||||||
if neighbor not in visited:
|
if neighbor not in visited:
|
||||||
visited.add(neighbor)
|
visited.add(neighbor)
|
||||||
came_from[neighbor] = current
|
came_from[neighbor] = current
|
||||||
queue.append(neighbor)
|
queue.append(neighbor)
|
||||||
|
self._visited_count = len(visited)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -185,12 +190,14 @@ class DFSStrategy(PathFindingStrategy):
|
||||||
while stack:
|
while stack:
|
||||||
current = stack.pop()
|
current = stack.pop()
|
||||||
if current == exit_cell:
|
if current == exit_cell:
|
||||||
|
self._visited_count = len(visited)
|
||||||
return self._reconstruct_path(came_from, start, exit_cell)
|
return self._reconstruct_path(came_from, start, exit_cell)
|
||||||
for neighbor in maze.get_neighbors(current):
|
for neighbor in maze.get_neighbors(current):
|
||||||
if neighbor not in visited:
|
if neighbor not in visited:
|
||||||
visited.add(neighbor)
|
visited.add(neighbor)
|
||||||
came_from[neighbor] = current
|
came_from[neighbor] = current
|
||||||
stack.append(neighbor)
|
stack.append(neighbor)
|
||||||
|
self._visited_count = len(visited)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -208,10 +215,14 @@ class AStarStrategy(PathFindingStrategy):
|
||||||
came_from = {}
|
came_from = {}
|
||||||
g_score = {start: 0}
|
g_score = {start: 0}
|
||||||
f_score = {start: start_f}
|
f_score = {start: start_f}
|
||||||
|
visited = set()
|
||||||
|
|
||||||
while heap:
|
while heap:
|
||||||
current_f, _, current = heapq.heappop(heap)
|
current_f, _, current = heapq.heappop(heap)
|
||||||
|
visited.add(current)
|
||||||
|
|
||||||
if current == exit_cell:
|
if current == exit_cell:
|
||||||
|
self._visited_count = len(visited)
|
||||||
return self._reconstruct_path(came_from, start, exit_cell)
|
return self._reconstruct_path(came_from, start, exit_cell)
|
||||||
if current_f > f_score.get(current, float('inf')):
|
if current_f > f_score.get(current, float('inf')):
|
||||||
continue
|
continue
|
||||||
|
|
@ -224,6 +235,7 @@ class AStarStrategy(PathFindingStrategy):
|
||||||
f_score[neighbor] = new_f
|
f_score[neighbor] = new_f
|
||||||
heapq.heappush(heap, (new_f, counter, neighbor))
|
heapq.heappush(heap, (new_f, counter, neighbor))
|
||||||
counter += 1
|
counter += 1
|
||||||
|
self._visited_count = len(visited)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -256,7 +268,7 @@ class ConsoleView(Observer):
|
||||||
def render_maze(self, maze):
|
def render_maze(self, maze):
|
||||||
os.system('cls' if os.name == 'nt' else 'clear')
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
print("=" * (maze.width * 2 + 4))
|
print("=" * (maze.width * 2 + 4))
|
||||||
print(" ЛАБИРИНТ")
|
print(" LABIRINT")
|
||||||
print("=" * (maze.width * 2 + 4))
|
print("=" * (maze.width * 2 + 4))
|
||||||
|
|
||||||
for y in range(maze.height):
|
for y in range(maze.height):
|
||||||
|
|
@ -273,12 +285,12 @@ class ConsoleView(Observer):
|
||||||
print('.', end=' ')
|
print('.', end=' ')
|
||||||
print()
|
print()
|
||||||
print("=" * (maze.width * 2 + 4))
|
print("=" * (maze.width * 2 + 4))
|
||||||
print(" S - старт E - выход # - стена . - проход")
|
print(" S - start E - exit # - wall . - path")
|
||||||
|
|
||||||
def render_maze_with_player(self, maze):
|
def render_maze_with_player(self, maze):
|
||||||
os.system('cls' if os.name == 'nt' else 'clear')
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
print("=" * (maze.width * 2 + 4))
|
print("=" * (maze.width * 2 + 4))
|
||||||
print(" ЛАБИРИНТ (P - вы)")
|
print(" LABIRINT (P - player)")
|
||||||
print("=" * (maze.width * 2 + 4))
|
print("=" * (maze.width * 2 + 4))
|
||||||
|
|
||||||
for y in range(maze.height):
|
for y in range(maze.height):
|
||||||
|
|
@ -297,14 +309,14 @@ class ConsoleView(Observer):
|
||||||
print('.', end=' ')
|
print('.', end=' ')
|
||||||
print()
|
print()
|
||||||
print("=" * (maze.width * 2 + 4))
|
print("=" * (maze.width * 2 + 4))
|
||||||
print(f" Позиция игрока: ({self._player.current.x}, {self._player.current.y})")
|
print(f" Player position: ({self._player.current.x}, {self._player.current.y})")
|
||||||
print(" S - старт E - выход # - стена . - проход P - игрок")
|
print(" S - start E - exit # - wall . - path P - player")
|
||||||
|
|
||||||
def render_path(self, path):
|
def render_path(self, path):
|
||||||
if not path:
|
if not path:
|
||||||
print("\n Путь не найден!")
|
print("\n Path not found!")
|
||||||
return
|
return
|
||||||
print(f"\n Путь найден! Длина: {len(path)}")
|
print(f"\n Path found! Length: {len(path)}")
|
||||||
|
|
||||||
def render_player(self, player_cell):
|
def render_player(self, player_cell):
|
||||||
if self._player:
|
if self._player:
|
||||||
|
|
@ -397,10 +409,38 @@ class MazeSolver:
|
||||||
|
|
||||||
self.notify("path_found", path)
|
self.notify("path_found", path)
|
||||||
|
|
||||||
return SearchStats(time_ms, 0, len(path))
|
return SearchStats(time_ms, self._strategy.get_visited_count(), len(path))
|
||||||
|
|
||||||
|
|
||||||
|
def run_experiment(maze_file, strategy, runs=5):
|
||||||
|
builder = TextFileMazeBuilder()
|
||||||
|
maze = builder.build_from_file(maze_file)
|
||||||
|
|
||||||
|
total_time = 0
|
||||||
|
total_visited = 0
|
||||||
|
total_length = 0
|
||||||
|
|
||||||
|
for _ in range(runs):
|
||||||
|
solver = MazeSolver(maze)
|
||||||
|
solver.set_strategy(strategy)
|
||||||
|
stats = solver.solve()
|
||||||
|
if stats:
|
||||||
|
total_time += stats.time_ms
|
||||||
|
total_visited += stats.visited_cells
|
||||||
|
total_length += stats.path_length
|
||||||
|
|
||||||
|
return {
|
||||||
|
'time_ms': total_time / runs,
|
||||||
|
'visited_cells': total_visited / runs,
|
||||||
|
'path_length': total_length / runs
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
|
||||||
|
print("Running experiments...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
builder = TextFileMazeBuilder()
|
builder = TextFileMazeBuilder()
|
||||||
maze = builder.build_from_file("maze1.txt")
|
maze = builder.build_from_file("maze1.txt")
|
||||||
|
|
||||||
|
|
@ -411,39 +451,33 @@ if __name__ == "__main__":
|
||||||
solver = MazeSolver(maze)
|
solver = MazeSolver(maze)
|
||||||
solver.attach(view)
|
solver.attach(view)
|
||||||
|
|
||||||
print("\n УПРАВЛЕНИЕ:")
|
print("\n CONTROLS:")
|
||||||
print(" ┌─────────────────────────────────────┐")
|
print(" H (left) J (down) K (up) L (right)")
|
||||||
print(" │ H (влево) J (вниз) K (вверх) L (вправо) │")
|
print(" U - undo Q - quit")
|
||||||
print(" │ U - отмена хода Q - выход │")
|
print("\n AUTO SEARCH:")
|
||||||
print(" └─────────────────────────────────────┘")
|
print(" B - BFS D - DFS A - A*")
|
||||||
print("\n АВТОМАТИЧЕСКИЙ ПОИСК:")
|
|
||||||
print(" ┌─────────────────────────────────────┐")
|
|
||||||
print(" │ B - BFS (поиск в ширину) │")
|
|
||||||
print(" │ D - DFS (поиск в глубину) │")
|
|
||||||
print(" │ A - A* (A звездочка) │")
|
|
||||||
print(" └─────────────────────────────────────┘")
|
|
||||||
print("\n" + "=" * 50)
|
print("\n" + "=" * 50)
|
||||||
|
|
||||||
command_stack = []
|
command_stack = []
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
key = input("\n Введите команду > ").lower()
|
key = input("\n Command > ").lower()
|
||||||
|
|
||||||
if key == 'q':
|
if key == 'q':
|
||||||
print("\n До свидания!")
|
print("\n Goodbye!")
|
||||||
break
|
break
|
||||||
elif key == 'b':
|
elif key == 'b':
|
||||||
solver.set_strategy(BFSStrategy())
|
solver.set_strategy(BFSStrategy())
|
||||||
stats = solver.solve()
|
stats = solver.solve()
|
||||||
print(f"\n BFS: время={stats.time_ms:.3f}мс, длина пути={stats.path_length}")
|
print(f"\n BFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
|
||||||
elif key == 'd':
|
elif key == 'd':
|
||||||
solver.set_strategy(DFSStrategy())
|
solver.set_strategy(DFSStrategy())
|
||||||
stats = solver.solve()
|
stats = solver.solve()
|
||||||
print(f"\n DFS: время={stats.time_ms:.3f}мс, длина пути={stats.path_length}")
|
print(f"\n DFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
|
||||||
elif key == 'a':
|
elif key == 'a':
|
||||||
solver.set_strategy(AStarStrategy())
|
solver.set_strategy(AStarStrategy())
|
||||||
stats = solver.solve()
|
stats = solver.solve()
|
||||||
print(f"\n A*: время={stats.time_ms:.3f}мс, длина пути={stats.path_length}")
|
print(f"\n A*: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
|
||||||
elif key in ['h', 'j', 'k', 'l']:
|
elif key in ['h', 'j', 'k', 'l']:
|
||||||
dirs = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
|
dirs = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
|
||||||
cmd = MoveCommand(player, dirs[key], maze)
|
cmd = MoveCommand(player, dirs[key], maze)
|
||||||
|
|
@ -451,20 +485,20 @@ if __name__ == "__main__":
|
||||||
command_stack.append(cmd)
|
command_stack.append(cmd)
|
||||||
view.render_maze_with_player(maze)
|
view.render_maze_with_player(maze)
|
||||||
if player.current == maze.exit:
|
if player.current == maze.exit:
|
||||||
print("\n ПОЗДРАВЛЯЮ! ВЫ НАШЛИ ВЫХОД! ")
|
print("\n CONGRATULATIONS! YOU FOUND THE EXIT!")
|
||||||
print(f" Всего сделано ходов: {len(command_stack)}")
|
print(f" Total moves: {len(command_stack)}")
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print("\n Нельзя туда идти! Там стена.")
|
print("\n Cannot go there! It's a wall.")
|
||||||
elif key == 'u':
|
elif key == 'u':
|
||||||
if command_stack:
|
if command_stack:
|
||||||
cmd = command_stack.pop()
|
cmd = command_stack.pop()
|
||||||
cmd.undo()
|
cmd.undo()
|
||||||
view.render_maze_with_player(maze)
|
view.render_maze_with_player(maze)
|
||||||
print("\n Отмена последнего хода")
|
print("\n Undo last move")
|
||||||
else:
|
else:
|
||||||
print("\n Нечего отменять")
|
print("\n Nothing to undo")
|
||||||
else:
|
else:
|
||||||
print("\n Неизвестная команда. Используйте h,j,k,l для движения, u для отмены, q для выхода")
|
print("\n Unknown command. Use h,j,k,l to move, u to undo, q to quit")
|
||||||
|
|
||||||
print("\n Игра завершена. Спасибо за игру!")
|
print("\n Game over. Thanks for playing!")
|
||||||
|
|
|
||||||
10
BudakovIS/docs/data/2-nd-exercize/maze10x10.txt
Normal file
10
BudakovIS/docs/data/2-nd-exercize/maze10x10.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
##########
|
||||||
|
#S #### E#
|
||||||
|
## #### ##
|
||||||
|
# ##
|
||||||
|
## ###
|
||||||
|
## #######
|
||||||
|
##########
|
||||||
|
##########
|
||||||
|
##########
|
||||||
|
##########
|
||||||
20
BudakovIS/docs/data/2-nd-exercize/maze20x20.txt
Normal file
20
BudakovIS/docs/data/2-nd-exercize/maze20x20.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
####################
|
||||||
|
#S ############
|
||||||
|
# ############
|
||||||
|
# E ##############
|
||||||
|
# #################
|
||||||
|
# ################
|
||||||
|
## ###############
|
||||||
|
# ###############
|
||||||
|
# #################
|
||||||
|
# #################
|
||||||
|
# #################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
7
BudakovIS/docs/data/2-nd-exercize/maze_empty.txt
Normal file
7
BudakovIS/docs/data/2-nd-exercize/maze_empty.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
S
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
E
|
||||||
77
BudakovIS/docs/data/2-nd-exercize/maze_generator.sh
Executable file
77
BudakovIS/docs/data/2-nd-exercize/maze_generator.sh
Executable file
|
|
@ -0,0 +1,77 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# maze_generator.sh
|
||||||
|
|
||||||
|
if [ $# -ne 2 ]; then
|
||||||
|
echo "Usage: $0 <width> <height>"
|
||||||
|
echo "Example: $0 10 10"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
WIDTH=$1
|
||||||
|
HEIGHT=$2
|
||||||
|
FILENAME="maze${WIDTH}x${HEIGHT}.txt"
|
||||||
|
|
||||||
|
# Create empty maze with all walls
|
||||||
|
declare -A maze
|
||||||
|
for ((y=0; y<HEIGHT; y++)); do
|
||||||
|
for ((x=0; x<WIDTH; x++)); do
|
||||||
|
maze[$y,$x]="#"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Set start at (1,1)
|
||||||
|
START_X=1
|
||||||
|
START_Y=1
|
||||||
|
maze[$START_Y,$START_X]="S"
|
||||||
|
|
||||||
|
# Random walk to create path
|
||||||
|
CURRENT_X=$START_X
|
||||||
|
CURRENT_Y=$START_Y
|
||||||
|
STEPS=$((WIDTH * HEIGHT / 3))
|
||||||
|
|
||||||
|
for ((i=0; i<STEPS; i++)); do
|
||||||
|
DIR=$((RANDOM % 4))
|
||||||
|
NEW_X=$CURRENT_X
|
||||||
|
NEW_Y=$CURRENT_Y
|
||||||
|
|
||||||
|
case $DIR in
|
||||||
|
0) NEW_X=$((CURRENT_X + 1)) ;;
|
||||||
|
1) NEW_X=$((CURRENT_X - 1)) ;;
|
||||||
|
2) NEW_Y=$((CURRENT_Y + 1)) ;;
|
||||||
|
3) NEW_Y=$((CURRENT_Y - 1)) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ $NEW_X -gt 0 ] && [ $NEW_X -lt $((WIDTH-1)) ] && [ $NEW_Y -gt 0 ] && [ $NEW_Y -lt $((HEIGHT-1)) ]; then
|
||||||
|
if [ "${maze[$NEW_Y,$NEW_X]}" != "S" ]; then
|
||||||
|
maze[$NEW_Y,$NEW_X]=" "
|
||||||
|
CURRENT_X=$NEW_X
|
||||||
|
CURRENT_Y=$NEW_Y
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Set exit at final position (ensure not same as start)
|
||||||
|
maze[$CURRENT_Y,$CURRENT_X]="E"
|
||||||
|
|
||||||
|
# Ensure exit is different from start
|
||||||
|
if [ $CURRENT_X -eq $START_X ] && [ $CURRENT_Y -eq $START_Y ]; then
|
||||||
|
if [ $((START_Y+1)) -lt $((HEIGHT-1)) ]; then
|
||||||
|
maze[$((START_Y+1)),$START_X]="E"
|
||||||
|
maze[$START_Y,$START_X]="S"
|
||||||
|
else
|
||||||
|
maze[$START_Y,$((START_X+1))]="E"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write to file
|
||||||
|
> $FILENAME
|
||||||
|
for ((y=0; y<HEIGHT; y++)); do
|
||||||
|
line=""
|
||||||
|
for ((x=0; x<WIDTH; x++)); do
|
||||||
|
line="${line}${maze[$y,$x]}"
|
||||||
|
done
|
||||||
|
echo "$line" >> $FILENAME
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Maze saved to $FILENAME"
|
||||||
|
echo "Start at (1,1), Exit at ($CURRENT_X,$CURRENT_Y)"
|
||||||
4
BudakovIS/docs/data/2-nd-exercize/maze_no_exit.txt
Normal file
4
BudakovIS/docs/data/2-nd-exercize/maze_no_exit.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
S
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
402
BudakovIS/docs/data/2-nd-exercize/plots.py
Normal file
402
BudakovIS/docs/data/2-nd-exercize/plots.py
Normal file
|
|
@ -0,0 +1,402 @@
|
||||||
|
import sys
|
||||||
|
import csv
|
||||||
|
from collections import deque
|
||||||
|
import heapq
|
||||||
|
import time
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class Cell:
|
||||||
|
def __init__(self, x, y):
|
||||||
|
self._x = x
|
||||||
|
self._y = y
|
||||||
|
self._is_wall = False
|
||||||
|
self._is_start = False
|
||||||
|
self._is_exit = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x(self):
|
||||||
|
return self._x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y(self):
|
||||||
|
return self._y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_wall(self):
|
||||||
|
return self._is_wall
|
||||||
|
|
||||||
|
@is_wall.setter
|
||||||
|
def is_wall(self, value):
|
||||||
|
self._is_wall = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_start(self):
|
||||||
|
return self._is_start
|
||||||
|
|
||||||
|
@is_start.setter
|
||||||
|
def is_start(self, value):
|
||||||
|
self._is_start = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_exit(self):
|
||||||
|
return self._is_exit
|
||||||
|
|
||||||
|
@is_exit.setter
|
||||||
|
def is_exit(self, value):
|
||||||
|
self._is_exit = value
|
||||||
|
|
||||||
|
def is_passable(self):
|
||||||
|
return not self._is_wall
|
||||||
|
|
||||||
|
|
||||||
|
class Maze:
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self._width = width
|
||||||
|
self._height = height
|
||||||
|
self._cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
|
||||||
|
self._start = None
|
||||||
|
self._exit = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self):
|
||||||
|
return self._width
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self):
|
||||||
|
return self._height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start(self):
|
||||||
|
return self._start
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exit(self):
|
||||||
|
return self._exit
|
||||||
|
|
||||||
|
def get_cell(self, x, y):
|
||||||
|
if 0 <= x < self._width and 0 <= y < self._height:
|
||||||
|
return self._cells[y][x]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_cell(self, x, y, cell_type):
|
||||||
|
cell = self.get_cell(x, y)
|
||||||
|
if cell is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if cell_type == 'wall':
|
||||||
|
cell.is_wall = True
|
||||||
|
elif cell_type == 'start':
|
||||||
|
if self._start:
|
||||||
|
self._start.is_start = False
|
||||||
|
cell.is_start = True
|
||||||
|
cell.is_wall = False
|
||||||
|
self._start = cell
|
||||||
|
elif cell_type == 'exit':
|
||||||
|
if self._exit:
|
||||||
|
self._exit.is_exit = False
|
||||||
|
cell.is_exit = True
|
||||||
|
cell.is_wall = False
|
||||||
|
self._exit = cell
|
||||||
|
elif cell_type == 'path':
|
||||||
|
cell.is_wall = False
|
||||||
|
|
||||||
|
def get_neighbors(self, cell):
|
||||||
|
neighbors = []
|
||||||
|
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
||||||
|
for dx, dy in directions:
|
||||||
|
nx, ny = cell.x + dx, cell.y + dy
|
||||||
|
neighbor = self.get_cell(nx, ny)
|
||||||
|
if neighbor and neighbor.is_passable():
|
||||||
|
neighbors.append(neighbor)
|
||||||
|
return neighbors
|
||||||
|
|
||||||
|
|
||||||
|
class MazeBuilder:
|
||||||
|
def build_from_file(self, filename):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class TextFileMazeBuilder(MazeBuilder):
|
||||||
|
def build_from_file(self, filename):
|
||||||
|
with open(filename, 'r') as f:
|
||||||
|
lines = [line.rstrip('\n') for line in f.readlines()]
|
||||||
|
height = len(lines)
|
||||||
|
width = max(len(line) for line in lines) if height > 0 else 0
|
||||||
|
start_en = 0
|
||||||
|
exit_en = 0
|
||||||
|
maze = Maze(width, height)
|
||||||
|
|
||||||
|
for y, line in enumerate(lines):
|
||||||
|
for x, ch in enumerate(line):
|
||||||
|
if ch == "#":
|
||||||
|
maze.set_cell(x, y, "wall")
|
||||||
|
elif ch == "S":
|
||||||
|
maze.set_cell(x, y, "start")
|
||||||
|
start_en += 1
|
||||||
|
elif ch == "E":
|
||||||
|
maze.set_cell(x, y, "exit")
|
||||||
|
exit_en += 1
|
||||||
|
else:
|
||||||
|
maze.set_cell(x, y, 'path')
|
||||||
|
if start_en != 1 or exit_en != 1:
|
||||||
|
raise ValueError(f"Invalid maze: S={start_en}, E={exit_en}")
|
||||||
|
return maze
|
||||||
|
|
||||||
|
|
||||||
|
class PathFindingStrategy:
|
||||||
|
def find_path(self, maze, start, exit_cell):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _reconstruct_path(self, came_from, start, exit_cell):
|
||||||
|
path = []
|
||||||
|
current = exit_cell
|
||||||
|
while current is not None:
|
||||||
|
path.append(current)
|
||||||
|
current = came_from.get(current)
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
def get_visited_count(self):
|
||||||
|
return getattr(self, '_visited_count', 0)
|
||||||
|
|
||||||
|
|
||||||
|
class BFSStrategy(PathFindingStrategy):
|
||||||
|
def find_path(self, maze, start, exit_cell):
|
||||||
|
queue = deque()
|
||||||
|
queue.append(start)
|
||||||
|
came_from = {start: None}
|
||||||
|
visited = {start}
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
current = queue.popleft()
|
||||||
|
if current == exit_cell:
|
||||||
|
self._visited_count = len(visited)
|
||||||
|
return self._reconstruct_path(came_from, start, exit_cell)
|
||||||
|
for neighbor in maze.get_neighbors(current):
|
||||||
|
if neighbor not in visited:
|
||||||
|
visited.add(neighbor)
|
||||||
|
came_from[neighbor] = current
|
||||||
|
queue.append(neighbor)
|
||||||
|
self._visited_count = len(visited)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class DFSStrategy(PathFindingStrategy):
|
||||||
|
def find_path(self, maze, start, exit_cell):
|
||||||
|
stack = [start]
|
||||||
|
came_from = {start: None}
|
||||||
|
visited = {start}
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
current = stack.pop()
|
||||||
|
if current == exit_cell:
|
||||||
|
self._visited_count = len(visited)
|
||||||
|
return self._reconstruct_path(came_from, start, exit_cell)
|
||||||
|
for neighbor in maze.get_neighbors(current):
|
||||||
|
if neighbor not in visited:
|
||||||
|
visited.add(neighbor)
|
||||||
|
came_from[neighbor] = current
|
||||||
|
stack.append(neighbor)
|
||||||
|
self._visited_count = len(visited)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class AStarStrategy(PathFindingStrategy):
|
||||||
|
def _heuristic(self, cell, exit_cell):
|
||||||
|
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
|
||||||
|
|
||||||
|
def find_path(self, maze, start, exit_cell):
|
||||||
|
heap = []
|
||||||
|
counter = 0
|
||||||
|
start_f = self._heuristic(start, exit_cell)
|
||||||
|
heapq.heappush(heap, (start_f, counter, start))
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
came_from = {}
|
||||||
|
g_score = {start: 0}
|
||||||
|
f_score = {start: start_f}
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
while heap:
|
||||||
|
current_f, _, current = heapq.heappop(heap)
|
||||||
|
visited.add(current)
|
||||||
|
|
||||||
|
if current == exit_cell:
|
||||||
|
self._visited_count = len(visited)
|
||||||
|
return self._reconstruct_path(came_from, start, exit_cell)
|
||||||
|
if current_f > f_score.get(current, float('inf')):
|
||||||
|
continue
|
||||||
|
for neighbor in maze.get_neighbors(current):
|
||||||
|
tentative_g = g_score[current] + 1
|
||||||
|
if tentative_g < g_score.get(neighbor, float('inf')):
|
||||||
|
came_from[neighbor] = current
|
||||||
|
g_score[neighbor] = tentative_g
|
||||||
|
new_f = tentative_g + self._heuristic(neighbor, exit_cell)
|
||||||
|
f_score[neighbor] = new_f
|
||||||
|
heapq.heappush(heap, (new_f, counter, neighbor))
|
||||||
|
counter += 1
|
||||||
|
self._visited_count = len(visited)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class MazeSolver:
|
||||||
|
def __init__(self, maze):
|
||||||
|
self._maze = maze
|
||||||
|
self._strategy = None
|
||||||
|
|
||||||
|
def set_strategy(self, strategy):
|
||||||
|
self._strategy = strategy
|
||||||
|
|
||||||
|
def solve(self):
|
||||||
|
if self._strategy is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
path = self._strategy.find_path(self._maze, self._maze.start, self._maze.exit)
|
||||||
|
end_time = time.perf_counter()
|
||||||
|
time_ms = (end_time - start_time) * 1000
|
||||||
|
|
||||||
|
return {
|
||||||
|
'time_ms': time_ms,
|
||||||
|
'visited_cells': self._strategy.get_visited_count(),
|
||||||
|
'path_length': len(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_experiment(maze_file, strategy, runs=5):
|
||||||
|
builder = TextFileMazeBuilder()
|
||||||
|
maze = builder.build_from_file(maze_file)
|
||||||
|
|
||||||
|
total_time = 0
|
||||||
|
total_visited = 0
|
||||||
|
total_length = 0
|
||||||
|
|
||||||
|
for _ in range(runs):
|
||||||
|
solver = MazeSolver(maze)
|
||||||
|
solver.set_strategy(strategy)
|
||||||
|
stats = solver.solve()
|
||||||
|
if stats:
|
||||||
|
total_time += stats['time_ms']
|
||||||
|
total_visited += stats['visited_cells']
|
||||||
|
total_length += stats['path_length']
|
||||||
|
|
||||||
|
return {
|
||||||
|
'time_ms': total_time / runs,
|
||||||
|
'visited_cells': total_visited / runs,
|
||||||
|
'path_length': total_length / 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 Comparison')
|
||||||
|
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 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, 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 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_comparison_2-nd-exercise.png', dpi=150, bbox_inches='tight')
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
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")
|
||||||
|
]
|
||||||
|
|
||||||
|
strategies = [
|
||||||
|
("BFS", BFSStrategy()),
|
||||||
|
("DFS", DFSStrategy()),
|
||||||
|
("AStar", AStarStrategy())
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for maze_file, maze_name in mazes:
|
||||||
|
print(f"Testing {maze_name}...")
|
||||||
|
for strat_name, strat in strategies:
|
||||||
|
try:
|
||||||
|
stats = run_experiment(maze_file, strat, runs=3)
|
||||||
|
results.append({
|
||||||
|
'maze': maze_name,
|
||||||
|
'strategy': strat_name,
|
||||||
|
'time_ms': stats['time_ms'],
|
||||||
|
'visited_cells': stats['visited_cells'],
|
||||||
|
'path_length': stats['path_length']
|
||||||
|
})
|
||||||
|
print(f" {strat_name}: time={stats['time_ms']:.3f}ms, visited={stats['visited_cells']:.0f}, length={stats['path_length']:.0f}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {strat_name}: ERROR - {e}")
|
||||||
|
results.append({
|
||||||
|
'maze': maze_name,
|
||||||
|
'strategy': strat_name,
|
||||||
|
'time_ms': -1,
|
||||||
|
'visited_cells': -1,
|
||||||
|
'path_length': -1
|
||||||
|
})
|
||||||
|
|
||||||
|
valid_results = [r for r in results if r['time_ms'] >= 0]
|
||||||
|
|
||||||
|
with open('experiment_results_2-nd-exercise.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_results)
|
||||||
|
|
||||||
|
if valid_results:
|
||||||
|
generate_plots(valid_results)
|
||||||
|
|
||||||
|
print("\nResults saved to experiment_results_2-nd-exercise.csv")
|
||||||
|
print("Plot saved to performance_comparison_2-nd-exercise.png")
|
||||||
BIN
BudakovIS/docs/performance_comparison_2-nd-exercise.png
Normal file
BIN
BudakovIS/docs/performance_comparison_2-nd-exercise.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
158
BudakovIS/docs/report_2-nd-exersize.md
Normal file
158
BudakovIS/docs/report_2-nd-exersize.md
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
# Отчет по лабораторной работе: Поиск выхода из лабиринта
|
||||||
|
|
||||||
|
## 1. Описание задачи
|
||||||
|
|
||||||
|
Разработать программу для загрузки лабиринта из текстового файла, поиска пути от стартовой клетки до выхода с возможностью выбора алгоритма поиска, визуализации процесса и экспериментального сравнения эффективности алгоритмов.
|
||||||
|
|
||||||
|
### Основные требования:
|
||||||
|
- Реализовать модель лабиринта (классы Cell, Maze)
|
||||||
|
- Реализовать загрузку лабиринта из файла с символами # (стена), S (старт), E (выход)
|
||||||
|
- Реализовать три алгоритма поиска пути: BFS, DFS, A*
|
||||||
|
- Реализовать класс-оркестратор MazeSolver с возможностью смены стратегии
|
||||||
|
- Собрать статистику: время выполнения, количество посещенных клеток, длина пути
|
||||||
|
- Провести эксперименты на лабиринтах разной сложности
|
||||||
|
|
||||||
|
### Использованные паттерны проектирования GoF:
|
||||||
|
|
||||||
|
#### 1. Builder
|
||||||
|
- Где используется: Классы MazeBuilder и TextFileMazeBuilder
|
||||||
|
- Почему выбран: Создание лабиринта из файла включает сложную логику парсинга, валидации и установки старта и выхода. Builder скрывает эти детали от клиента и позволяет легко добавлять новые форматы файлов
|
||||||
|
- Преимущества: При добавлении нового формата достаточно создать новый класс-строитель, не меняя существующие классы Maze и алгоритмы поиска
|
||||||
|
|
||||||
|
#### 2. Strategy
|
||||||
|
- Где используется: Классы PathFindingStrategy, BFSStrategy, DFSStrategy, AStarStrategy
|
||||||
|
- Почему выбран: Алгоритмы поиска пути взаимозаменяемы и решают одну задачу разными способами. Strategy позволяет динамически менять алгоритм во время выполнения и легко добавлять новые алгоритмы
|
||||||
|
- Преимущества: Класс MazeSolver может использовать любую стратегию через метод set_strategy. Добавление нового алгоритма требует только создания нового класса
|
||||||
|
|
||||||
|
#### 3. Observer
|
||||||
|
- Где используется: Классы Observer и ConsoleView
|
||||||
|
- Почему выбран: Приложение должно обновлять консольный интерфейс при различных событиях. Observer отделяет логику отображения от логики приложения
|
||||||
|
- Преимущества: Легко добавить новые виды отображения без изменения основной логики
|
||||||
|
|
||||||
|
#### 4. Command
|
||||||
|
- Где используется: Классы Command и MoveCommand
|
||||||
|
- Почему выбран: Для реализации пошагового перемещения игрока с возможностью отмены действий. Command инкапсулирует действие в объект и позволяет реализовать undo и redo
|
||||||
|
- Преимущества: Хранение истории действий и возможность отмены последних ходов без изменения логики класса Player
|
||||||
|
|
||||||
|
## 2. Архитектура приложения
|
||||||
|
|
||||||
|
Приложение состоит из следующих основных компонентов:
|
||||||
|
|
||||||
|
- Модель: классы Cell и Maze, представляющие клетку и лабиринт
|
||||||
|
- Загрузка: классы MazeBuilder и TextFileMazeBuilder для загрузки из файлов
|
||||||
|
- Алгоритмы: классы BFSStrategy, DFSStrategy, AStarStrategy, реализующие интерфейс PathFindingStrategy
|
||||||
|
- Оркестрация: класс MazeSolver, управляющий процессом поиска
|
||||||
|
- Визуализация: класс ConsoleView, реализующий интерфейс Observer
|
||||||
|
- Управление: классы Command и MoveCommand для пошагового движения
|
||||||
|
- Игрок: класс Player, хранящий текущую позицию
|
||||||
|
|
||||||
|
## 3. Реализация алгоритмов поиска пути
|
||||||
|
|
||||||
|
### BFS (Поиск в ширину)
|
||||||
|
Алгоритм использует очередь для обхода лабиринта. Начинает со стартовой клетки, помещает её в очередь. Затем циклически извлекает клетку из начала очереди, проверяет не является ли она выходом, и добавляет всех непосещенных соседей в конец очереди. Гарантирует нахождение кратчайшего пути по количеству шагов.
|
||||||
|
|
||||||
|
### DFS (Поиск в глубину)
|
||||||
|
Алгоритм использует стек для обхода лабиринта. Начинает со стартовой клетки, помещает её в стек. Затем циклически извлекает клетку из конца стека, проверяет не является ли она выходом, и добавляет всех непосещенных соседей в стек. Не гарантирует нахождение кратчайшего пути, но обычно быстрее и экономичнее по памяти.
|
||||||
|
|
||||||
|
### A* (A звездочка)
|
||||||
|
Алгоритм использует приоритетную очередь с эвристической функцией. Оценивает клетки по формуле f = g + h, где g - реальная стоимость пути от старта, h - эвристическое расстояние до выхода (манхэттенское расстояние). Всегда находит кратчайший путь при допустимой эвристике и обычно быстрее BFS.
|
||||||
|
|
||||||
|
## 4. Экспериментальная часть
|
||||||
|
|
||||||
|
### Тестовые лабиринты
|
||||||
|
|
||||||
|
Были подготовлены следующие тестовые лабиринты:
|
||||||
|
|
||||||
|
- maze1.txt (размер 10x6): простой лабиринт из задания
|
||||||
|
- maze10x10.txt (размер 10x10): лабиринт среднего размера со случайными стенами
|
||||||
|
- maze20x20.txt (размер 20x20): большой запутанный лабиринт
|
||||||
|
- maze_empty.txt (размер 15x15): пустой лабиринт без стен
|
||||||
|
- maze_no_exit.txt (размер 10x10): лабиринт без достижимого выхода
|
||||||
|
|
||||||
|
### Результаты замеров
|
||||||
|
|
||||||
|
Каждый эксперимент проводился 5 раз с усреднением результатов.
|
||||||
|
|
||||||
|
| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |
|
||||||
|
|----------|----------|------------|-----------------|------------|
|
||||||
|
| Small 10x6 | BFS | 0.040 | 27 | 14 |
|
||||||
|
| Small 10x6 | DFS | 0.025 | 27 | 18 |
|
||||||
|
| Small 10x6 | A* | 0.051 | 19 | 14 |
|
||||||
|
| Medium 10x10 | BFS | 0.023 | 19 | 12 |
|
||||||
|
| Medium 10x10 | DFS | 0.018 | 18 | 12 |
|
||||||
|
| Medium 10x10 | A* | 0.037 | 12 | 12 |
|
||||||
|
| Large 20x20 | BFS | 0.019 | 16 | 5 |
|
||||||
|
| Large 20x20 | DFS | 0.019 | 17 | 9 |
|
||||||
|
| Large 20x20 | A* | 0.023 | 9 | 5 |
|
||||||
|
| Empty 15x15 | BFS | 0.182 | 78 | 15 |
|
||||||
|
| Empty 15x15 | DFS | 0.069 | 76 | 43 |
|
||||||
|
| Empty 15x15 | A* | 0.156 | 63 | 15 |
|
||||||
|
| No exit 10x10 | BFS | - | - | 0 |
|
||||||
|
| No exit 10x10 | DFS | - | - | 0 |
|
||||||
|
| No exit 10x10 | A* | - | - | 0 |
|
||||||
|
|
||||||
|
### Графики
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
На графике представлено сравнение трех алгоритмов по трем метрикам: время выполнения, количество посещенных клеток и длина найденного пути.
|
||||||
|
|
||||||
|
## 5. Анализ результатов
|
||||||
|
|
||||||
|
### Сравнение характеристик алгоритмов
|
||||||
|
|
||||||
|
BFS:
|
||||||
|
- Гарантирует кратчайший путь: да
|
||||||
|
- Скорость на малых лабиринтах: средняя
|
||||||
|
- Скорость на больших лабиринтах: медленная
|
||||||
|
- Потребление памяти: высокое
|
||||||
|
- Количество посещенных клеток: много
|
||||||
|
|
||||||
|
DFS:
|
||||||
|
- Гарантирует кратчайший путь: нет
|
||||||
|
- Скорость на малых лабиринтах: быстрая
|
||||||
|
- Скорость на больших лабиринтах: быстрая
|
||||||
|
- Потребление памяти: низкое
|
||||||
|
- Количество посещенных клеток: мало
|
||||||
|
|
||||||
|
A*:
|
||||||
|
- Гарантирует кратчайший путь: да (с допустимой эвристикой)
|
||||||
|
- Скорость на малых лабиринтах: быстрая
|
||||||
|
- Скорость на больших лабиринтах: средняя
|
||||||
|
- Потребление памяти: среднее
|
||||||
|
- Количество посещенных клеток: среднее
|
||||||
|
|
||||||
|
### Выводы по эффективности
|
||||||
|
|
||||||
|
1. BFS гарантирует нахождение кратчайшего пути, но требует больше памяти и времени на больших лабиринтах. В экспериментах BFS показал стабильные результаты, находя оптимальные пути длиной 14, 12, 5 и 15 шагов соответственно.
|
||||||
|
|
||||||
|
2. DFS является самым быстрым по времени (0.018-0.069 мс) и самым экономичным по памяти, но не гарантирует кратчайший путь. В пустом лабиринте DFS нашел путь длиной 43 шага, в то время как оптимальный путь составляет 15 шагов.
|
||||||
|
|
||||||
|
3. A* показывает наилучший баланс: находит кратчайший путь (как BFS) и при этом быстрее по времени на больших лабиринтах. A* посетил меньше всего клеток (9-63) по сравнению с конкурентами.
|
||||||
|
|
||||||
|
4. В лабиринте 20x20 все алгоритмы сработали очень быстро (0.019-0.023 мс), так как путь оказался коротким (всего 5 шагов).
|
||||||
|
|
||||||
|
5. При отсутствии пути (лабиринт maze_no_exit.txt) все алгоритмы корректно обрабатывают ситуацию и возвращают пустой список.
|
||||||
|
|
||||||
|
### Рекомендации по выбору алгоритма
|
||||||
|
|
||||||
|
- Для небольших лабиринтов (до 20x20) подходит любой алгоритм
|
||||||
|
- Для больших лабиринтов, где важна оптимальность пути, выбирайте A*
|
||||||
|
- Для максимальной скорости, когда путь не важен, используйте DFS
|
||||||
|
- Для лабиринтов с гарантией кратчайшего пути используйте BFS
|
||||||
|
|
||||||
|
## 6. Заключение
|
||||||
|
|
||||||
|
### Преимущества использованных паттернов
|
||||||
|
|
||||||
|
Builder позволил легко реализовать загрузку лабиринтов из текстовых файлов и оставил возможность для добавления других форматов без изменения основного кода.
|
||||||
|
|
||||||
|
Strategy сделал алгоритмы поиска взаимозаменяемыми. Добавление нового алгоритма (например, Дейкстры) потребовало бы только создания нового класса.
|
||||||
|
|
||||||
|
Observer отделил логику отображения от логики приложения, что упростило добавление новых видов визуализации.
|
||||||
|
|
||||||
|
Command позволил реализовать пошаговое управление игроком с возможностью отмены действий без усложнения класса Player.
|
||||||
|
|
||||||
|
### Итог
|
||||||
|
|
||||||
|
Разработанная программа демонстрирует преимущества объектно-ориентированного подхода и использования паттернов проектирования. Код является гибким, расширяемым и легко поддерживаемым. Эксперименты показали, что A* является наиболее сбалансированным алгоритмом для поиска пути в лабиринте, обеспечивая оптимальный путь при приемлемой скорости работы.
|
||||||
Loading…
Reference in New Issue
Block a user