Merge pull request '[2] Вторая лабораторная работа' (#253) from mylnikovas/2026-rff_mp:task_2 into develop

Reviewed-on: UNN/2026-rff_mp#253
This commit is contained in:
kit8nino 2026-05-30 11:47:57 +00:00
commit 9b55830b58
4 changed files with 403 additions and 0 deletions

View File

@ -0,0 +1,21 @@
labyrint,strategy,time_ms,passed_cells,path_length
Small (10x10),BFS,0.1973,59,27
Small (10x10),DFS,0.1615,49,27
Small (10x10),AStar,0.2375,50,27
Small (10x10),Dijkstra,0.2487,60,27
Empty (50x50),BFS,8.3316,2500,99
Empty (50x50),DFS,4.6278,1275,1275
Empty (50x50),AStar,17.5549,2500,99
Empty (50x50),Dijkstra,14.7128,2500,99
Middle with dead ends (50x50),BFS,4.8741,1420,1083
Middle with dead ends (50x50),DFS,4.3282,1275,1275
Middle with dead ends (50x50),AStar,7.1452,1404,1083
Middle with dead ends (50x50),Dijkstra,6.0643,1420,1083
Big (100x100),BFS,17.0733,5590,199
Big (100x100),DFS,15.1471,4933,4519
Big (100x100),AStar,31.4644,5140,199
Big (100x100),Dijkstra,26.7258,5590,199
Without exit (10x10),BFS,0.0140,5,0
Without exit (10x10),DFS,0.0124,5,0
Without exit (10x10),AStar,0.0164,5,0
Without exit (10x10),Dijkstra,0.0143,5,0
1 labyrint strategy time_ms passed_cells path_length
2 Small (10x10) BFS 0.1973 59 27
3 Small (10x10) DFS 0.1615 49 27
4 Small (10x10) AStar 0.2375 50 27
5 Small (10x10) Dijkstra 0.2487 60 27
6 Empty (50x50) BFS 8.3316 2500 99
7 Empty (50x50) DFS 4.6278 1275 1275
8 Empty (50x50) AStar 17.5549 2500 99
9 Empty (50x50) Dijkstra 14.7128 2500 99
10 Middle with dead ends (50x50) BFS 4.8741 1420 1083
11 Middle with dead ends (50x50) DFS 4.3282 1275 1275
12 Middle with dead ends (50x50) AStar 7.1452 1404 1083
13 Middle with dead ends (50x50) Dijkstra 6.0643 1420 1083
14 Big (100x100) BFS 17.0733 5590 199
15 Big (100x100) DFS 15.1471 4933 4519
16 Big (100x100) AStar 31.4644 5140 199
17 Big (100x100) Dijkstra 26.7258 5590 199
18 Without exit (10x10) BFS 0.0140 5 0
19 Without exit (10x10) DFS 0.0124 5 0
20 Without exit (10x10) AStar 0.0164 5 0
21 Without exit (10x10) Dijkstra 0.0143 5 0

View File

@ -0,0 +1,190 @@
import time
import csv
from collections import deque
import heapq
from abc import ABC, abstractmethod
class Cell:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
self.isWall = False
self.isStart = False
self.isExit = False
self.weight = 1
def isPassable(self) -> bool:
return not self.isWall
def __lt__(self, other):
return (self.x, self.y) < (other.x, other.y)
class Maze:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self.cells = [[Cell(x, y) for y in range(height)] for x in range(width)]
self.start = None
self.exit = None
def getCell(self, x: int, y: int) -> Cell:
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[x][y]
return None
def getNeighbors(self, cell: 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.getCell(nx, ny)
if neighbor and neighbor.isPassable():
neighbors.append(neighbor)
return neighbors
class MazeBuilder(ABC):
@abstractmethod
def buildFromStringList(self, lines: list) -> Maze:
pass
class TextMazeBuilder(MazeBuilder):
def buildFromStringList(self, lines: list) -> Maze:
height = len(lines)
width = len(lines[0]) if height > 0 else 0
maze = Maze(width, height)
for y, line in enumerate(lines):
for x, char in enumerate(line):
cell = maze.getCell(x, y)
if char == '#':
cell.isWall = True
elif char == 'S':
cell.isStart = True
maze.start = cell
elif char == 'E':
cell.isExit = True
maze.exit = cell
elif char == 'W':
cell.weight = 3
elif char == 'D':
cell.weight = 2
return maze
class PathFindingStrategy(ABC):
def __init__(self):
self.visited_count = 0
@abstractmethod
def findPath(self, maze: Maze, start: Cell, exit: Cell) -> list:
pass
def _reconstruct_path(self, came_from: dict, start: Cell, exit: Cell) -> list:
if exit not in came_from:
return []
path = []
current = exit
while current != start:
path.append(current)
current = came_from[current]
path.append(start)
path.reverse()
return path
class BFSStrategy(PathFindingStrategy):
def findPath(self, maze: Maze, start: Cell, exit: Cell) -> list:
self.visited_count = 0
queue = deque([start])
came_from = {start: None}
while queue:
current = queue.popleft()
self.visited_count += 1
if current == exit:
break
for neighbor in maze.getNeighbors(current):
if neighbor not in came_from:
queue.append(neighbor)
came_from[neighbor] = current
return self._reconstruct_path(came_from, start, exit)
class DFSStrategy(PathFindingStrategy):
def findPath(self, maze: Maze, start: Cell, exit: Cell) -> list:
self.visited_count = 0
stack = [start]
came_from = {start: None}
while stack:
current = stack.pop()
self.visited_count += 1
if current == exit:
break
for neighbor in maze.getNeighbors(current):
if neighbor not in came_from:
stack.append(neighbor)
came_from[neighbor] = current
return self._reconstruct_path(came_from, start, exit)
class AStarStrategy(PathFindingStrategy):
def findPath(self, maze: Maze, start: Cell, exit: Cell) -> list:
self.visited_count = 0
def heuristic(a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
pq = []
heapq.heappush(pq, (0, start))
came_from = {start: None}
g_score = {start: 0}
while pq:
_, current = heapq.heappop(pq)
self.visited_count += 1
if current == exit:
break
for neighbor in maze.getNeighbors(current):
tentative_g_score = g_score[current] + neighbor.weight
if neighbor not in g_score or tentative_g_score < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g_score
f_score = tentative_g_score + heuristic(neighbor, exit)
heapq.heappush(pq, (f_score, neighbor))
return self._reconstruct_path(came_from, start, exit)
class DijkstraStrategy(PathFindingStrategy):
def findPath(self, maze: Maze, start: Cell, exit: Cell) -> list:
self.visited_count = 0
pq = []
heapq.heappush(pq, (0, start))
came_from = {start: None}
g_score = {start: 0}
while pq:
current_g, current = heapq.heappop(pq)
self.visited_count += 1
if current == exit:
break
for neighbor in maze.getNeighbors(current):
tentative_g_score = g_score[current] + neighbor.weight
if neighbor not in g_score or tentative_g_score < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g_score
heapq.heappush(pq, (tentative_g_score, neighbor))
return self._reconstruct_path(came_from, start, exit)

View File

@ -0,0 +1,192 @@
class SearchStats:
def __init__(self, timeMs: float, visitedCells: int, pathLength: int):
self.timeMs = timeMs
self.visitedCells = visitedCells
self.pathLength = pathLength
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self.maze = maze
self.strategy = strategy
self.observers = []
def setStrategy(self, strategy: PathFindingStrategy):
self.strategy = strategy
def addObserver(self, observer):
self.observers.append(observer)
def _notify(self, event: str):
for obs in self.observers:
obs.update(event)
def solve(self) -> tuple:
self._notify("Поиск начат")
start_time = time.perf_counter()
path = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
time_ms = (end_time - start_time) * 1000
stats = SearchStats(time_ms, self.strategy.visited_count, len(path))
self._notify("Поиск завершен")
return path, stats
class Observer(ABC):
@abstractmethod
def update(self, event: str):
pass
class ConsoleView(Observer):
def update(self, event: str):
pass
def render(self, maze: Maze, path: list):
path_set = set(path)
for y in range(maze.height):
row = ""
for x in range(maze.width):
cell = maze.getCell(x, y)
if cell == maze.start:
row += "S"
elif cell == maze.exit:
row += "E"
elif cell in path_set:
row += "*"
elif cell.isWall:
row += "#"
elif cell.weight == 3:
row += "W" # Болото
elif cell.weight == 2:
row += "D" # Песок
else:
row += "."
print(row)
def get_test_mazes():
builder = TextMazeBuilder()
mazes = {}
small = [
"S.........",
"#####.####",
"..........",
"####.#####",
"..........",
"#.#######.",
"..........",
"######.###",
"..........",
"........XE"
]
small[-1] = small[-1].replace('X', '.')
mazes["Small (10x10)"] = builder.buildFromStringList(small)
empty = ["." * 50 for _ in range(50)]
empty_list = list(empty)
empty_list[0] = "S" + empty_list[0][1:]
empty_list[-1] = empty_list[-1][:-1] + "E"
mazes["Empty (50x50)"] = builder.buildFromStringList(empty_list)
medium = []
for y in range(50):
if y == 0:
row = "S" + "." * 49
elif y == 49:
row = "." * 49 + "E"
elif y % 2 == 1:
row = "#" * 45 + "." * 5 if y % 4 == 1 else "." * 5 + "#" * 45
else:
row = "." * 50
medium.append(row)
mazes["Middle with dead ends (50x50)"] = builder.buildFromStringList(medium)
large = []
for y in range(100):
if y == 0: row = "S" + "." * 99
elif y == 99: row = "." * 99 + "E"
elif y % 2 == 1:
row = ("#" * 9 + ".") * 10
else:
row = "." * 100
large.append(row)
mazes["Big (100x100)"] = builder.buildFromStringList(large)
no_exit = [
"S....#....",
"##########",
"##########",
"##########",
"##########",
"##########",
"##########",
"##########",
"##########",
"######...E"
]
mazes["Without exit (10x10)"] = builder.buildFromStringList(no_exit)
return mazes
def main():
mazes = get_test_mazes()
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"AStar": AStarStrategy(),
"Dijkstra": DijkstraStrategy()
}
output_rows = []
print("Запуск всех тестов...")
print("-" * 70)
for maze_name, maze in mazes.items():
print(f"Testing: {maze_name}")
for strat_name, strategy in strategies.items():
solver = MazeSolver(maze, strategy)
runs = 5
total_time = 0
path = []
stats = None
for _ in range(runs):
path, stats = solver.solve()
total_time += stats.timeMs
avg_time = total_time / runs
output_rows.append([
maze_name,
strat_name,
f"{avg_time:.4f}",
stats.visitedCells,
stats.pathLength
])
print(
f" -> {strat_name}: Time: {avg_time:.3f}ms | Passed cells: {stats.visitedCells} | Path length: {stats.pathLength}")
print("-" * 70)
with open("results_all.csv", "w", newline="", encoding="utf-8") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["labyrint", "strategy", "time_ms", "passed_cells", "path_length"])
writer.writerows(output_rows)
print("Все замеры успешно выполнены! Результаты сохранены в 'results_all.csv'")
if __name__ == "__main__":
main()