1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8

# конспект · шаг 8 из 8

Конспект: генерация комбинаторных объектов

выжимка - её можно унести в заметки

Понятия

Подмножество - любой выбор части элементов, порядок не важен; всего их 2ⁿ.

Перестановка - порядок всех элементов; всего n!.

Сочетание - выбор k элементов без учёта порядка; всего C(n,k) = n!/(k!·(n−k)!).

Маска - подмножество, закодированное числом: разряд i равен единице, если элемент взят. Читается арифметикой mask // 2**i % 2 или побитово - (mask >> i) & 1.

Монотонный индекс - приём «назад не возвращаемся»: после взятия элемента i спуск идёт с i + 1. Механическая форма отказа от порядка внутри набора.

Разбиение числа - сумма без учёта порядка; перечисление по невозрастанию частей, подсчёт разменом (внешний цикл по частям). Разбиение множества - группы различных предметов; номер группы не больше «максимум + 1», число Белла.

Формула Кэли - n^(n−2) деревьев на n пронумерованных вершинах. Код Прюфера - отрезать наименьший лист, записать соседа; степень = 1 + число появлений в коде.

Что унести

ЧИСЛО ОБЪЕКТОВ ИЗ ОДНОЙ СТРОКИ КОДА
rec(i + 1, ...) из цикла с i = start      сочетания           C(n,k)
rec(i, ...)     из цикла с i = start      с повторениями      C(n+k−1, k)
rec(i + 1, ...) из цикла с i = 0          все последовательности  nᵏ
две ветки «берём / не берём»              подмножества        2ⁿ
флаг «занято» на каждый элемент           перестановки        n!
n 2ⁿ n! во сколько раз больше
10 1024 3 628 800 3,5 тысячи
15 32 768 1,3·10¹² 40 миллионов
20 1 048 576 2,4·10¹⁸ 2,3 триллиона

Узлов в дереве перебора - не столько же, сколько ответов: у подмножеств 2ⁿ⁺¹ − 1 (вдвое больше листьев), у перестановок около e·n! ≈ 2,72·n!. То есть накладные расходы перебора - константа, и бороться надо с числом ответов, а не с устройством обхода.

ТРИ СПОСОБА ПЕРЕБРАТЬ ПЕРЕСТАНОВКИ (замер при n = 6, у обеих рекурсий 1957 вызовов)
флаги «занято» + склейка набора     45 036 шагов   лексикографический порядок
обмен на месте, с откатом           16 889 шагов   порядок СВОЙ, не лексикографический
next_permutation без рекурсии       12 454 шага    лексикографический, без стека и без дублей

при n = 7 версия с флагами в предел шагов движка НЕ укладывается, обмен - да
ЦЕНА ВЫБОРА ПРИЁМА (одинаковый ответ, разные шаги)
сочетания C(8,4) = 70
    монотонный индекс            1583 шага
    фильтр по маске (2⁸ наборов) 9036 шагов   в 5,7 раза дороже
    сломанная монотонность       46 727 шагов и ОТВЕТ 4096

подмножества, n = 10
    рекурсия                     12 285 шагов
    счётчик по маске             41 986 шагов   в 3,4 раза дороже: n разрядов на набор

Порядок выдачи - свойство приёма, а не задачи. Рекурсия «берём первым» идёт от полного набора к пустому; счётчик по маске - от пустого, но не лексикографически (после [2, 3, 4] у него идёт [1, 2, 5]); монотонный индекс даёт лексикографический порядок сам, без сортировки. Нужен конкретный порядок - проверяй запуском на трёх элементах.

Отсечение - сделка, а не улучшение. Проверка стоит шаг на каждом узле, экономит - только на отсечённых ветках. Замер отсечения по остатку в сочетаниях: C(10,8) 63 процента выигрыша, C(10,5) семь, C(20,3) один процент ПРОИГРЫША. Мерить, а не предполагать.

Ловушки джокера

Сохранил набор, а не его копию. otvety.append(nabor) при переборе с append/pop записывает ссылку: восемь ответов на трёх элементах превращаются в восемь пустых списков. Лечится срезом nabor[:].

Забыл откат. Состояние, изменённое перед спуском (обмен, флаг, добавленный элемент), обязано быть восстановлено после возврата. Без отката программа не падает - она выдаёт не те ответы.

start объявлен и не использован. Сочетания превращаются в последовательности: 25 вместо 10 при n = 5, k = 2. Признак - число ответов похоже на степень, а не на биномиальный коэффициент.

Дубли при равных элементах. [1, 1, 2, 2] даёт 24 перестановки вместо 6. Пропуск второго экземпляра равного значения (пока свободен первый) чинит и ответ, и цену: 390 шагов вместо 1122.

Собрал все ответы в список. C(30,15) - 155 миллионов наборов, 2²⁵ - 33 миллиона. Обрабатывать надо по одному, на месте.

Решил, что программа сломана, когда она упёрлась в предел. При n = 15 перебор подмножеств не укладывается в 300 000 шагов движка - столько работы и заказано. Лечится не отладкой, а сокращением перебора.

Посчитал C(n,k) через факториалы в лоб. 30! не помещается в 64-битное целое, хотя C(30,15) помещается легко. Считают произведением с делением на каждом шаге.

Куда это ведёт

Перебирать мы научились - и упёрлись в стену ровно там, где её и ждали: 2ⁿ при n = 15, n! при n = 7. Следующая глава - про то, как не раскрывать ветки, в которых ответа заведомо нет: поиск с возвратомРазбирается в разделе 16, глава «Поиск с возвратом и отсечения»: Поиск с возвратом: проверять на входе в ветку, а не на дне с проверкой на входе в ветку, отсечение по оценкеРазбирается в разделе 16, глава «Поиск с возвратом и отсечения»: Отсечения: по оценке, по симметрии и по порядку переменных и отсечение по симметрии. Выигрыш там измеряется уже не процентами, а разами - но правило «отсечение надо мерить» останется.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.