Merge pull request '[2] 2-nd-exercise' (#268) from anikinvd/2026-rff_mp:2-nd-exercise into develop
Reviewed-on: #268
This commit is contained in:
commit
0bda7aa621
31
anikinvd/docs/data/1-st-exercise/experiment_results.csv
Normal file
31
anikinvd/docs/data/1-st-exercise/experiment_results.csv
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
Structure,Mode,Repeat,Insert (sec),Search (sec),Delete (sec)
|
||||||
|
LinkedList,random,1,4.026961,0.027873,0.012806
|
||||||
|
LinkedList,random,2,4.057927,0.024120,0.015494
|
||||||
|
LinkedList,random,3,4.159901,0.031027,0.012129
|
||||||
|
LinkedList,random,4,4.209198,0.028752,0.015955
|
||||||
|
LinkedList,random,5,4.217042,0.029317,0.012541
|
||||||
|
LinkedList,sorted,1,3.702052,0.023465,0.010952
|
||||||
|
LinkedList,sorted,2,3.723771,0.023921,0.014212
|
||||||
|
LinkedList,sorted,3,3.756407,0.023732,0.010483
|
||||||
|
LinkedList,sorted,4,3.746887,0.026972,0.011036
|
||||||
|
LinkedList,sorted,5,3.784009,0.025765,0.011212
|
||||||
|
HashTable,random,1,0.010695,0.000075,0.000038
|
||||||
|
HashTable,random,2,0.009009,0.000076,0.000039
|
||||||
|
HashTable,random,3,0.009032,0.000069,0.000033
|
||||||
|
HashTable,random,4,0.009581,0.000085,0.000038
|
||||||
|
HashTable,random,5,0.008664,0.000071,0.000035
|
||||||
|
HashTable,sorted,1,0.010321,0.000071,0.000030
|
||||||
|
HashTable,sorted,2,0.008763,0.000070,0.000034
|
||||||
|
HashTable,sorted,3,0.009035,0.000071,0.000033
|
||||||
|
HashTable,sorted,4,0.008954,0.000068,0.000032
|
||||||
|
HashTable,sorted,5,0.008670,0.000071,0.000033
|
||||||
|
BST,random,1,0.025128,0.000209,0.000137
|
||||||
|
BST,random,2,0.023434,0.000202,0.000131
|
||||||
|
BST,random,3,0.023199,0.000195,0.000119
|
||||||
|
BST,random,4,0.023011,0.000210,0.000123
|
||||||
|
BST,random,5,0.025045,0.000263,0.000122
|
||||||
|
BST,sorted,1,9.047348,0.077555,0.047565
|
||||||
|
BST,sorted,2,9.058836,0.081414,0.044913
|
||||||
|
BST,sorted,3,9.021041,0.067645,0.053180
|
||||||
|
BST,sorted,4,9.096998,0.089720,0.047616
|
||||||
|
BST,sorted,5,9.334407,0.081513,0.062546
|
||||||
|
274
anikinvd/docs/data/1-st-exercise/phonebook.py
Normal file
274
anikinvd/docs/data/1-st-exercise/phonebook.py
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import csv
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.setrecursionlimit(20000)
|
||||||
|
|
||||||
|
|
||||||
|
def llist_insert(head, name, phone):
|
||||||
|
current = head
|
||||||
|
while current is not None:
|
||||||
|
if current['name'] == name:
|
||||||
|
current['phone'] = phone
|
||||||
|
return head
|
||||||
|
current = current['next']
|
||||||
|
new_node = {'name': name, 'phone': phone, 'next': None}
|
||||||
|
if head is None:
|
||||||
|
return new_node
|
||||||
|
current = head
|
||||||
|
while current['next'] is not None:
|
||||||
|
current = current['next']
|
||||||
|
current['next'] = new_node
|
||||||
|
return head
|
||||||
|
|
||||||
|
|
||||||
|
def llist_find(head, name):
|
||||||
|
current = head
|
||||||
|
while current is not None:
|
||||||
|
if current['name'] == name:
|
||||||
|
return current['phone']
|
||||||
|
current = current['next']
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def llist_delete(head, name):
|
||||||
|
if head is None:
|
||||||
|
return None
|
||||||
|
if head['name'] == name:
|
||||||
|
return head['next']
|
||||||
|
prev = head
|
||||||
|
current = head['next']
|
||||||
|
while current is not None:
|
||||||
|
if current['name'] == name:
|
||||||
|
prev['next'] = current['next']
|
||||||
|
return head
|
||||||
|
prev = current
|
||||||
|
current = current['next']
|
||||||
|
return head
|
||||||
|
|
||||||
|
|
||||||
|
def llist_get_all(head):
|
||||||
|
entries = []
|
||||||
|
current = head
|
||||||
|
while current is not None:
|
||||||
|
entries.append((current['name'], current['phone']))
|
||||||
|
current = current['next']
|
||||||
|
entries.sort(key=lambda x: x[0])
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
BUCKET_SIZE = 1000
|
||||||
|
|
||||||
|
def ht_create():
|
||||||
|
return [None] * BUCKET_SIZE
|
||||||
|
|
||||||
|
|
||||||
|
def ht_insert(table, name, phone):
|
||||||
|
idx = hash(name) % len(table)
|
||||||
|
table[idx] = llist_insert(table[idx], name, phone)
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
def ht_find(table, name):
|
||||||
|
idx = hash(name) % len(table)
|
||||||
|
return llist_find(table[idx], name)
|
||||||
|
|
||||||
|
|
||||||
|
def ht_delete(table, name):
|
||||||
|
idx = hash(name) % len(table)
|
||||||
|
table[idx] = llist_delete(table[idx], name)
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
def ht_get_all(table):
|
||||||
|
all_entries = []
|
||||||
|
for head in table:
|
||||||
|
current = head
|
||||||
|
while current is not None:
|
||||||
|
all_entries.append((current['name'], current['phone']))
|
||||||
|
current = current['next']
|
||||||
|
all_entries.sort(key=lambda x: x[0])
|
||||||
|
return all_entries
|
||||||
|
|
||||||
|
|
||||||
|
def bst_create_node(name, phone):
|
||||||
|
return {'name': name, 'phone': phone, 'left': None, 'right': None}
|
||||||
|
|
||||||
|
|
||||||
|
def bst_insert(root, name, phone):
|
||||||
|
if root is None:
|
||||||
|
return bst_create_node(name, phone)
|
||||||
|
if name == root['name']:
|
||||||
|
root['phone'] = phone
|
||||||
|
elif name < root['name']:
|
||||||
|
root['left'] = bst_insert(root['left'], name, phone)
|
||||||
|
else:
|
||||||
|
root['right'] = bst_insert(root['right'], name, phone)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def bst_find(root, name):
|
||||||
|
if root is None:
|
||||||
|
return None
|
||||||
|
if name == root['name']:
|
||||||
|
return root['phone']
|
||||||
|
elif name < root['name']:
|
||||||
|
return bst_find(root['left'], name)
|
||||||
|
else:
|
||||||
|
return bst_find(root['right'], name)
|
||||||
|
|
||||||
|
|
||||||
|
def bst_find_min(node):
|
||||||
|
while node['left'] is not None:
|
||||||
|
node = node['left']
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def bst_delete(root, name):
|
||||||
|
if root is None:
|
||||||
|
return None
|
||||||
|
if name < root['name']:
|
||||||
|
root['left'] = bst_delete(root['left'], name)
|
||||||
|
elif name > root['name']:
|
||||||
|
root['right'] = bst_delete(root['right'], name)
|
||||||
|
else:
|
||||||
|
if root['left'] is None:
|
||||||
|
return root['right']
|
||||||
|
if root['right'] is None:
|
||||||
|
return root['left']
|
||||||
|
min_node = bst_find_min(root['right'])
|
||||||
|
root['name'] = min_node['name']
|
||||||
|
root['phone'] = min_node['phone']
|
||||||
|
root['right'] = bst_delete(root['right'], min_node['name'])
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def bst_inorder_collect(root, out_list):
|
||||||
|
if root is not None:
|
||||||
|
bst_inorder_collect(root['left'], out_list)
|
||||||
|
out_list.append((root['name'], root['phone']))
|
||||||
|
bst_inorder_collect(root['right'], out_list)
|
||||||
|
|
||||||
|
|
||||||
|
def bst_get_all(root):
|
||||||
|
result = []
|
||||||
|
bst_inorder_collect(root, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def generate_phonebook_entries(n, seed=42):
|
||||||
|
random.seed(seed)
|
||||||
|
records = []
|
||||||
|
for i in range(1, n + 1):
|
||||||
|
name = f"User_{i:05d}"
|
||||||
|
phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}"
|
||||||
|
records.append((name, phone))
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_datasets(base_records):
|
||||||
|
shuffled = base_records.copy()
|
||||||
|
random.shuffle(shuffled)
|
||||||
|
sorted_records = sorted(base_records, key=lambda x: x[0])
|
||||||
|
return shuffled, sorted_records
|
||||||
|
|
||||||
|
|
||||||
|
def run_experiment(struct_funcs, records, mode_name, repeats=5):
|
||||||
|
all_results = []
|
||||||
|
for rep in range(repeats):
|
||||||
|
struct = struct_funcs['create']()
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name, phone in records:
|
||||||
|
struct = struct_funcs['insert'](struct, name, phone)
|
||||||
|
insert_time = time.perf_counter() - start
|
||||||
|
|
||||||
|
existing_names = [name for name, _ in records]
|
||||||
|
sample_existing = random.sample(existing_names, 100)
|
||||||
|
nonexistent = [f"None_{i}" for i in range(10)]
|
||||||
|
search_names = sample_existing + nonexistent
|
||||||
|
random.shuffle(search_names)
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name in search_names:
|
||||||
|
_ = struct_funcs['find'](struct, name)
|
||||||
|
find_time = time.perf_counter() - start
|
||||||
|
|
||||||
|
to_delete = random.sample(existing_names, 50)
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name in to_delete:
|
||||||
|
struct = struct_funcs['delete'](struct, name)
|
||||||
|
delete_time = time.perf_counter() - start
|
||||||
|
|
||||||
|
all_results.append({
|
||||||
|
'structure': struct_funcs['name'],
|
||||||
|
'mode': mode_name,
|
||||||
|
'repetition': rep + 1,
|
||||||
|
'insert_time': insert_time,
|
||||||
|
'find_time': find_time,
|
||||||
|
'delete_time': delete_time
|
||||||
|
})
|
||||||
|
return all_results
|
||||||
|
|
||||||
|
|
||||||
|
def run_benchmark():
|
||||||
|
N = 10000
|
||||||
|
REPEATS = 5
|
||||||
|
|
||||||
|
base_records = generate_phonebook_entries(N)
|
||||||
|
shuffled_records, sorted_records = prepare_datasets(base_records)
|
||||||
|
|
||||||
|
structures = {
|
||||||
|
'LinkedList': {
|
||||||
|
'name': 'LinkedList',
|
||||||
|
'create': lambda: None,
|
||||||
|
'insert': llist_insert,
|
||||||
|
'find': llist_find,
|
||||||
|
'delete': llist_delete,
|
||||||
|
'get_all': llist_get_all
|
||||||
|
},
|
||||||
|
'HashTable': {
|
||||||
|
'name': 'HashTable',
|
||||||
|
'create': ht_create,
|
||||||
|
'insert': ht_insert,
|
||||||
|
'find': ht_find,
|
||||||
|
'delete': ht_delete,
|
||||||
|
'get_all': ht_get_all
|
||||||
|
},
|
||||||
|
'BST': {
|
||||||
|
'name': 'BST',
|
||||||
|
'create': lambda: None,
|
||||||
|
'insert': bst_insert,
|
||||||
|
'find': bst_find,
|
||||||
|
'delete': bst_delete,
|
||||||
|
'get_all': bst_get_all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
|
||||||
|
for struct_name, funcs in structures.items():
|
||||||
|
results_random = run_experiment(funcs, shuffled_records, 'random', REPEATS)
|
||||||
|
all_results.extend(results_random)
|
||||||
|
results_sorted = run_experiment(funcs, sorted_records, 'sorted', REPEATS)
|
||||||
|
all_results.extend(results_sorted)
|
||||||
|
|
||||||
|
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)'])
|
||||||
|
for r in all_results:
|
||||||
|
writer.writerow([
|
||||||
|
r['structure'],
|
||||||
|
r['mode'],
|
||||||
|
r['repetition'],
|
||||||
|
f"{r['insert_time']:.6f}",
|
||||||
|
f"{r['find_time']:.6f}",
|
||||||
|
f"{r['delete_time']:.6f}"
|
||||||
|
])
|
||||||
|
|
||||||
|
print("Experiment finished. Results saved to 'experiment_results.csv'.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_benchmark()
|
||||||
38
anikinvd/docs/data/1-st-exercise/plot_results.py
Normal file
38
anikinvd/docs/data/1-st-exercise/plot_results.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
df = pd.read_csv('experiment_results.csv')
|
||||||
|
|
||||||
|
mean_times = df.groupby(['Structure', 'Mode'])[['Insert (sec)', 'Search (sec)', 'Delete (sec)']].mean().reset_index()
|
||||||
|
|
||||||
|
structures = mean_times['Structure'].unique()
|
||||||
|
modes = mean_times['Mode'].unique()
|
||||||
|
|
||||||
|
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||||||
|
operations = ['Insert (sec)', 'Search (sec)', 'Delete (sec)']
|
||||||
|
titles = ['Insertion', 'Search', 'Deletion']
|
||||||
|
|
||||||
|
for ax, op, title in zip(axes, operations, titles):
|
||||||
|
x = np.arange(len(structures))
|
||||||
|
width = 0.35
|
||||||
|
|
||||||
|
random_vals = []
|
||||||
|
sorted_vals = []
|
||||||
|
for s in structures:
|
||||||
|
random_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'random')]
|
||||||
|
sorted_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'sorted')]
|
||||||
|
random_vals.append(random_row[op].values[0] if not random_row.empty else 0)
|
||||||
|
sorted_vals.append(sorted_row[op].values[0] if not sorted_row.empty else 0)
|
||||||
|
|
||||||
|
ax.bar(x - width/2, random_vals, width, label='Random order')
|
||||||
|
ax.bar(x + width/2, sorted_vals, width, label='Sorted order')
|
||||||
|
ax.set_xticks(x)
|
||||||
|
ax.set_xticklabels(structures)
|
||||||
|
ax.set_ylabel('Time (seconds)')
|
||||||
|
ax.set_title(title)
|
||||||
|
ax.legend()
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig('performance_comparison.png', dpi=150)
|
||||||
|
plt.show()
|
||||||
16
anikinvd/docs/data/2-nd-exercise/experiment_data.csv
Normal file
16
anikinvd/docs/data/2-nd-exercise/experiment_data.csv
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
maze,strategy,time_ms,visited_cells,path_length
|
||||||
|
Small 10x6,BFS,0.025158666630886728,9.0,5.0
|
||||||
|
Small 10x6,DFS,0.04097166674910113,26.0,19.0
|
||||||
|
Small 10x6,AStar,0.015256333426805213,5.0,5.0
|
||||||
|
Medium 10x10,BFS,0.015568000132285912,18.0,8.0
|
||||||
|
Medium 10x10,DFS,0.007917000099647945,9.0,8.0
|
||||||
|
Medium 10x10,AStar,0.014829333395027788,8.0,8.0
|
||||||
|
Large 20x20,BFS,0.13646366672522467,116.0,69.0
|
||||||
|
Large 20x20,DFS,0.15918433321833922,173.0,69.0
|
||||||
|
Large 20x20,AStar,0.19781433320531505,110.0,69.0
|
||||||
|
Empty 15x15,BFS,0.25488699990698177,240.0,29.0
|
||||||
|
Empty 15x15,DFS,0.14207733314227275,224.0,119.0
|
||||||
|
Empty 15x15,AStar,0.5900679999892114,224.0,29.0
|
||||||
|
No exit 10x10,BFS,0.04236899985698983,36.0,0.0
|
||||||
|
No exit 10x10,DFS,0.03538033342920244,36.0,0.0
|
||||||
|
No exit 10x10,AStar,0.06468633318945649,36.0,0.0
|
||||||
|
490
anikinvd/docs/data/2-nd-exercise/main.py
Normal file
490
anikinvd/docs/data/2-nd-exercise/main.py
Normal file
|
|
@ -0,0 +1,490 @@
|
||||||
|
import sys
|
||||||
|
from collections import deque
|
||||||
|
import heapq
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Модель лабиринта ----------
|
||||||
|
class Tile:
|
||||||
|
def __init__(self, column, row):
|
||||||
|
self.col = column
|
||||||
|
self.row = row
|
||||||
|
self.blocked = False
|
||||||
|
self.is_start = False
|
||||||
|
self.is_exit = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x(self):
|
||||||
|
return self.col
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y(self):
|
||||||
|
return self.row
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_wall(self):
|
||||||
|
return self.blocked
|
||||||
|
|
||||||
|
@is_wall.setter
|
||||||
|
def is_wall(self, value):
|
||||||
|
self.blocked = value
|
||||||
|
|
||||||
|
def can_step(self):
|
||||||
|
return not self.blocked
|
||||||
|
|
||||||
|
|
||||||
|
class Labyrinth:
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self._w = width
|
||||||
|
self._h = height
|
||||||
|
self._grid = [[Tile(x, y) for x in range(width)] for y in range(height)]
|
||||||
|
self._start_tile = None
|
||||||
|
self._exit_tile = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self):
|
||||||
|
return self._w
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self):
|
||||||
|
return self._h
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start(self):
|
||||||
|
return self._start_tile
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exit(self):
|
||||||
|
return self._exit_tile
|
||||||
|
|
||||||
|
def get_tile(self, x, y):
|
||||||
|
if 0 <= x < self._w and 0 <= y < self._h:
|
||||||
|
return self._grid[y][x]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_tile_type(self, x, y, kind):
|
||||||
|
tile = self.get_tile(x, y)
|
||||||
|
if tile is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if kind == 'wall':
|
||||||
|
tile.blocked = True
|
||||||
|
elif kind == 'start':
|
||||||
|
if self._start_tile:
|
||||||
|
self._start_tile.is_start = False
|
||||||
|
tile.is_start = True
|
||||||
|
tile.blocked = False
|
||||||
|
self._start_tile = tile
|
||||||
|
elif kind == 'exit':
|
||||||
|
if self._exit_tile:
|
||||||
|
self._exit_tile.is_exit = False
|
||||||
|
tile.is_exit = True
|
||||||
|
tile.blocked = False
|
||||||
|
self._exit_tile = tile
|
||||||
|
elif kind == 'path':
|
||||||
|
tile.blocked = False
|
||||||
|
|
||||||
|
def neighbours(self, tile):
|
||||||
|
"""Возвращает список проходимых соседей"""
|
||||||
|
result = []
|
||||||
|
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] # вверх, вниз, влево, вправо
|
||||||
|
for dx, dy in directions:
|
||||||
|
nx, ny = tile.x + dx, tile.y + dy
|
||||||
|
nb = self.get_tile(nx, ny)
|
||||||
|
if nb and nb.can_step():
|
||||||
|
result.append(nb)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Загрузка лабиринта ----------
|
||||||
|
class MazeLoader:
|
||||||
|
def load(self, filename):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class TxtMazeLoader(MazeLoader):
|
||||||
|
def load(self, filename):
|
||||||
|
with open(filename, '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
|
||||||
|
lab = Labyrinth(w, h)
|
||||||
|
|
||||||
|
for y, line in enumerate(lines):
|
||||||
|
for x, ch in enumerate(line):
|
||||||
|
if ch == "#":
|
||||||
|
lab.set_tile_type(x, y, "wall")
|
||||||
|
elif ch == "S":
|
||||||
|
lab.set_tile_type(x, y, "start")
|
||||||
|
start_cnt += 1
|
||||||
|
elif ch == "E":
|
||||||
|
lab.set_tile_type(x, y, "exit")
|
||||||
|
exit_cnt += 1
|
||||||
|
else:
|
||||||
|
lab.set_tile_type(x, y, 'path')
|
||||||
|
|
||||||
|
if start_cnt != 1 or exit_cnt != 1:
|
||||||
|
raise ValueError(f"Maze error: S={start_cnt}, E={exit_cnt} (need exactly one each)")
|
||||||
|
return lab
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Стратегии поиска пути ----------
|
||||||
|
class SearchStrategy:
|
||||||
|
def find_path(self, lab, start, goal):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _rebuild_path(self, came_from, start, goal):
|
||||||
|
path = []
|
||||||
|
cur = goal
|
||||||
|
while cur is not None:
|
||||||
|
path.append(cur)
|
||||||
|
cur = came_from.get(cur)
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
def visited_cells(self):
|
||||||
|
return getattr(self, '_visited', 0)
|
||||||
|
|
||||||
|
|
||||||
|
class BFS(SearchStrategy):
|
||||||
|
def find_path(self, lab, start, goal):
|
||||||
|
q = deque()
|
||||||
|
q.append(start)
|
||||||
|
parent = {start: None}
|
||||||
|
visited = {start}
|
||||||
|
|
||||||
|
while q:
|
||||||
|
cur = q.popleft()
|
||||||
|
if cur == goal:
|
||||||
|
self._visited = len(visited)
|
||||||
|
return self._rebuild_path(parent, start, goal)
|
||||||
|
for nb in lab.neighbours(cur):
|
||||||
|
if nb not in visited:
|
||||||
|
visited.add(nb)
|
||||||
|
parent[nb] = cur
|
||||||
|
q.append(nb)
|
||||||
|
self._visited = len(visited)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class DFS(SearchStrategy):
|
||||||
|
def find_path(self, lab, start, goal):
|
||||||
|
stack = [start]
|
||||||
|
parent = {start: None}
|
||||||
|
visited = {start}
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
cur = stack.pop()
|
||||||
|
if cur == goal:
|
||||||
|
self._visited = len(visited)
|
||||||
|
return self._rebuild_path(parent, start, goal)
|
||||||
|
for nb in lab.neighbours(cur):
|
||||||
|
if nb not in visited:
|
||||||
|
visited.add(nb)
|
||||||
|
parent[nb] = cur
|
||||||
|
stack.append(nb)
|
||||||
|
self._visited = len(visited)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class AStar(SearchStrategy):
|
||||||
|
def _heuristic(self, a, b):
|
||||||
|
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||||
|
|
||||||
|
def find_path(self, lab, start, goal):
|
||||||
|
heap = []
|
||||||
|
counter = 0
|
||||||
|
start_f = self._heuristic(start, goal)
|
||||||
|
heapq.heappush(heap, (start_f, counter, start))
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
parent = {}
|
||||||
|
g = {start: 0}
|
||||||
|
f = {start: start_f}
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
while heap:
|
||||||
|
cur_f, _, cur = heapq.heappop(heap)
|
||||||
|
visited.add(cur)
|
||||||
|
if cur == goal:
|
||||||
|
self._visited = len(visited)
|
||||||
|
return self._rebuild_path(parent, start, goal)
|
||||||
|
if cur_f > f.get(cur, float('inf')):
|
||||||
|
continue
|
||||||
|
for nb in lab.neighbours(cur):
|
||||||
|
new_g = g[cur] + 1
|
||||||
|
if new_g < g.get(nb, float('inf')):
|
||||||
|
parent[nb] = cur
|
||||||
|
g[nb] = new_g
|
||||||
|
new_f = new_g + self._heuristic(nb, goal)
|
||||||
|
f[nb] = new_f
|
||||||
|
heapq.heappush(heap, (new_f, counter, nb))
|
||||||
|
counter += 1
|
||||||
|
self._visited = len(visited)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Статистика ----------
|
||||||
|
class SearchStats:
|
||||||
|
def __init__(self, time_ms, visited, path_len):
|
||||||
|
self.time_ms = time_ms
|
||||||
|
self.visited_cells = visited
|
||||||
|
self.path_length = path_len
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Наблюдатель ----------
|
||||||
|
class Observer:
|
||||||
|
def notify(self, event, data):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleDisplay(Observer):
|
||||||
|
def __init__(self, player=None):
|
||||||
|
self._last_path = None
|
||||||
|
self._player = player
|
||||||
|
|
||||||
|
def notify(self, event, data):
|
||||||
|
if event == "maze_loaded":
|
||||||
|
self._draw_maze(data)
|
||||||
|
elif event == "path_found":
|
||||||
|
self._last_path = data
|
||||||
|
self._show_path(data)
|
||||||
|
elif event == "player_moved":
|
||||||
|
self._draw_maze_with_player(data)
|
||||||
|
|
||||||
|
def _draw_maze(self, lab):
|
||||||
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
|
print("=" * (lab.width * 2 + 4))
|
||||||
|
print(" LABYRINTH")
|
||||||
|
print("=" * (lab.width * 2 + 4))
|
||||||
|
|
||||||
|
for y in range(lab.height):
|
||||||
|
print(" ", end='')
|
||||||
|
for x in range(lab.width):
|
||||||
|
cell = lab.get_tile(x, y)
|
||||||
|
if cell == lab.start:
|
||||||
|
print('S', end=' ')
|
||||||
|
elif cell == lab.exit:
|
||||||
|
print('E', end=' ')
|
||||||
|
elif cell.is_wall:
|
||||||
|
print('#', end=' ')
|
||||||
|
else:
|
||||||
|
print('.', end=' ')
|
||||||
|
print()
|
||||||
|
print("=" * (lab.width * 2 + 4))
|
||||||
|
print(" S - start E - exit # - wall . - free")
|
||||||
|
|
||||||
|
def _draw_maze_with_player(self, lab):
|
||||||
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
|
print("=" * (lab.width * 2 + 4))
|
||||||
|
print(" LABYRINTH (P = player)")
|
||||||
|
print("=" * (lab.width * 2 + 4))
|
||||||
|
|
||||||
|
for y in range(lab.height):
|
||||||
|
print(" ", end='')
|
||||||
|
for x in range(lab.width):
|
||||||
|
cell = lab.get_tile(x, y)
|
||||||
|
if self._player and cell == self._player.position:
|
||||||
|
print('P', end=' ')
|
||||||
|
elif cell == lab.start:
|
||||||
|
print('S', end=' ')
|
||||||
|
elif cell == lab.exit:
|
||||||
|
print('E', end=' ')
|
||||||
|
elif cell.is_wall:
|
||||||
|
print('#', end=' ')
|
||||||
|
else:
|
||||||
|
print('.', end=' ')
|
||||||
|
print()
|
||||||
|
print("=" * (lab.width * 2 + 4))
|
||||||
|
print(f" Player at: ({self._player.position.x}, {self._player.position.y})")
|
||||||
|
print(" S - start E - exit # - wall . - free P - player")
|
||||||
|
|
||||||
|
def _show_path(self, path):
|
||||||
|
if not path:
|
||||||
|
print("\n No route found!")
|
||||||
|
return
|
||||||
|
print(f"\n Route found! Length = {len(path)}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Игрок и команды ----------
|
||||||
|
class Player:
|
||||||
|
def __init__(self, start_cell, lab):
|
||||||
|
self._pos = start_cell
|
||||||
|
self._prev = None
|
||||||
|
self._lab = lab
|
||||||
|
|
||||||
|
@property
|
||||||
|
def position(self):
|
||||||
|
return self._pos
|
||||||
|
|
||||||
|
def move(self, target):
|
||||||
|
if target and target.can_step():
|
||||||
|
self._prev = self._pos
|
||||||
|
self._pos = target
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self._prev:
|
||||||
|
self._pos, self._prev = self._prev, None
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Action:
|
||||||
|
def execute(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def revert(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class MoveAction(Action):
|
||||||
|
def __init__(self, player, direction, lab):
|
||||||
|
self._player = player
|
||||||
|
self._dx, self._dy = direction
|
||||||
|
self._lab = lab
|
||||||
|
self._done = False
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
new_x = self._player.position.x + self._dx
|
||||||
|
new_y = self._player.position.y + self._dy
|
||||||
|
target = self._lab.get_tile(new_x, new_y)
|
||||||
|
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 LabyrinthSolver:
|
||||||
|
def __init__(self, lab):
|
||||||
|
self._lab = lab
|
||||||
|
self._strategy = None
|
||||||
|
self._watchers = []
|
||||||
|
|
||||||
|
def attach(self, observer):
|
||||||
|
self._watchers.append(observer)
|
||||||
|
|
||||||
|
def _broadcast(self, event, data):
|
||||||
|
for obs in self._watchers:
|
||||||
|
obs.notify(event, data)
|
||||||
|
|
||||||
|
def set_algorithm(self, strategy):
|
||||||
|
self._strategy = strategy
|
||||||
|
|
||||||
|
def solve(self):
|
||||||
|
if self._strategy is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit)
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
elapsed_ms = (t1 - t0) * 1000
|
||||||
|
|
||||||
|
self._broadcast("path_found", path)
|
||||||
|
return SearchStats(elapsed_ms, self._strategy.visited_cells(), len(path))
|
||||||
|
|
||||||
|
|
||||||
|
def run_experiment(maze_file, algorithm, runs=5):
|
||||||
|
loader = TxtMazeLoader()
|
||||||
|
lab = loader.load(maze_file)
|
||||||
|
|
||||||
|
total_time = 0.0
|
||||||
|
total_visited = 0
|
||||||
|
total_length = 0
|
||||||
|
|
||||||
|
for _ in range(runs):
|
||||||
|
solver = LabyrinthSolver(lab)
|
||||||
|
solver.set_algorithm(algorithm)
|
||||||
|
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 len(sys.argv) > 1 and sys.argv[1] == 'experiment':
|
||||||
|
print("Running experiments...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
loader = TxtMazeLoader()
|
||||||
|
lab = loader.load("maze1.txt")
|
||||||
|
|
||||||
|
player = Player(lab.start, lab)
|
||||||
|
view = ConsoleDisplay(player)
|
||||||
|
view.notify("maze_loaded", lab)
|
||||||
|
|
||||||
|
solver = LabyrinthSolver(lab)
|
||||||
|
solver.attach(view)
|
||||||
|
|
||||||
|
print("\n CONTROLS:")
|
||||||
|
print(" H (left) J (down) K (up) L (right)")
|
||||||
|
print(" U - undo Q - quit")
|
||||||
|
print("\n AUTO SEARCH:")
|
||||||
|
print(" B - BFS D - DFS A - A*")
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
|
||||||
|
history = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
cmd = input("\n Command > ").lower()
|
||||||
|
|
||||||
|
if cmd == 'q':
|
||||||
|
print("\n Goodbye!")
|
||||||
|
break
|
||||||
|
elif cmd == 'b':
|
||||||
|
solver.set_algorithm(BFS())
|
||||||
|
stats = solver.solve()
|
||||||
|
print(f"\n BFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
|
||||||
|
elif cmd == 'd':
|
||||||
|
solver.set_algorithm(DFS())
|
||||||
|
stats = solver.solve()
|
||||||
|
print(f"\n DFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
|
||||||
|
elif cmd == 'a':
|
||||||
|
solver.set_algorithm(AStar())
|
||||||
|
stats = solver.solve()
|
||||||
|
print(f"\n A*: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
|
||||||
|
elif cmd in ['h', 'j', 'k', 'l']:
|
||||||
|
dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
|
||||||
|
action = MoveAction(player, dir_map[cmd], lab)
|
||||||
|
if action.execute():
|
||||||
|
history.append(action)
|
||||||
|
view.notify("player_moved", lab)
|
||||||
|
if player.position == lab.exit:
|
||||||
|
print("\n *** VICTORY! EXIT REACHED ***")
|
||||||
|
print(f" Moves made: {len(history)}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("\n Blocked by wall!")
|
||||||
|
elif cmd == 'u':
|
||||||
|
if history:
|
||||||
|
act = history.pop()
|
||||||
|
act.revert()
|
||||||
|
view.notify("player_moved", lab)
|
||||||
|
print("\n Undo successful")
|
||||||
|
else:
|
||||||
|
print("\n Nothing to undo")
|
||||||
|
else:
|
||||||
|
print("\n Unknown command. Use h,j,k,l to move, u to undo, q to quit")
|
||||||
|
|
||||||
|
print("\n Game over. Thanks for playing!")
|
||||||
7
anikinvd/docs/data/2-nd-exercise/maze1.txt
Normal file
7
anikinvd/docs/data/2-nd-exercise/maze1.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
##########
|
||||||
|
#S.......#
|
||||||
|
#.######.#
|
||||||
|
#.#......#
|
||||||
|
#.#.######
|
||||||
|
#E........
|
||||||
|
##########
|
||||||
10
anikinvd/docs/data/2-nd-exercise/maze10x10.txt
Normal file
10
anikinvd/docs/data/2-nd-exercise/maze10x10.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
##########
|
||||||
|
#S......E#
|
||||||
|
#.########
|
||||||
|
#.#......#
|
||||||
|
#.#.#.##.#
|
||||||
|
#...#..#.#
|
||||||
|
###.#..#.#
|
||||||
|
#...#....#
|
||||||
|
#.########
|
||||||
|
##########
|
||||||
21
anikinvd/docs/data/2-nd-exercise/maze20x20.txt
Normal file
21
anikinvd/docs/data/2-nd-exercise/maze20x20.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
####################
|
||||||
|
#S.................#
|
||||||
|
#.####.###########.#
|
||||||
|
#.#..#.#.........#.#
|
||||||
|
#.#.##.#.#######.#.#
|
||||||
|
#.#....#.#.....#.#.#
|
||||||
|
#.######.#.###.#.#.#
|
||||||
|
#........#.#...#.#.#
|
||||||
|
##########.#.###.#.#
|
||||||
|
#..........#.....#.#
|
||||||
|
#.################.#
|
||||||
|
#.#..............#.#
|
||||||
|
#.#.############.#.#
|
||||||
|
#.#.#..........#.#.#
|
||||||
|
#.#.#.########.#.#.#
|
||||||
|
#...#........#...#.#
|
||||||
|
#.###########.###.#.#
|
||||||
|
#.................#.#
|
||||||
|
#.#################.#
|
||||||
|
#E.................#
|
||||||
|
####################
|
||||||
15
anikinvd/docs/data/2-nd-exercise/maze_empty.txt
Normal file
15
anikinvd/docs/data/2-nd-exercise/maze_empty.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
S...............
|
||||||
|
................
|
||||||
|
................
|
||||||
|
................
|
||||||
|
................
|
||||||
|
................
|
||||||
|
................
|
||||||
|
................
|
||||||
|
................
|
||||||
|
................
|
||||||
|
................
|
||||||
|
................
|
||||||
|
................
|
||||||
|
...............E
|
||||||
|
................
|
||||||
10
anikinvd/docs/data/2-nd-exercise/maze_no_exit.txt
Normal file
10
anikinvd/docs/data/2-nd-exercise/maze_no_exit.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
##########
|
||||||
|
#S........
|
||||||
|
#.######.#
|
||||||
|
#.#......#
|
||||||
|
#.#.######
|
||||||
|
#.#......#
|
||||||
|
#.########
|
||||||
|
#........#
|
||||||
|
##########
|
||||||
|
##########
|
||||||
370
anikinvd/docs/data/2-nd-exercise/plots.py
Normal file
370
anikinvd/docs/data/2-nd-exercise/plots.py
Normal file
|
|
@ -0,0 +1,370 @@
|
||||||
|
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")
|
||||||
BIN
anikinvd/docs/performance_comparison.png
Normal file
BIN
anikinvd/docs/performance_comparison.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
BIN
anikinvd/docs/performance_plot.png
Normal file
BIN
anikinvd/docs/performance_plot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
78
anikinvd/docs/report-1-st.md
Normal file
78
anikinvd/docs/report-1-st.md
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Лабораторная работа: Сравнение структур данных для телефонного справочника
|
||||||
|
|
||||||
|
## 1. Введение
|
||||||
|
|
||||||
|
В рамках работы были реализованы три структуры данных «с нуля» на языке Python без использования классов, в процедурной парадигме:
|
||||||
|
|
||||||
|
- **Связный список** — узлы хранятся в виде словарей `{'name', 'phone', 'next'}`.
|
||||||
|
- **Хеш-таблица** — массив фиксированного размера (1000 корзин), в каждой из которых хранится связный список (отдельные цепочки).
|
||||||
|
- **Двоичное дерево поиска (BST)** — узлы имеют поля `left`, `right`.
|
||||||
|
|
||||||
|
Для каждой структуры реализованы операции `insert`, `find`, `delete`, `list_all` (возвращает записи, отсортированные по имени).
|
||||||
|
|
||||||
|
Цель эксперимента — измерить производительность операций на наборе из **10 000 записей** при двух режимах подачи данных: **случайный порядок** и **отсортированный по имени**. Каждый опыт повторялся 5 раз, результаты усреднены. Измерялось общее время:
|
||||||
|
|
||||||
|
- вставки всех 10 000 записей;
|
||||||
|
- поиска 110 записей (100 существующих + 10 несуществующих);
|
||||||
|
- удаления 50 случайных записей.
|
||||||
|
|
||||||
|
## 2. Результаты измерений
|
||||||
|
|
||||||
|
В таблице приведены средние значения времени (в секундах) для каждой структуры и режима.
|
||||||
|
|
||||||
|
| Структура | Режим | Вставка (с) | Поиск (с) | Удаление (с) |
|
||||||
|
|----------------|-------------|------------|-----------|--------------|
|
||||||
|
| **LinkedList** | случайный | 4.1342 | 0.0282 | 0.0138 |
|
||||||
|
| LinkedList | сортир. | 3.7426 | 0.0248 | 0.0116 |
|
||||||
|
| **HashTable** | случайный | 0.00940 | 0.000075 | 0.000037 |
|
||||||
|
| HashTable | сортир. | 0.00915 | 0.000070 | 0.000032 |
|
||||||
|
| **BST** | случайный | 0.02396 | 0.000216 | 0.000126 |
|
||||||
|
| BST | сортир. | 9.1117 | 0.0796 | 0.0512 |
|
||||||
|
|
||||||
|
*Графическое представление результатов* приведено на рисунке `performance_comparison.png`, где для каждой операции построены столбчатые диаграммы с группировкой по структурам и режимам.
|
||||||
|
|
||||||
|
## 3. Анализ результатов
|
||||||
|
|
||||||
|
### 3.1. Влияние порядка данных на BST
|
||||||
|
|
||||||
|
Двоичное дерево поиска чувствительно к порядку поступления ключей. При вставке в отсортированном порядке дерево вырождается в линейный список (все узлы уходят в правое поддерево). Высота становится O(n), что приводит к резкому падению производительности:
|
||||||
|
|
||||||
|
- Вставка на отсортированных данных (9.11 с) **медленнее в 380 раз**, чем на случайных (0.024 с).
|
||||||
|
- Поиск замедляется в ~370 раз, удаление — в ~406 раз.
|
||||||
|
|
||||||
|
Фактически BST на отсортированных данных работает хуже даже связного списка из‑за рекурсивных вызовов и накладных расходов.
|
||||||
|
|
||||||
|
### 3.2. Устойчивость хеш-таблицы к порядку
|
||||||
|
|
||||||
|
Хеш-функция равномерно распределяет имена по корзинам вне зависимости от порядка поступления. Поэтому времена вставки, поиска и удаления практически идентичны для случайного и отсортированного режимов:
|
||||||
|
|
||||||
|
- Вставка: 0.00940 с (случ.) vs 0.00915 с (сорт.) — разница в пределах погрешности.
|
||||||
|
- Поиск и удаление также стабильны.
|
||||||
|
|
||||||
|
Средняя сложность O(1) подтверждается на практике.
|
||||||
|
|
||||||
|
### 3.3. Связный список — линейная сложность на всех операциях
|
||||||
|
|
||||||
|
Связный список не обеспечивает прямого доступа к элементам. Для поиска, обновления или удаления требуется последовательный проход, что даёт O(n).
|
||||||
|
|
||||||
|
- Вставка 10 000 элементов занимает около 4 секунд (даже больше, чем BST на случайных данных).
|
||||||
|
- Поиск (~0.028 с) на порядок медленнее, чем в хеш-таблице и BST на случайных данных.
|
||||||
|
- Порядок входных данных почти не влияет на производительность (разница менее 10%), так как в любом случае приходится обходить список до конца для вставки новых уникальных имён.
|
||||||
|
|
||||||
|
### 3.4. Сравнение удаления
|
||||||
|
|
||||||
|
- **Связный список**: удаление требует сначала найти элемент (O(n)), затем переставить ссылки. Время ~0.012–0.014 с, что близко ко времени поиска.
|
||||||
|
- **Хеш-таблица**: удаление за O(1) в среднем — достаточно вычислить хеш и удалить из короткого списка корзины. Время ~0.00003–0.00004 с.
|
||||||
|
- **BST**: на случайных данных удаление очень быстрое (0.000126 с) благодаря логарифмической высоте. На отсортированных данных время возрастает до 0.051 с (деградация до O(n)).
|
||||||
|
|
||||||
|
## 4. Выводы и рекомендации
|
||||||
|
|
||||||
|
На основе полученных результатов можно сделать следующие выводы о применимости структур в реальных задачах:
|
||||||
|
|
||||||
|
- **Хеш-таблица** — лучший выбор, когда требуется максимальная скорость всех операций (вставка, поиск, удаление) и не важен порядок хранения. Она стабильна, не чувствительна к порядку входных данных и показывает среднее время O(1). Идеальна для реализации словарей, кэшей, индексов по ключу.
|
||||||
|
|
||||||
|
- **Двоичное дерево поиска** — подходит, когда необходимо часто получать данные в отсортированном виде (например, вывод справочника по алфавиту) и гарантируется, что данные не будут поступать в отсортированном порядке (иначе дерево вырождается). В реальных проектах вместо простого BST следует использовать самобалансирующиеся деревья (AVL, красно-чёрные), которые сохраняют логарифмическую высоту при любых порядках. В эксперименте BST на случайных данных показал отличные результаты, близкие к хеш-таблице.
|
||||||
|
|
||||||
|
- **Связный список** — из‑за линейной сложности основных операций непригоден для хранения больших объёмов данных (тысячи и более записей). Может применяться лишь для очень маленьких коллекций, при частых вставках в начало (здесь не рассматривалось) или в учебных целях.
|
||||||
|
|
||||||
|
Таким образом, для телефонного справочника с 10 000 записей наиболее эффективной является **хеш-таблица**, обеспечивающая мгновенный доступ по имени. Если же требуется ещё и алфавитный вывод без дополнительной сортировки, стоит использовать **сбалансированное дерево поиска**.
|
||||||
125
anikinvd/docs/report-2-nd.md
Normal file
125
anikinvd/docs/report-2-nd.md
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
# Лабораторная работа: Поиск выхода из лабиринта
|
||||||
|
|
||||||
|
## 1. Постановка задачи
|
||||||
|
|
||||||
|
Разработать программу для загрузки лабиринта из текстового файла, поиска пути от стартовой клетки до выхода с возможностью выбора алгоритма поиска, визуализации процесса и экспериментального сравнения эффективности алгоритмов.
|
||||||
|
|
||||||
|
### Основные требования
|
||||||
|
|
||||||
|
- Реализовать модель лабиринта (классы `Cell`, `Maze`)
|
||||||
|
- Реализовать загрузку лабиринта из файла с символами `#` (стена), `S` (старт), `E` (выход)
|
||||||
|
- Реализовать три алгоритма поиска пути: BFS, DFS, A*
|
||||||
|
- Реализовать класс-оркестратор `MazeSolver` с возможностью смены стратегии
|
||||||
|
- Собрать статистику: время выполнения, количество посещённых клеток, длина пути
|
||||||
|
- Провести эксперименты на лабиринтах разной сложности
|
||||||
|
|
||||||
|
### Использованные паттерны проектирования GoF
|
||||||
|
|
||||||
|
| Паттерн | Где используется | Преимущества |
|
||||||
|
|---------|----------------|---------------|
|
||||||
|
| **Builder** | `MazeBuilder`, `TextFileMazeBuilder` | Скрывает детали парсинга, позволяет легко добавлять новые форматы файлов |
|
||||||
|
| **Strategy** | `PathFindingStrategy`, `BFSStrategy`, `DFSStrategy`, `AStarStrategy` | Позволяет динамически менять алгоритм поиска, упрощает добавление новых |
|
||||||
|
| **Observer** | `Observer`, `ConsoleView` | Отделяет отображение от логики, легко добавить новые виды вывода |
|
||||||
|
| **Command** | `Command`, `MoveCommand` | Реализует пошаговое перемещение с возможностью отмены (undo/redo) |
|
||||||
|
|
||||||
|
## 2. Архитектура приложения
|
||||||
|
|
||||||
|
Основные компоненты:
|
||||||
|
|
||||||
|
- **Модель** – `Cell`, `Maze` (хранение сетки, проверка стен, получение соседей)
|
||||||
|
- **Загрузка** – `MazeBuilder`, `TextFileMazeBuilder` (парсинг `.txt`‑файлов)
|
||||||
|
- **Алгоритмы** – `BFSStrategy`, `DFSStrategy`, `AStarStrategy` (реализация поиска пути)
|
||||||
|
- **Оркестрация** – `MazeSolver` (управление стратегией, сбор статистики, уведомление наблюдателей)
|
||||||
|
- **Визуализация** – `ConsoleView` (отрисовка лабиринта, игрока, пути)
|
||||||
|
- **Интерактив** – `Player`, `MoveCommand` (перемещение, история ходов)
|
||||||
|
|
||||||
|
## 3. Реализация алгоритмов поиска пути
|
||||||
|
|
||||||
|
| Алгоритм | Структура данных | Гарантия кратчайшего пути | Особенности |
|
||||||
|
|----------|-----------------|---------------------------|-------------|
|
||||||
|
| **BFS** | Очередь (`deque`) | Да | Обходит лабиринт по слоям, гарантирует минимум шагов |
|
||||||
|
| **DFS** | Стек | Нет | Углубляется до конца, затем возвращается; экономичен по памяти |
|
||||||
|
| **A*** | Приоритетная очередь (`heapq`) + эвристика | Да (при допустимой эвристике) | Использует манхэттенское расстояние, обычно быстрее BFS |
|
||||||
|
|
||||||
|
## 4. Экспериментальная часть
|
||||||
|
|
||||||
|
### Тестовые лабиринты
|
||||||
|
|
||||||
|
| Имя | Размер | Описание |
|
||||||
|
|-----|--------|----------|
|
||||||
|
| `Small 10x6` | 10×6 | Простой лабиринт из условия |
|
||||||
|
| `Medium 10x10` | 10×10 | Лабиринт среднего размера со случайными стенами |
|
||||||
|
| `Large 20x20` | 20×20 | Большой запутанный лабиринт |
|
||||||
|
| `Empty 15x15` | 15×15 | Пустой лабиринт (без стен) |
|
||||||
|
| `No exit 10x10` | 10×10 | Лабиринт без достижимого выхода |
|
||||||
|
|
||||||
|
Каждый алгоритм запускался **3 раза** на каждом лабиринте, результаты усреднены.
|
||||||
|
|
||||||
|
### Результаты замеров
|
||||||
|
|
||||||
|
| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |
|
||||||
|
|----------|----------|------------|-----------------|------------|
|
||||||
|
| Small 10x6 | BFS | 0.025 | 9 | 5 |
|
||||||
|
| Small 10x6 | DFS | 0.041 | 26 | 19 |
|
||||||
|
| Small 10x6 | A* | 0.015 | 5 | 5 |
|
||||||
|
| Medium 10x10 | BFS | 0.016 | 18 | 8 |
|
||||||
|
| Medium 10x10 | DFS | 0.008 | 9 | 8 |
|
||||||
|
| Medium 10x10 | A* | 0.015 | 8 | 8 |
|
||||||
|
| Large 20x20 | BFS | 0.136 | 116 | 69 |
|
||||||
|
| Large 20x20 | DFS | 0.159 | 173 | 69 |
|
||||||
|
| Large 20x20 | A* | 0.198 | 110 | 69 |
|
||||||
|
| Empty 15x15 | BFS | 0.255 | 240 | 29 |
|
||||||
|
| Empty 15x15 | DFS | 0.142 | 224 | 119 |
|
||||||
|
| Empty 15x15 | A* | 0.590 | 224 | 29 |
|
||||||
|
| No exit 10x10 | BFS | 0.042 | 36 | 0 |
|
||||||
|
| No exit 10x10 | DFS | 0.035 | 36 | 0 |
|
||||||
|
| No exit 10x10 | A* | 0.065 | 36 | 0 |
|
||||||
|
|
||||||
|
### Графики
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
На графике представлено сравнение трёх алгоритмов по трём метрикам: время выполнения, количество посещённых клеток и длина найденного пути.
|
||||||
|
|
||||||
|
## 5. Анализ результатов
|
||||||
|
|
||||||
|
### Сравнение характеристик
|
||||||
|
|
||||||
|
- **BFS**
|
||||||
|
- Гарантирует кратчайший путь (во всех лабиринтах, где путь существует, длина совпадает с A*).
|
||||||
|
- Посещает довольно много клеток (например, 240 в пустом лабиринте).
|
||||||
|
- Время стабильно, но на больших лабиринтах уступает DFS по скорости.
|
||||||
|
|
||||||
|
- **DFS**
|
||||||
|
- Самый быстрый на средних и больших лабиринтах (0.008–0.159 мс).
|
||||||
|
- Не находит кратчайший путь: в пустом лабиринте длина пути 119 вместо 29.
|
||||||
|
- Посещает среднее количество клеток (224 в пустом, 173 в большом).
|
||||||
|
|
||||||
|
- **A***
|
||||||
|
- Всегда находит оптимальный путь (как BFS).
|
||||||
|
- Посещает **наименьшее** число клеток среди всех алгоритмов (5 в маленьком лабиринте, 110 в большом).
|
||||||
|
- Время работы на пустом лабиринте выше из‑за накладных расходов на эвристику и приоритетную очередь (0.590 мс против 0.142 мс у DFS).
|
||||||
|
- На сложных лабиринтах (Large 20x20) время сравнимо с BFS и даже немного больше из‑за более сложных операций с кучей.
|
||||||
|
|
||||||
|
### Ключевые выводы
|
||||||
|
|
||||||
|
1. **A* показывает лучший баланс** между оптимальностью пути и количеством посещённых клеток. Он особенно эффективен, когда требуется минимальное разрастание поиска.
|
||||||
|
2. **DFS – самый быстрый**, если не важна длина пути (например, для проверки существования выхода).
|
||||||
|
3. **BFS** остаётся простым и предсказуемым, но уступает A* по числу посещений.
|
||||||
|
4. В лабиринте без выхода все алгоритмы корректно обходят всю достижимую область (36 клеток) и возвращают пустой путь.
|
||||||
|
|
||||||
|
### Рекомендации по выбору алгоритма
|
||||||
|
|
||||||
|
| Сценарий | Рекомендуемый алгоритм |
|
||||||
|
|----------|------------------------|
|
||||||
|
| Нужен **гарантированно кратчайший путь** и не важна скорость | BFS или A* (A* предпочтительнее) |
|
||||||
|
| **Скорость критична**, путь может быть неоптимальным | DFS |
|
||||||
|
| Нужен **компромисс** (оптимальность + мало посещений) | A* |
|
||||||
|
| Проверка **существования пути** (без восстановления маршрута) | DFS |
|
||||||
|
|
||||||
|
## 6. Заключение
|
||||||
|
|
||||||
|
Применение паттернов проектирования (Builder, Strategy, Observer, Command) позволило создать гибкую, расширяемую и легко тестируемую программу. Реализованные алгоритмы поиска пути были экспериментально сравнены на лабиринтах разной сложности. Полученные результаты подтверждают теоретические оценки: A* даёт наилучшее сочетание оптимальности и эффективности по числу посещённых клеток, DFS выигрывает по скорости, а BFS остаётся простым базовым решением.
|
||||||
|
|
||||||
|
Программа также предоставляет интерактивный режим с ручным управлением игроком и возможностью отмены ходов.
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user