К обычному разбору
Тренировка по собеседованиюТехническое собеседованиеBHFT2026-04-10

BHFT: Алгоритмическая секция

Идите сверху вниз: сначала попробуйте сами, затем откройте разбор. Если шаг с кодом, пишите решение прямо здесь и запускайте проверки на странице.

Шагов
10
Вопросов
8
Задач
2
1Вопрос10 мин

RL для моделирования молекул

Как сформулировать RL-задачу для оптимизации молекул и почему direct optimization может быть недостаточной?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Состояние - молекула/граф, действие - допустимая модификация, reward связан с целевым свойством и штрафами за невалидность. Direct optimization часто игнорирует constraints и trajectory.

Подробный разбор

RL-постановка задает состояние как текущую молекулу или ее графовое представление, действия как допустимые изменения структуры, а reward как комбинацию целевого свойства, валидности, synthesizability, токсичности и diversity. Среда проверяет, что шаг приводит к допустимой молекуле.

Прямая оптимизация одного score может находить артефакты: невалидные структуры, нереалистичные модификации или молекулы вне допустимого химического пространства. RL полезен, когда важны последовательные изменения, constraints на действия и trade-off между несколькими критериями.

2Вопрос10 мин

Почему RL в трейдинге опасен

Какие риски возникают при применении RL к trading/market-making задачам?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Главный риск - неверная среда. Агент оптимизирует симулятор, а не рынок, переобучается на микроструктурные артефакты и не учитывает impact, latency и regime shifts.

Подробный разбор

RL в трейдинге требует среды, которая реалистично моделирует order book, latency, fills, transaction costs, market impact, queue position и поведение других участников. Если симулятор неточен, агент учится эксплуатировать его слабости, а не зарабатывать на реальном рынке.

Дополнительные риски: non-stationarity, редкие стресс-сценарии, leakage через будущие данные, нестабильный reward и сложность offline evaluation. Поэтому RL-идею обычно сравнивают с supervised/bandit/baseline стратегиями, добавляют risk limits и тестируют на walk-forward slices с консервативным rollout.

3ЗадачаMedium

Обход бинарного дерева зигзагом

Условие

Дано бинарное дерево. Нужно вернуть значения узлов по уровням, но направление обхода чередуется:

  • первый уровень слева направо;
  • второй уровень справа налево;
  • третий снова слева направо.

Signature

zigzag_level_order(root: TreeNode | None) -> list[list[int]]

В проверках дерево строится из level-order tokens и передается в вашу функцию как TreeNode.

Решение прямо на странице

Напишите код, запустите проверки и только потом открывайте разбор.

Проверка решения

Нажмите «Запустить проверки» или Ctrl+Enter.

Показать разбор

Подсказки

  • Идите по уровням

    Используйте BFS и в начале каждого уровня сохраняйте текущий размер очереди.

  • Меняйте направление по уровню

    Соберите level, потом при необходимости разверните его.

Идея решения

Строим дерево из массива level-order, затем запускаем BFS по уровням.

На каждом уровне собираем значения слева направо. Если текущий уровень должен идти справа налево, разворачиваем список значений перед добавлением в ответ.

Ключевой момент: направление меняется после каждого уровня, а не после каждого узла.

Эталонный код

def zigzag_level_order(tokens: list) -> list[list[int]]:
    from collections import deque

    class TreeNode:
        def __init__(self, val=0, left=None, right=None):
            self.val = val
            self.left = left
            self.right = right

    def build_tree(values):
        if not values or values[0] is None:
            return None

        root = TreeNode(values[0])
        queue = deque([root])
        i = 1
        while queue and i < len(values):
            node = queue.popleft()
            if i < len(values) and values[i] is not None:
                node.left = TreeNode(values[i])
                queue.append(node.left)
            i += 1
            if i < len(values) and values[i] is not None:
                node.right = TreeNode(values[i])
                queue.append(node.right)
            i += 1
        return root

    root = build_tree(tokens)
    if not root:
        return []

    result = []
    queue = deque([root])
    left_to_right = True

    while queue:
        level_size = len(queue)
        level = []
        for _ in range(level_size):
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)

        if not left_to_right:
            level.reverse()
        result.append(level)
        left_to_right = not left_to_right

    return result
Сложность
Время: O(n). Память: O(n).
Каждый узел обрабатывается один раз. Очередь и результат занимают O(n).
4ЗадачаMedium

LRU Cache с операциями get/put

Условие

Реализуйте LRU cache с фиксированной вместимостью.

Кеш поддерживает:

  • get(key): вернуть значение или -1, если ключа нет;
  • put(key, value): положить или обновить значение.

После get и put ключ считается recently used. Если capacity превышена, удаляется least recently used ключ.

Signature

class LRUCache: 
    def __init__(self, capacity: int): ...
    def get(self, key: int) -> int: ...
    def put(self, key: int, value: int) -> None: ...

В проверках список операций проигрывается против вашего класса.

Решение прямо на странице

Напишите код, запустите проверки и только потом открывайте разбор.

Проверка решения

Нажмите «Запустить проверки» или Ctrl+Enter.

Показать разбор

Подсказки

  • Почему одного dict мало

    dict быстро ищет ключ, но не говорит, какой ключ least recently used.

  • Обновление тоже usage

    Если ключ обновили через put, он становится недавно использованным.

Идея решения

Нужны две структуры данных:

  • словарь для доступа к ключу за O(1);
  • порядок использования, чтобы быстро удалить самый старый ключ.

В Python это удобно выразить через OrderedDict: при get и обновлении ключ переносится в конец, а при переполнении удаляется первый элемент.

