This commit is contained in:
2025-11-04 15:02:02 +03:00
parent 83be98e923
commit 8e8e0abd0d
6 changed files with 657 additions and 1 deletions

192
lab4/gp/crossovers.py Normal file
View File

@@ -0,0 +1,192 @@
import random
from typing import Optional
from .chromosome import Chromosome
from .operation import Operation
from .terminal import Terminal
def crossover_subtree(
p1: Chromosome, p2: Chromosome, max_depth: int | None = None
) -> tuple[Chromosome, Chromosome]:
"""
Кроссовер поддеревьев: выбираются случайные узлы в каждом родителе,
затем соответствующие поддеревья обмениваются местами. Возвращаются два новых потомка.
Аргументы:
p1 : первый родитель (не изменяется).
p2 : второй родитель (не изменяется).
max_depth : максимальная допустимая глубина потомков. Если None, ограничение не применяется.
Примечания:
- Для «совместимости» узлов сперва пытаемся подобрать пары одного класса
(оба Terminal или оба Operation). Если подходящей пары не нашлось за
разумное число попыток — допускаем любой обмен.
- Если задан max_depth, проверяется глубина результирующих потомков,
и при превышении лимита выбор узлов повторяется.
- Обмен выполняется на КЛОНАХ родителей, чтобы не портить входные деревья.
"""
# -------- Вспомогательные функции --------
def enumerate_nodes_with_meta(
root: Chromosome.Node,
) -> list[
tuple[Chromosome.Node, Optional[Chromosome.Node], Optional[int], list[int], int]
]:
"""
Возвращает список кортежей: (node, parent, index_in_parent, path_from_root, depth_from_root)
path_from_root — список индексов детей от корня до узла.
depth_from_root: 1 для корня, 2 для детей корня и т.д.
"""
out: list[
tuple[
Chromosome.Node,
Optional[Chromosome.Node],
Optional[int],
list[int],
int,
]
] = []
def dfs(
node: Chromosome.Node,
parent: Optional[Chromosome.Node],
idx: Optional[int],
path: list[int],
depth: int,
) -> None:
out.append((node, parent, idx, path, depth))
for i, ch in enumerate(node.children):
dfs(ch, node, i, path + [i], depth + 1)
dfs(root, None, None, [], 1)
return out
def get_parent_and_index_by_path(
root: Chromosome.Node, path: list[int]
) -> tuple[Optional[Chromosome.Node], Optional[int], Chromosome.Node]:
"""
По пути возвращает (parent, index_in_parent, node).
Для корня parent/index равны None.
"""
if not path:
return None, None, root
parent = root
for i in path[:-1]:
parent = parent.children[i]
idx = path[-1]
return parent, idx, parent.children[idx]
def is_op(node: Chromosome.Node) -> bool:
return isinstance(node.value, Operation)
def is_term(node: Chromosome.Node) -> bool:
return isinstance(node.value, Terminal)
def check_depth_after_swap(
node_depth: int, new_subtree: Chromosome.Node, max_d: int
) -> bool:
"""
Проверяет, не превысит ли глубина дерева max_d после замены узла на глубине node_depth
на поддерево new_subtree.
node_depth: глубина узла, который заменяем (1 для корня)
new_subtree: поддерево, которое вставляем
max_d: максимальная допустимая глубина
Возвращает True, если глубина будет в пределах нормы.
"""
# Глубина нового поддерева
subtree_depth = new_subtree.get_depth()
# Итоговая глубина = глубина узла + глубина поддерева - 1
# (т.к. node_depth уже включает этот узел)
resulting_depth = node_depth + subtree_depth - 1
return resulting_depth <= max_d
# -------- Выбор доминантного/рецессивного родителя (просто случайно) --------
dom, rec = (p1, p2) if random.random() < 0.5 else (p2, p1)
# Собираем все узлы с метаданными
dom_nodes = enumerate_nodes_with_meta(dom.root)
rec_nodes = enumerate_nodes_with_meta(rec.root)
# Пытаемся выбрать совместимые узлы: оба термины ИЛИ оба операции.
# Дадим несколько попыток, затем, если не повезло — возьмём любые.
MAX_TRIES = 64
chosen_dom = None
chosen_rec = None
for _ in range(MAX_TRIES):
nd = random.choice(dom_nodes)
# Предпочтём узел того же «класса»
if is_term(nd[0]):
same_type_pool = [nr for nr in rec_nodes if is_term(nr[0])]
elif is_op(nd[0]):
same_type_pool = [nr for nr in rec_nodes if is_op(nr[0])]
else:
same_type_pool = rec_nodes # на всякий
if same_type_pool:
nr = random.choice(same_type_pool)
# Если задан max_depth, проверяем, что обмен не приведёт к превышению глубины
if max_depth is not None:
nd_node, _, _, nd_path, nd_depth = nd
nr_node, _, _, nr_path, nr_depth = nr
# Проверяем обе возможные замены
dom_ok = check_depth_after_swap(nd_depth, nr_node, max_depth)
rec_ok = check_depth_after_swap(nr_depth, nd_node, max_depth)
if dom_ok and rec_ok:
chosen_dom, chosen_rec = nd, nr
break
# Иначе пробуем другую пару
else:
# Если ограничения нет, принимаем первую подходящую пару
chosen_dom, chosen_rec = nd, nr
break
# Если подобрать подходящую пару не удалось
if chosen_dom is None or chosen_rec is None:
# Возвращаем клоны родителей без изменений
return (p1.clone(), p2.clone())
_, _, _, dom_path, _ = chosen_dom
_, _, _, rec_path, _ = chosen_rec
# -------- Создаём клоны родителей --------
c_dom = dom.clone()
c_rec = rec.clone()
# Выцепляем соответствующие позиции на клонах по тем же путям
c_dom_parent, c_dom_idx, c_dom_node = get_parent_and_index_by_path(
c_dom.root, dom_path
)
c_rec_parent, c_rec_idx, c_rec_node = get_parent_and_index_by_path(
c_rec.root, rec_path
)
# Клонируем поддеревья, чтобы не смешивать ссылки между хромосомами
subtree_dom = c_dom_node.clone()
subtree_rec = c_rec_node.clone()
# Меняем местами
if c_dom_parent is None:
# Меняем корень
c_dom.root = subtree_rec
else:
c_dom_parent.children[c_dom_idx] = subtree_rec # type: ignore[index]
if c_rec_parent is None:
c_rec.root = subtree_dom
else:
c_rec_parent.children[c_rec_idx] = subtree_dom # type: ignore[index]
# Возвращаем потомков в том же порядке, что и вход (p1 -> first, p2 -> second)
if dom is p1:
return (c_dom, c_rec)
else:
return (c_rec, c_dom)