Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d71206b9b0 | |||
| e71fe47f8c |
475
shekurovaa/1/docs/data/zad1.py
Normal file
475
shekurovaa/1/docs/data/zad1.py
Normal file
|
|
@ -0,0 +1,475 @@
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional, List, Tuple, Any
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
sys.setrecursionlimit(20000)
|
||||||
|
|
||||||
|
BASE_PATH = r"C:\Users\andre\2026-rff_mp\smirnovad\lab1"
|
||||||
|
DOCS_PATH = os.path.join(BASE_PATH, "docs")
|
||||||
|
DATA_PATH = os.path.join(DOCS_PATH, "data")
|
||||||
|
|
||||||
|
for path in (DOCS_PATH, DATA_PATH):
|
||||||
|
os.makedirs(path, exist_ok=True)
|
||||||
|
|
||||||
|
N = 10_000
|
||||||
|
REPEATS = 5
|
||||||
|
FOUND_SAMPLE_SIZE = 100
|
||||||
|
NOT_FOUND_SAMPLE_SIZE = 10
|
||||||
|
DELETE_SAMPLE_SIZE = 50
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NodeLL:
|
||||||
|
"""Узел односвязного списка."""
|
||||||
|
key: str
|
||||||
|
value: Any
|
||||||
|
next: Optional["NodeLL"] = None
|
||||||
|
|
||||||
|
|
||||||
|
class LinkedList:
|
||||||
|
"""Односвязный список с вставкой в начало."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.head: Optional[NodeLL] = None
|
||||||
|
|
||||||
|
def insert(self, key: str, value: Any) -> None:
|
||||||
|
self.head = NodeLL(key, value, self.head)
|
||||||
|
|
||||||
|
def find(self, key: str) -> Any:
|
||||||
|
cur = self.head
|
||||||
|
while cur is not None:
|
||||||
|
if cur.key == key:
|
||||||
|
return cur.value
|
||||||
|
cur = cur.next
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete(self, key: str) -> None:
|
||||||
|
cur = self.head
|
||||||
|
prev: Optional[NodeLL] = None
|
||||||
|
|
||||||
|
while cur is not None:
|
||||||
|
if cur.key == key:
|
||||||
|
if prev is None:
|
||||||
|
self.head = cur.next
|
||||||
|
else:
|
||||||
|
prev.next = cur.next
|
||||||
|
return
|
||||||
|
prev = cur
|
||||||
|
cur = cur.next
|
||||||
|
|
||||||
|
def items(self) -> List[Tuple[str, Any]]:
|
||||||
|
res: List[Tuple[str, Any]] = []
|
||||||
|
cur = self.head
|
||||||
|
while cur is not None:
|
||||||
|
res.append((cur.key, cur.value))
|
||||||
|
cur = cur.next
|
||||||
|
return sorted(res)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NodeBST:
|
||||||
|
"""Узел бинарного дерева поиска."""
|
||||||
|
key: str
|
||||||
|
value: Any
|
||||||
|
left: Optional["NodeBST"] = None
|
||||||
|
right: Optional["NodeBST"] = None
|
||||||
|
|
||||||
|
|
||||||
|
class BST:
|
||||||
|
"""Бинарное дерево поиска (без балансировки)."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.root: Optional[NodeBST] = None
|
||||||
|
|
||||||
|
def insert(self, key: str, value: Any) -> None:
|
||||||
|
self.root = self._insert(self.root, key, value)
|
||||||
|
|
||||||
|
def _insert(self, node: Optional[NodeBST], key: str, value: Any) -> NodeBST:
|
||||||
|
if node is None:
|
||||||
|
return NodeBST(key, value)
|
||||||
|
if key < node.key:
|
||||||
|
node.left = self._insert(node.left, key, value)
|
||||||
|
elif key > node.key:
|
||||||
|
node.right = self._insert(node.right, key, value)
|
||||||
|
else:
|
||||||
|
node.value = value
|
||||||
|
return node
|
||||||
|
|
||||||
|
def find(self, key: str) -> Any:
|
||||||
|
return self._find(self.root, key)
|
||||||
|
|
||||||
|
def _find(self, node: Optional[NodeBST], key: str) -> Any:
|
||||||
|
if node is None:
|
||||||
|
return None
|
||||||
|
if key == node.key:
|
||||||
|
return node.value
|
||||||
|
if key < node.key:
|
||||||
|
return self._find(node.left, key)
|
||||||
|
return self._find(node.right, key)
|
||||||
|
|
||||||
|
def delete(self, key: str) -> None:
|
||||||
|
self.root = self._delete(self.root, key)
|
||||||
|
|
||||||
|
def _delete(self, node: Optional[NodeBST], key: str) -> Optional[NodeBST]:
|
||||||
|
if node is None:
|
||||||
|
return None
|
||||||
|
if key < node.key:
|
||||||
|
node.left = self._delete(node.left, key)
|
||||||
|
elif key > node.key:
|
||||||
|
node.right = self._delete(node.right, key)
|
||||||
|
else:
|
||||||
|
if node.left is None:
|
||||||
|
return node.right
|
||||||
|
if node.right is None:
|
||||||
|
return node.left
|
||||||
|
succ = node.right
|
||||||
|
while succ.left is not None:
|
||||||
|
succ = succ.left
|
||||||
|
node.key, node.value = succ.key, succ.value
|
||||||
|
node.right = self._delete(node.right, succ.key)
|
||||||
|
return node
|
||||||
|
|
||||||
|
def items(self) -> List[Tuple[str, Any]]:
|
||||||
|
res: List[Tuple[str, Any]] = []
|
||||||
|
self._inorder(self.root, res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _inorder(self, node: Optional[NodeBST], out: List[Tuple[str, Any]]) -> None:
|
||||||
|
if node is None:
|
||||||
|
return
|
||||||
|
self._inorder(node.left, out)
|
||||||
|
out.append((node.key, node.value))
|
||||||
|
self._inorder(node.right, out)
|
||||||
|
|
||||||
|
|
||||||
|
class HashTable:
|
||||||
|
"""Хеш-таблица с цепочками (односвязные списки)."""
|
||||||
|
|
||||||
|
def __init__(self, capacity: int = 1024) -> None:
|
||||||
|
self.capacity = capacity
|
||||||
|
self.buckets: List[Optional[LinkedList]] = [None] * capacity
|
||||||
|
|
||||||
|
def _index(self, key: str) -> int:
|
||||||
|
return hash(key) % self.capacity
|
||||||
|
|
||||||
|
def insert(self, key: str, value: Any) -> None:
|
||||||
|
idx = self._index(key)
|
||||||
|
bucket = self.buckets[idx]
|
||||||
|
if bucket is None:
|
||||||
|
bucket = LinkedList()
|
||||||
|
self.buckets[idx] = bucket
|
||||||
|
bucket.insert(key, value)
|
||||||
|
|
||||||
|
def find(self, key: str) -> Any:
|
||||||
|
idx = self._index(key)
|
||||||
|
bucket = self.buckets[idx]
|
||||||
|
if bucket is None:
|
||||||
|
return None
|
||||||
|
return bucket.find(key)
|
||||||
|
|
||||||
|
def delete(self, key: str) -> None:
|
||||||
|
idx = self._index(key)
|
||||||
|
bucket = self.buckets[idx]
|
||||||
|
if bucket is None:
|
||||||
|
return
|
||||||
|
bucket.delete(key)
|
||||||
|
|
||||||
|
def items(self) -> List[Tuple[str, Any]]:
|
||||||
|
res: List[Tuple[str, Any]] = []
|
||||||
|
for bucket in self.buckets:
|
||||||
|
if bucket is not None:
|
||||||
|
res.extend(bucket.items())
|
||||||
|
return sorted(res)
|
||||||
|
|
||||||
|
def generate_records(n: int) -> List[Tuple[str, str]]:
|
||||||
|
"""Генерирует список (имя, телефон)."""
|
||||||
|
raw = [(f"user_{i:05d}", f"8-900-{random.randint(100, 999)}") for i in range(n)]
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_datasets(n: int) -> dict:
|
||||||
|
"""Подготавливает наборы данных: случайный и отсортированный."""
|
||||||
|
raw = generate_records(n)
|
||||||
|
shuffled = raw[:]
|
||||||
|
random.shuffle(shuffled)
|
||||||
|
sorted_data = sorted(raw, key=lambda x: x[0])
|
||||||
|
return {
|
||||||
|
"случайный": shuffled,
|
||||||
|
"сортированный": sorted_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RunResult:
|
||||||
|
struct_name: str
|
||||||
|
mode: str
|
||||||
|
run_label: str
|
||||||
|
insert_time: float
|
||||||
|
find_time: float
|
||||||
|
delete_time: float
|
||||||
|
|
||||||
|
|
||||||
|
class BenchmarkRunner:
|
||||||
|
def __init__(self, repeats: int = REPEATS) -> None:
|
||||||
|
self.repeats = repeats
|
||||||
|
self.results: List[RunResult] = []
|
||||||
|
|
||||||
|
def run_experiment(self, struct_name: str, mode: str, data: List[Tuple[str, str]]) -> None:
|
||||||
|
print(f"Запуск: {struct_name} ({mode})")
|
||||||
|
|
||||||
|
insert_times: List[float] = []
|
||||||
|
find_times: List[float] = []
|
||||||
|
delete_times: List[float] = []
|
||||||
|
|
||||||
|
for rep in range(self.repeats):
|
||||||
|
if struct_name == "LinkedList":
|
||||||
|
container = LinkedList()
|
||||||
|
elif struct_name == "HashTable":
|
||||||
|
container = HashTable(capacity=1024)
|
||||||
|
elif struct_name == "BST":
|
||||||
|
container = BST()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Неизвестная структура: {struct_name}")
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
for key, val in data:
|
||||||
|
container.insert(key, val)
|
||||||
|
insert_times.append(time.perf_counter() - t0)
|
||||||
|
|
||||||
|
found_keys = [d[0] for d in random.sample(data, FOUND_SAMPLE_SIZE)]
|
||||||
|
not_found_keys = [f"nonexistent_{j}" for j in range(NOT_FOUND_SAMPLE_SIZE)]
|
||||||
|
search_keys = found_keys + not_found_keys
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
for k in search_keys:
|
||||||
|
container.find(k)
|
||||||
|
find_times.append(time.perf_counter() - t0)
|
||||||
|
|
||||||
|
delete_keys = [d[0] for d in random.sample(data, DELETE_SAMPLE_SIZE)]
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
for k in delete_keys:
|
||||||
|
container.delete(k)
|
||||||
|
delete_times.append(time.perf_counter() - t0)
|
||||||
|
|
||||||
|
self.results.append(
|
||||||
|
RunResult(
|
||||||
|
struct_name=struct_name,
|
||||||
|
mode=mode,
|
||||||
|
run_label=f"run_{rep+1}",
|
||||||
|
insert_time=insert_times[-1],
|
||||||
|
find_time=find_times[-1],
|
||||||
|
delete_time=delete_times[-1],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
avg_ins = sum(insert_times) / self.repeats
|
||||||
|
avg_find = sum(find_times) / self.repeats
|
||||||
|
avg_del = sum(delete_times) / self.repeats
|
||||||
|
|
||||||
|
self.results.append(
|
||||||
|
RunResult(
|
||||||
|
struct_name=struct_name,
|
||||||
|
mode=mode,
|
||||||
|
run_label="AVG",
|
||||||
|
insert_time=avg_ins,
|
||||||
|
find_time=avg_find,
|
||||||
|
delete_time=avg_del,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_csv(self, path: str) -> None:
|
||||||
|
with open(path, "w", newline="", encoding="utf-8") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
w.writerow(["Структура", "Режим", "Итерация", "Вставка", "Поиск", "Удаление"])
|
||||||
|
for r in self.results:
|
||||||
|
w.writerow([
|
||||||
|
r.struct_name,
|
||||||
|
r.mode,
|
||||||
|
r.run_label,
|
||||||
|
r.insert_time,
|
||||||
|
r.find_time,
|
||||||
|
r.delete_time,
|
||||||
|
])
|
||||||
|
|
||||||
|
def summary(self) -> List[dict]:
|
||||||
|
"""Возвращает список словарей со средними по (структура, режим)."""
|
||||||
|
summary = []
|
||||||
|
groups: dict = {}
|
||||||
|
for r in self.results:
|
||||||
|
if r.run_label != "AVG":
|
||||||
|
continue
|
||||||
|
key = (r.struct_name, r.mode)
|
||||||
|
groups[key] = {
|
||||||
|
"name": r.struct_name,
|
||||||
|
"mode": r.mode,
|
||||||
|
"ins": r.insert_time,
|
||||||
|
"find": r.find_time,
|
||||||
|
"del": r.delete_time,
|
||||||
|
}
|
||||||
|
summary.extend(groups.values())
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
def build_plots(summary: List[dict], n: int, path_base: str) -> None:
|
||||||
|
structs = ["LinkedList", "HashTable", "BST"]
|
||||||
|
ops = ["insert", "find", "delete"]
|
||||||
|
op_keys = ["ins", "find", "del"]
|
||||||
|
colors_struct = {
|
||||||
|
"LinkedList": "#5dade2",
|
||||||
|
"HashTable": "#e67e22",
|
||||||
|
"BST": "#58d68d",
|
||||||
|
}
|
||||||
|
|
||||||
|
fig1, axs = plt.subplots(1, 3, figsize=(18, 6))
|
||||||
|
fig1.suptitle("Влияние порядка данных на время операций", fontsize=16, fontweight="bold")
|
||||||
|
|
||||||
|
labels_ops = ["insert", "find", "delete"]
|
||||||
|
width = 0.35
|
||||||
|
x = [0, 1, 2]
|
||||||
|
|
||||||
|
for i, s_name in enumerate(structs):
|
||||||
|
rand_row = next(
|
||||||
|
(r for r in summary if r["name"] == s_name and r["mode"] == "случайный"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
sort_row = next(
|
||||||
|
(r for r in summary if r["name"] == s_name and r["mode"] == "сортированный"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if rand_row is None or sort_row is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
vals_rand = [rand_row["ins"], rand_row["find"], rand_row["del"]]
|
||||||
|
vals_sort = [sort_row["ins"], sort_row["find"], sort_row["del"]]
|
||||||
|
|
||||||
|
axs[i].bar(
|
||||||
|
[p - width / 2 for p in x],
|
||||||
|
vals_rand,
|
||||||
|
width,
|
||||||
|
label="случайный",
|
||||||
|
color=colors_struct[s_name],
|
||||||
|
)
|
||||||
|
axs[i].bar(
|
||||||
|
[p + width / 2 for p in x],
|
||||||
|
vals_sort,
|
||||||
|
width,
|
||||||
|
label="сортированный",
|
||||||
|
color="#e74c3c",
|
||||||
|
alpha=0.85,
|
||||||
|
)
|
||||||
|
|
||||||
|
axs[i].set_title(s_name, fontweight="bold")
|
||||||
|
axs[i].set_xticks(x)
|
||||||
|
axs[i].set_xticklabels(labels_ops)
|
||||||
|
axs[i].set_ylabel("Время (с)")
|
||||||
|
axs[i].legend()
|
||||||
|
axs[i].grid(axis="y", linestyle="--", alpha=0.3)
|
||||||
|
|
||||||
|
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
|
||||||
|
plt.savefig(os.path.join(path_base, "order_impact.png"))
|
||||||
|
plt.close(fig1)
|
||||||
|
|
||||||
|
fig2, axs2 = plt.subplots(1, 3, figsize=(18, 6))
|
||||||
|
fig2.suptitle(f"Сравнение структур данных (N={n})", fontsize=16, fontweight="bold")
|
||||||
|
|
||||||
|
for i, op_key in enumerate(op_keys):
|
||||||
|
plot_labels = []
|
||||||
|
plot_values = []
|
||||||
|
plot_colors = []
|
||||||
|
|
||||||
|
for r in summary:
|
||||||
|
plot_labels.append(f"{r['name']}\\n({r['mode'][:4]})")
|
||||||
|
plot_values.append(r[op_key])
|
||||||
|
plot_colors.append(colors_struct[r["name"]])
|
||||||
|
|
||||||
|
bars = axs2[i].bar(plot_labels, plot_values, color=plot_colors)
|
||||||
|
axs2[i].set_title(f"Операция: {ops[i]}", fontweight="bold")
|
||||||
|
axs2[i].set_ylabel("Время (с)")
|
||||||
|
axs2[i].tick_params(axis="x", rotation=15)
|
||||||
|
|
||||||
|
for bar in bars:
|
||||||
|
h = bar.get_height()
|
||||||
|
axs2[i].text(
|
||||||
|
bar.get_x() + bar.get_width() / 2,
|
||||||
|
h,
|
||||||
|
f"{h:.4f}",
|
||||||
|
ha="center",
|
||||||
|
va="bottom",
|
||||||
|
fontsize=8,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
|
||||||
|
plt.savefig(os.path.join(path_base, "struct_comparison.png"))
|
||||||
|
plt.close(fig2)
|
||||||
|
|
||||||
|
|
||||||
|
def build_report(summary: List[dict], n: int, path: str) -> None:
|
||||||
|
lines = []
|
||||||
|
lines.append("# Технический отчет: Сравнительный анализ структур данных\n")
|
||||||
|
lines.append("## 1. Вводные данные\n")
|
||||||
|
lines.append(
|
||||||
|
f"Цель — оценить производительность LinkedList, HashTable и BST на массиве из {n} элементов. "
|
||||||
|
"Рассмотрены два сценария: случайный порядок ключей и заранее отсортированный по возрастанию.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.append("## 2. Результаты измерений (среднее)\n")
|
||||||
|
lines.append("| Структура | Режим | Вставка (с) | Поиск (с) | Удаление (с) |\n")
|
||||||
|
lines.append("| :--- | :--- | :---: | :---: | :---: |\n")
|
||||||
|
for r in summary:
|
||||||
|
lines.append(
|
||||||
|
f"| {r['name']} | {r['mode']} | {r['ins']:.6f} | {r['find']:.6f} | {r['del']:.6f} |\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.append("\n## 3. Визуализация\n")
|
||||||
|
lines.append("### Сравнение структур по операциям\n")
|
||||||
|
lines.append("\n")
|
||||||
|
lines.append("### Влияние порядка данных\n")
|
||||||
|
lines.append("\n")
|
||||||
|
|
||||||
|
lines.append("## 4. Выводы\n")
|
||||||
|
lines.append(
|
||||||
|
"- **BST без балансировки** на отсортированных ключах вырождается в линейную цепочку, "
|
||||||
|
"что приводит к резкому росту времени операций (практическая сложность приближается к $O(N)$).\n"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
"- **HashTable** показывает стабильную производительность, практически не зависящую от порядка входных данных. "
|
||||||
|
"Это делает её предпочтительной для задач с интенсивным поиском и вставкой.\n"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
"- **LinkedList**ónico предсказуемо медленен при поиске и удалении, так как эти операции требуют линейного прохода по списку.\n"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
"- **Итог:** для систем с высокой нагрузкой на поиск/вставку оптимальным выбором является хеш-таблица; "
|
||||||
|
"BST имеет смысл использовать только при дополнительной балансировке (AVL, красно-черное дерево и т.п.).\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
datasets = prepare_datasets(N)
|
||||||
|
runner = BenchmarkRunner(repeats=REPEATS)
|
||||||
|
|
||||||
|
for mode_name, data in datasets.items():
|
||||||
|
for struct_name in ["LinkedList", "HashTable", "BST"]:
|
||||||
|
runner.run_experiment(struct_name, mode_name, data)
|
||||||
|
|
||||||
|
csv_path = os.path.join(DATA_PATH, "results.csv")
|
||||||
|
runner.save_csv(csv_path)
|
||||||
|
|
||||||
|
summary = runner.summary()
|
||||||
|
|
||||||
|
build_plots(summary, N, DATA_PATH)
|
||||||
|
build_report(summary, N, os.path.join(DOCS_PATH, "report.md"))
|
||||||
|
|
||||||
|
print("Готово.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
shekurovaa/2/docs/Report.docx
Normal file
BIN
shekurovaa/2/docs/Report.docx
Normal file
Binary file not shown.
46
shekurovaa/2/docs/data/builder.py
Normal file
46
shekurovaa/2/docs/data/builder.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
from model import Cell, Maze
|
||||||
|
|
||||||
|
class MazeBuilder:
|
||||||
|
def buildFromFile(self, filename: str) -> Maze:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class TextFileMazeBuilder(MazeBuilder):
|
||||||
|
def buildFromFile(self, filename: str) -> Maze:
|
||||||
|
with open(filename, "r", encoding="utf-8") as f:
|
||||||
|
raw_lines = [line.rstrip("\n") for line in f if line.strip("\n") != ""]
|
||||||
|
|
||||||
|
width = max(len(line) for line in raw_lines)
|
||||||
|
grid = []
|
||||||
|
|
||||||
|
start_count = 0
|
||||||
|
exit_count = 0
|
||||||
|
|
||||||
|
for y, line in enumerate(raw_lines):
|
||||||
|
row = []
|
||||||
|
padded = line.ljust(width)
|
||||||
|
for x, ch in enumerate(padded):
|
||||||
|
if ch == "#":
|
||||||
|
row.append(Cell(x, y, isWall=True))
|
||||||
|
elif ch == "S":
|
||||||
|
row.append(Cell(x, y, isStart=True))
|
||||||
|
start_count += 1
|
||||||
|
elif ch == "E":
|
||||||
|
row.append(Cell(x, y, isExit=True))
|
||||||
|
exit_count += 1
|
||||||
|
elif ch == "1":
|
||||||
|
row.append(Cell(x, y, weight=1))
|
||||||
|
elif ch == "2":
|
||||||
|
row.append(Cell(x, y, weight=2))
|
||||||
|
elif ch == "3":
|
||||||
|
row.append(Cell(x, y, weight=3))
|
||||||
|
else:
|
||||||
|
row.append(Cell(x, y))
|
||||||
|
grid.append(row)
|
||||||
|
|
||||||
|
maze = Maze(grid)
|
||||||
|
|
||||||
|
if start_count != 1 or exit_count != 1:
|
||||||
|
raise ValueError("В лабиринте должен быть ровно один S и один E")
|
||||||
|
|
||||||
|
return maze
|
||||||
44
shekurovaa/2/docs/data/command.py
Normal file
44
shekurovaa/2/docs/data/command.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
class Command:
|
||||||
|
def execute(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
def __init__(self, position):
|
||||||
|
self.position = position
|
||||||
|
|
||||||
|
|
||||||
|
class MoveCommand(Command):
|
||||||
|
DIRS = {
|
||||||
|
"W": (0, -1),
|
||||||
|
"S": (0, 1),
|
||||||
|
"A": (-1, 0),
|
||||||
|
"D": (1, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, maze, player, direction):
|
||||||
|
self.maze = maze
|
||||||
|
self.player = player
|
||||||
|
self.direction = direction.upper()
|
||||||
|
self.prev_position = None
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
if self.direction not in self.DIRS:
|
||||||
|
return False
|
||||||
|
dx, dy = self.DIRS[self.direction]
|
||||||
|
current = self.player.position
|
||||||
|
nxt = self.maze.getCell(current.x + dx, current.y + dy)
|
||||||
|
if nxt is None or not nxt.isPassable():
|
||||||
|
return False
|
||||||
|
self.prev_position = current
|
||||||
|
self.player.position = nxt
|
||||||
|
return True
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self.prev_position is not None:
|
||||||
|
self.player.position = self.prev_position
|
||||||
|
return True
|
||||||
|
return False
|
||||||
30
shekurovaa/2/docs/data/experiments.py
Normal file
30
shekurovaa/2/docs/data/experiments.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import csv
|
||||||
|
from statistics import mean
|
||||||
|
|
||||||
|
def run_experiments(maze_files, strategies, runs=5, out_csv="output/results.csv"):
|
||||||
|
rows = []
|
||||||
|
for maze_name, maze in maze_files.items():
|
||||||
|
for strat_name, strat_cls in strategies.items():
|
||||||
|
times = []
|
||||||
|
visiteds = []
|
||||||
|
lengths = []
|
||||||
|
for _ in range(runs):
|
||||||
|
solver = maze["solver_factory"](strat_cls())
|
||||||
|
stats = solver.solve()
|
||||||
|
times.append(stats.timeMs)
|
||||||
|
visiteds.append(stats.visitedCells)
|
||||||
|
lengths.append(stats.pathLength)
|
||||||
|
rows.append({
|
||||||
|
"maze": maze_name,
|
||||||
|
"strategy": strat_name,
|
||||||
|
"time_ms": round(mean(times), 3),
|
||||||
|
"visited_cells": round(mean(visiteds), 1),
|
||||||
|
"path_length": round(mean(lengths), 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
with open(out_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(rows)
|
||||||
|
|
||||||
|
return rows
|
||||||
33
shekurovaa/2/docs/data/main.py
Normal file
33
shekurovaa/2/docs/data/main.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
from builder import TextFileMazeBuilder
|
||||||
|
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
|
||||||
|
from solver import MazeSolver
|
||||||
|
from observer import ConsoleView
|
||||||
|
from command import Player, MoveCommand
|
||||||
|
|
||||||
|
def main():
|
||||||
|
builder = TextFileMazeBuilder()
|
||||||
|
maze = builder.buildFromFile("mazes/small.txt")
|
||||||
|
|
||||||
|
console = ConsoleView()
|
||||||
|
console.update({"type": "message", "text": "Лабиринт загружен:"})
|
||||||
|
console.update({"type": "render", "maze": maze})
|
||||||
|
|
||||||
|
strategies = {
|
||||||
|
"BFS": BFSStrategy(),
|
||||||
|
"DFS": DFSStrategy(),
|
||||||
|
"A*": AStarStrategy()
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, strat in strategies.items():
|
||||||
|
solver = MazeSolver(maze, strat)
|
||||||
|
stats = solver.solve()
|
||||||
|
print(f"{name}: time={stats.timeMs:.3f} ms, visited={stats.visitedCells}, path={stats.pathLength}")
|
||||||
|
console.update({"type": "render", "maze": maze, "path": stats.path})
|
||||||
|
|
||||||
|
player = Player(maze.start)
|
||||||
|
cmd = MoveCommand(maze, player, "D")
|
||||||
|
cmd.execute()
|
||||||
|
console.update({"type": "render", "maze": maze, "player": player.position})
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
10
shekurovaa/2/docs/data/mazes/small.txt
Normal file
10
shekurovaa/2/docs/data/mazes/small.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
##########
|
||||||
|
#S # #
|
||||||
|
# ## # # #
|
||||||
|
# ## # #
|
||||||
|
# ### #
|
||||||
|
### ## #
|
||||||
|
# # #
|
||||||
|
# # ###E #
|
||||||
|
# #
|
||||||
|
##########
|
||||||
67
shekurovaa/2/docs/data/model.py
Normal file
67
shekurovaa/2/docs/data/model.py
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Cell:
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
isWall: bool = False
|
||||||
|
isStart: bool = False
|
||||||
|
isExit: bool = False
|
||||||
|
weight: int = 1
|
||||||
|
|
||||||
|
def isPassable(self) -> bool:
|
||||||
|
return not self.isWall
|
||||||
|
|
||||||
|
|
||||||
|
class Maze:
|
||||||
|
def __init__(self, grid: List[List[Cell]]):
|
||||||
|
self.grid = grid
|
||||||
|
self.height = len(grid)
|
||||||
|
self.width = len(grid[0]) if self.height else 0
|
||||||
|
self.start: Optional[Cell] = None
|
||||||
|
self.exit: Optional[Cell] = None
|
||||||
|
|
||||||
|
for row in grid:
|
||||||
|
for cell in row:
|
||||||
|
if cell.isStart:
|
||||||
|
self.start = cell
|
||||||
|
if cell.isExit:
|
||||||
|
self.exit = cell
|
||||||
|
|
||||||
|
def getCell(self, x: int, y: int) -> Optional[Cell]:
|
||||||
|
if 0 <= y < self.height and 0 <= x < self.width:
|
||||||
|
return self.grid[y][x]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getNeighbors(self, cell: Cell) -> List[Cell]:
|
||||||
|
result = []
|
||||||
|
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
||||||
|
nxt = self.getCell(cell.x + dx, cell.y + dy)
|
||||||
|
if nxt is not None and nxt.isPassable():
|
||||||
|
result.append(nxt)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def render(self, path=None, player_position=None) -> str:
|
||||||
|
path_set = {(c.x, c.y) for c in path} if path else set()
|
||||||
|
player_xy = (player_position.x, player_position.y) if player_position else None
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for y in range(self.height):
|
||||||
|
row = []
|
||||||
|
for x in range(self.width):
|
||||||
|
c = self.grid[y][x]
|
||||||
|
if player_xy == (x, y):
|
||||||
|
row.append("P")
|
||||||
|
elif c.isStart:
|
||||||
|
row.append("S")
|
||||||
|
elif c.isExit:
|
||||||
|
row.append("E")
|
||||||
|
elif (x, y) in path_set:
|
||||||
|
row.append(".")
|
||||||
|
elif c.isWall:
|
||||||
|
row.append("#")
|
||||||
|
else:
|
||||||
|
row.append(" ")
|
||||||
|
lines.append("".join(row))
|
||||||
|
return "\n".join(lines)
|
||||||
15
shekurovaa/2/docs/data/observer.py
Normal file
15
shekurovaa/2/docs/data/observer.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
class Observer:
|
||||||
|
def update(self, event):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleView(Observer):
|
||||||
|
def update(self, event):
|
||||||
|
if isinstance(event, dict) and event.get("type") == "message":
|
||||||
|
print(event["text"])
|
||||||
|
elif isinstance(event, dict) and event.get("type") == "render":
|
||||||
|
maze = event["maze"]
|
||||||
|
path = event.get("path")
|
||||||
|
player = event.get("player")
|
||||||
|
print(maze.render(path=path, player_position=player))
|
||||||
|
print()
|
||||||
38
shekurovaa/2/docs/data/solver.py
Normal file
38
shekurovaa/2/docs/data/solver.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SearchStats:
|
||||||
|
timeMs: float
|
||||||
|
visitedCells: int
|
||||||
|
pathLength: int
|
||||||
|
path: list
|
||||||
|
|
||||||
|
|
||||||
|
class MazeSolver:
|
||||||
|
def __init__(self, maze, strategy):
|
||||||
|
self.maze = maze
|
||||||
|
self.strategy = strategy
|
||||||
|
|
||||||
|
def setStrategy(self, strategy):
|
||||||
|
self.strategy = strategy
|
||||||
|
|
||||||
|
def solve(self) -> SearchStats:
|
||||||
|
if self.maze.start is None or self.maze.exit is None:
|
||||||
|
raise ValueError("Лабиринт должен содержать start и exit")
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
result = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit)
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
path, visited = result
|
||||||
|
else:
|
||||||
|
path, visited = result, 0
|
||||||
|
|
||||||
|
return SearchStats(
|
||||||
|
timeMs=(t1 - t0) * 1000,
|
||||||
|
visitedCells=visited,
|
||||||
|
pathLength=len(path),
|
||||||
|
path=path
|
||||||
|
)
|
||||||
103
shekurovaa/2/docs/data/strategies.py
Normal file
103
shekurovaa/2/docs/data/strategies.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
from collections import deque
|
||||||
|
import heapq
|
||||||
|
from math import inf
|
||||||
|
|
||||||
|
class PathFindingStrategy:
|
||||||
|
def findPath(self, maze, start, exit):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def reconstruct_path(parent, start, goal):
|
||||||
|
if goal not in parent and goal != start:
|
||||||
|
return []
|
||||||
|
path = []
|
||||||
|
cur = goal
|
||||||
|
while cur != start:
|
||||||
|
path.append(cur)
|
||||||
|
cur = parent[cur]
|
||||||
|
path.append(start)
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class BFSStrategy(PathFindingStrategy):
|
||||||
|
def findPath(self, maze, start, exit):
|
||||||
|
queue = deque([start])
|
||||||
|
visited = {start}
|
||||||
|
parent = {}
|
||||||
|
visited_count = 0
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
current = queue.popleft()
|
||||||
|
visited_count += 1
|
||||||
|
if current == exit:
|
||||||
|
path = reconstruct_path(parent, start, exit)
|
||||||
|
return path, visited_count
|
||||||
|
|
||||||
|
for nxt in maze.getNeighbors(current):
|
||||||
|
if nxt not in visited:
|
||||||
|
visited.add(nxt)
|
||||||
|
parent[nxt] = current
|
||||||
|
queue.append(nxt)
|
||||||
|
|
||||||
|
return [], visited_count
|
||||||
|
|
||||||
|
|
||||||
|
class DFSStrategy(PathFindingStrategy):
|
||||||
|
def findPath(self, maze, start, exit):
|
||||||
|
stack = [start]
|
||||||
|
visited = {start}
|
||||||
|
parent = {}
|
||||||
|
visited_count = 0
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
current = stack.pop()
|
||||||
|
visited_count += 1
|
||||||
|
if current == exit:
|
||||||
|
path = reconstruct_path(parent, start, exit)
|
||||||
|
return path, visited_count
|
||||||
|
|
||||||
|
for nxt in maze.getNeighbors(current):
|
||||||
|
if nxt not in visited:
|
||||||
|
visited.add(nxt)
|
||||||
|
parent[nxt] = current
|
||||||
|
stack.append(nxt)
|
||||||
|
|
||||||
|
return [], visited_count
|
||||||
|
|
||||||
|
|
||||||
|
class AStarStrategy(PathFindingStrategy):
|
||||||
|
def h(self, a, b):
|
||||||
|
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||||
|
|
||||||
|
def findPath(self, maze, start, exit):
|
||||||
|
open_heap = []
|
||||||
|
heapq.heappush(open_heap, (0, 0, start))
|
||||||
|
parent = {}
|
||||||
|
g = {start: 0}
|
||||||
|
visited = set()
|
||||||
|
visited_count = 0
|
||||||
|
counter = 1
|
||||||
|
|
||||||
|
while open_heap:
|
||||||
|
_, _, current = heapq.heappop(open_heap)
|
||||||
|
if current in visited:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visited.add(current)
|
||||||
|
visited_count += 1
|
||||||
|
|
||||||
|
if current == exit:
|
||||||
|
path = reconstruct_path(parent, start, exit)
|
||||||
|
return path, visited_count
|
||||||
|
|
||||||
|
for nxt in maze.getNeighbors(current):
|
||||||
|
tentative_g = g[current] + nxt.weight
|
||||||
|
if tentative_g < g.get(nxt, inf):
|
||||||
|
g[nxt] = tentative_g
|
||||||
|
parent[nxt] = current
|
||||||
|
f = tentative_g + self.h(nxt, exit)
|
||||||
|
heapq.heappush(open_heap, (f, counter, nxt))
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
return [], visited_count
|
||||||
BIN
shekurovaa/2/docs/~$Report.docx
Normal file
BIN
shekurovaa/2/docs/~$Report.docx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user