На реальном собеседовании могут попросить реализовать то же самое вручную через dict[key] -> node и двусвязный список.

Эталонный код

def lru_cache_interview(capacity: int, operations: list[list]) -> list[int]:
    from collections import OrderedDict

    cache = OrderedDict()
    result = []

    for op in operations:
        if op[0] == 'get':
            key = op[1]
            if key not in cache:
                result.append(-1)
                continue
            cache.move_to_end(key)
            result.append(cache[key])
        else:
            key, value = op[1], op[2]
            if key in cache:
                cache.move_to_end(key)
            cache[key] = value
            if len(cache) > capacity:
                cache.popitem(last=False)

    return result
Сложность
Время: O(1). Память: O(capacity).
Каждая операция get/put должна работать за O(1). Пространство ограничено вместимостью кеша.
5Вопрос10 мин

LRU cache как generic container: edge cases

Какие edge cases появляются, если LRU cache должен хранить любые пользовательские значения?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Нельзя смешивать “нет значения” и пользовательские значения вроде None. Нужны sentinel, точный API get/put, обработка capacity и стабильная O(1) структура.

Подробный разбор

Если cache хранит любые значения, None может быть нормальным значением. Поэтому get не должен возвращать None как единственный сигнал отсутствия ключа. Используют sentinel object, exception, pair found/value или API с explicit default.

Другие edge cases: capacity 0, повторный put существующего ключа, eviction самого старого элемента, hashability ключей, NaN как ключ/значение, порядок обновления при get и put. Для O(1) обычно нужен dict плюс doubly linked list или OrderedDict-подобная структура.

6Вопрос10 мин

Что такое p-value

Как объяснить p-value без ошибки “вероятность, что нулевая гипотеза верна”?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

P-value - вероятность при верной H0 получить статистику не менее экстремальную, чем наблюдаемая. Это не вероятность истинности H0.

Подробный разбор

P-value считается в мире, где нулевая гипотеза H0 верна. Он показывает вероятность получить наблюдаемое или более экстремальное значение тестовой статистики. Малый p-value означает, что такие данные плохо согласуются с H0 при выбранной модели теста.

Он не равен вероятности, что H0 верна, и не показывает размер эффекта. Для решения нужны заранее выбранный уровень значимости, мощность/MDE, доверительный интервал и практическая значимость эффекта.

7Вопрос10 мин

100 экспериментов и ложные открытия

Если провести 100 независимых тестов на уровне значимости 5%, что означает два p-value ниже 0.05?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

При 100 истинных H0 ожидается около 5 ложноположительных результатов. Два значимых результата сами по себе не сильное доказательство без multiple-testing correction.

Подробный разбор

Если все 100 нулевых гипотез истинны и тесты независимы, уровень значимости 5% означает ожидаемо около пяти false positives. Поэтому два p-value ниже 0.05 могут быть полностью совместимы со случайностью, особенно если тесты выбирались постфактум.

Для контроля используют Bonferroni, Holm, Benjamini-Hochberg/FDR или заранее фиксируют primary metrics. В продуктовых экспериментах также важно отделять exploratory analysis от confirmatory test и не выбирать метрику после просмотра результатов.

8Вопрос10 мин

Base rate и положительный тест

Почему высокая accuracy медицинского теста не означает высокую вероятность болезни после положительного результата?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Posterior зависит от prevalence. При редкой болезни даже небольшой false positive rate может дать много ложных положительных среди всех positive tests.

Подробный разбор

Нужно считать условную вероятность болезни при положительном тесте через Bayes theorem. В числителе стоит prevalence * sensitivity. В знаменателе - все положительные тесты: true positives плюс false positives среди здоровых.

Если болезнь редкая, здоровых людей намного больше. Даже при хорошей specificity абсолютное число false positives может быть сравнимо или больше true positives. Поэтому “99% accurate” без prevalence, sensitivity и specificity не отвечает на вопрос о posterior probability.

9Вопрос10 мин

Почему в LSTM явно выделяют time dimension

Какой смысл имеет time dimension в LSTM input и почему порядок шагов важен?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

LSTM обрабатывает последовательность по шагам времени, переносит hidden/cell state и поэтому различает порядок событий. Batch и feature dimensions имеют другой смысл.

Подробный разбор

Для LSTM вход обычно имеет axes batch, time и features. На каждом time step модель получает вектор признаков и обновляет hidden state и cell state. Эти состояния переносят информацию из прошлых шагов, поэтому перестановка времени меняет вычисление.

Разные фреймворки могут ожидать batch-first или time-first формат, но семантика остается той же: time axis задает порядок recurrence. Если перепутать time и feature dimensions, модель будет учить неверную структуру зависимости.

10Вопрос10 мин

Truncated BPTT для длинных последовательностей

Как обучать LSTM на последовательности длиной 100k шагов, если полный backprop слишком дорогой?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Последовательность режут на окна, hidden state переносят вперед, а граф градиентов периодически detach-ят. Это ограничивает память и длину backprop.

Подробный разбор

Полный backpropagation through time по 100k шагов хранит слишком много активаций и дает нестабильные градиенты. Truncated BPTT обрабатывает последовательность чанками: состояние переносится между окнами, но computational graph обрывается через detach на границе.

Длина окна выбирается как компромисс между дальним контекстом, памятью и стабильностью обучения. Дополнительно используют gradient clipping, padding/masking, bucketing по длинам и иногда архитектуры с attention/temporal convolution, если зависимость слишком дальняя для LSTM.