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)