# теория · шаг 3 из 8
ДП по маскам: состояние-подмножество и n до двадцати
Коротко
Когда в состоянии нужно «какое ПОДМНОЖЕСТВО уже использовано», его кодируют числом: бит i равен
единице, если элемент i взят. Состояний 2^n, и это переводит задачи вроде коммивояжёра из
n! в 2^n·n² - при n = 20 разница между 2.4·10¹⁸ и 4·10⁸. Замер на движке курса: перебор
перестановок умирает на n = 8, ДП по маскам доходит до n = 10 за 153 тысячи шагов и даёт тот же
ответ. Порядок обхода простой - маски по возрастанию числа, потому что переход добавляет элемент и
только увеличивает число. Граница применимости жёсткая: n до 20-22, дальше 2^n не помещается
никуда.
Если ты умеешь закодировать подмножество числом, помнишь оценку 2^n·n² и границу применимости, листай до «Теперь сам».
Как думаешь, когда подмножество приходится держать в состоянии
Во второй главе мы видели задачу, где подструктуры НЕТ именно из-за общего ресурса: самый длинный
простой путь, где нельзя посещать вершину дважды. Тогда же было сказано, что ресурс можно внести в
состояние - и что ценой этого становится 2^n.
Прежде чем читать дальше: как вообще уместить «множество посещённых вершин» в индекс массива?
На что это похоже
Отметки в журнале регистрации. Вместо списка фамилий - строка галочек по числу участников: пришёл или нет. Строка из двадцати галочек - это одно число, и по нему мгновенно видно всё состояние зала. Маска и есть такой журнал: компактный, целочисленный и годный в качестве индекса.
Механизм: маска, переход, порядок
КОДИРОВАНИЕ ПОДМНОЖЕСТВА ЧИСЛОМ
элемент i взят ⇔ i-й бит числа равен единице
маска 0 = 00000 = {} маска 5 = 00101 = {0, 2}
маска 1 = 00001 = {0} маска 31 = 11111 = {0,1,2,3,4}
всего подмножеств n элементов: 2^n
ОПЕРАЦИИ НАД МАСКОЙ (битовые - и их арифметические двойники)
взят ли элемент i: mask & (1 << i) или mask // 2**i % 2
добавить элемент i: mask | (1 << i) или mask + 2**i (если не взят)
полное множество: (1 << n) − 1 или 2**n − 1
ЗАДАЧА КОММИВОЯЖЁРА (TSP) - эталонный пример
состояние: (маска посещённых городов, город, в котором стоим)
dp[mask][v] = минимальная длина пути, прошедшего ровно города из mask
и закончившегося в v
переход: из (mask, v) идём в не посещённый город u
dp[mask + 2**u][u] = min(..., dp[mask][v] + d[v][u])
база: dp[1][0] = 0 - стоим в городе 0, посещён только он
ответ: min по v от dp[полная маска][v] + d[v][0]
состояний 2^n·n, переход перебирает n городов ⇒ Θ(2^n · n²)
ПОРЯДОК: маски по ВОЗРАСТАНИЮ числа
переход добавляет элемент, то есть всегда увеличивает число маски -
значит к моменту счёта маски все меньшие уже готовы
Правило
Если в состоянии нужно «какое подмножество использовано», его кодируют числом: бит i показывает,
взят ли элемент i. Состояний 2^n, обходят их по ВОЗРАСТАНИЮ числа - переход добавляет элемент и
только увеличивает маску. Коммивояжёр: состояние «(маска посещённых, текущий город)», цена
Θ(2^n·n²) вместо Θ(n!) у перебора перестановок. Граница применимости жёсткая: при n = 20 это
4·10⁸ операций и 2·10⁷ клеток - на пределе, при n = 25 уже 2·10¹⁰ и гигабайты. Поэтому ДП по
маскам - инструмент для n до двадцати с небольшим, и ровно столько же, сколько стоит сама маска,
стоит помнить про её арифметические двойники: не во всякой среде есть побитовые операции.
Разбор: коммивояжёр по маскам против перебора
Задача: обойти все n городов, вернуться в начальный, суммарную длину минимизировать.
ПЕРЕБОР ПЕРЕСТАНОВОК (n−1)! маршрутов
ДП ПО МАСКАМ 2^n·n состояний, переход n
ЗАМЕР НА ДВИЖКЕ КУРСА (один и тот же граф, ответы совпадают)
n перебор перестановок ДП по маскам
5 4 623 шага 1 656 шагов
6 31 375 ...
7 245 158 10 706
8 не уложился 26 465
9 не уложился 64 330
10 не уложился 153 906
Перебор умирает между 7 и 8 городами, маски доходят до десяти - и это предел уже самого движка (300 тысяч шагов), а не метода. На настоящей машине маски считают до 20-22 городов.
Разберём, почему при таком состоянии подструктура ПОЯВЛЯЕТСЯ. Во второй главе её не было: оптимальный
путь q→r нельзя было приклеить к r→t, потому что они делили вершины. Теперь множество
использованных вершин стоит В СОСТОЯНИИ - и подзадачи «пройти ровно эти города и оказаться в v»
между собой независимы. Общий ресурс перестал быть общим: он стал параметром.
цена этого фокуса видна прямо в формуле
было: состояний n (только текущая вершина) - но подструктуры нет
стало: состояний 2^n·n - подструктура есть, зато экспонента
ПОЧЕМУ ЭКСПОНЕНТА ВСЁ-ТАКИ ЛУЧШЕ ФАКТОРИАЛА
(маршрутов (n−1)!, потому что начальный город фиксирован)
n маршрутов (n−1)! 2^n·n² выигрыш
10 362 880 102 400 в 3.5 раза
15 8.7·10¹⁰ 7 372 800 в 12 тысяч раз
20 1.2·10¹⁷ 4.2·10⁸ в 290 миллионов раз
25 6.2·10²³ 2.1·10¹⁰ в 3·10¹³ раз, но столько
не считается ни тем, ни этим
Разбор: другие задачи с маской в состоянии
РАЗБИЕНИЕ НА ПАРЫ (идеальное паросочетание минимального веса)
состояние: маска уже разобранных элементов
переход: взять младший незанятый элемент и перебрать ему партнёра
цена Θ(2^n · n) - младший элемент фиксирован, поэтому не n²
ПОКРЫТИЕ МНОЖЕСТВАМИ (точное, в отличие от жадного из раздела 14)
состояние: маска накрытых элементов
переход: добавить одно множество
цена Θ(2^n · число множеств)
РАСКЛАДКА ПО ЯЩИКАМ, РАСКРАСКА, РАСПИСАНИЕ С ЗАПРЕТАМИ
везде одно и то же: «что уже использовано» не сжимается в число,
поэтому приходится помнить множество целиком
Важная деталь из первого примера: перебирать партнёра надо для ЗАФИКСИРОВАННОГО элемента (например,
младшего свободного), а не для всех пар. Это убирает множитель n и заодно избавляет от дублей -
типовой приём в задачах на разбиение.
И общий признак, по которому узнают эти задачи: в условии есть слово «каждый ровно один раз» или
«все, без повторов», а число элементов подозрительно маленькое - 15, 18, 20. Маленькое n в условии
при NP-трудной формулировке почти всегда означает «решай по маскам».
Что ломается без этого
Первое: обходить маски по убыванию - переход ссылается на меньшие маски, и они окажутся
несчитанными. Второе: писать mask + 2**u, не проверив, что элемент ещё не взят: иначе число
«переполнится» в чужой бит и состояние поедет. Третье: забыть, что dp[mask][v] имеет смысл только
если v входит в mask - половина таблицы останется мусором, и минимум по ней даст неверный ответ.
Четвёртое: применять маски при n = 30: это 2^30 ≈ 10⁹ масок и 3·10¹⁰ клеток, и никакая
константа не спасёт.
Пятое: в задаче на разбиение перебирать все пары вместо фиксации младшего свободного элемента:
получится лишний множитель n и дубли. Шестое: считать, что битовые операции обязательны - в
среде без них (как в движке этого курса) та же маска читается арифметикой mask // 2**i % 2, и
цена вырастает лишь на константу.
Зачем это в работе
ДП по маскам - это инструмент для маленьких точных задач, и такие задачи в работе встречаются чаще,
чем кажется. Распределить восемнадцать смен между сотрудниками без нарушений; подобрать набор из
двадцати признаков, дающий лучшую модель; найти оптимальный порядок обхода десяти точек для курьера;
разложить двадцать деталей по минимальному числу заготовок. Везде одно и то же: формулировка
NP-трудна, но размер данных маленький, и точный ответ достижим за секунды - вместо того чтобы брать
приближение из раздела 14. Второй практический слой - распознавание: если в задаче n <= 20 и в
условии «каждый ровно один раз», это почти наверняка маски. И третий - честность в оценках: у
коммивояжёра на двадцати городах состояний 2^20 · 20 ≈ 2·10⁷, то есть 160 мегабайт при восьми
байтах на клетку, а при двадцати пяти городах - больше шести гигабайт. Метод упирается не только во
время, но и в память, и об этом стоит сказать до того, как код написан.
Теперь сам
Задача: восемь человек надо разбить на четыре пары, у каждой пары своя «стоимость несовместимости». Минимизировать сумму. Какое состояние и какая цена?
Ответ. Состояние - маска уже разобранных людей, dp[mask] = минимальная стоимость, если люди из
маски уже разбиты на пары.
переход: берём МЛАДШЕГО свободного человека i (он обязан попасть в какую-то пару)
и перебираем ему партнёра j из оставшихся:
dp[mask + 2**i + 2**j] = min(..., dp[mask] + cost[i][j])
состояний 2^8 = 256, переход перебирает не больше 7 партнёров
цена Θ(2^n · n) ≈ 2048 операций
Почему берём именно младшего свободного: любой человек должен быть в паре, значит можно без потери
общности разбирать их в фиксированном порядке. Если вместо этого перебирать ВСЕ пары свободных, одна
и та же раскладка посчитается многократно - ответ останется верным, а цена вырастет в n раз.
Сравни с перебором: разбиений восьми человек на пары - 7 · 5 · 3 · 1 = 105, то есть на восьми людях
перебор даже проще. А на двадцати: 19·17·15·...·1 = 654 729 075 разбиений против 2^20 · 20 ≈ 2·10⁷
операций у масок - разница в 31 раз. На двадцати четырёх она уже в 785 раз, и дальше только растёт.
Главное
Подмножество в состоянии кодируют числом: бит i показывает, взят ли элемент i; состояний 2^n,
обходят их по ВОЗРАСТАНИЮ числа, потому что переход добавляет элемент и увеличивает маску.
Коммивояжёр: состояние «(маска посещённых, текущий город)», цена Θ(2^n·n²) против Θ(n!) у
перебора; замер на движке - перебор перестановок не укладывается уже на 8 городах, маски доходят до
10 за 153 906 шагов с тем же ответом. Маска - это тот самый приём «внести общий ресурс в состояние»
из второй главы: подструктура появляется, но ценой экспоненты. Граница жёсткая: n до 20-22 (у коммивояжёра на
двадцати городах 2^20·20 ≈ 2·10⁷ клеток - 160 МБ, при двадцати пяти - больше шести гигабайт). В задачах на разбиение перебирают
партнёра для ЗАФИКСИРОВАННОГО младшего свободного элемента - это убирает множитель n и дубли.
Признак задачи: «каждый ровно один раз» плюс подозрительно маленькое n.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий