← Вернуться в блог

DP по подмножествам со счётом перестановок — задача «Axis Walking» РАЗБОР

  • 2200
  • bitmask-dp
  • dp
  • subset-dp
  • permutations
  • counting
  • dynamic-programming

Bitmask DP — техника, у которой пугающий ярлык: nn до 24, состояние «подмножество», экспоненциальное по подмножествам. На самом деле это та же динамика, что и линейная — только индекс состояния пробегает не отрезок [0,n][0, n], а множество всех 2n2^n подмножеств. Когда из условия ясно, что порядок прохождения элементов важен, а сами элементы — фиксированный набор без повторений, и nn маленькое (до 22–24) — bitmask DP часто оказывается единственным разумным способом.

Эта задача — эталонный учебный кейс: ответ — число перестановок с ограничением «не попадать в запрещённые точки», и эти запреты завязаны на сумму уже сделанных шагов. То есть состояние полностью описывается набором уже сделанных шагов, без необходимости знать их порядок — потому что сумма коммутативна. Ровно та структура, на которую ложится bitmask DP с состоянием «маска использованных».


Что дано

Игрок ходит по числовой прямой. Перед игрой ему даны nn положительных целых чисел a1,,ana_1, \ldots, a_n — длины шагов. За один ход игрок выбирает один ещё не использованный шаг и сдвигается на эту длину вправо. Каждый шаг используется ровно один раз; всего ходов — ровно nn.

Перед игрой на прямой отмечены kk «плохих» точек b1,,bkb_1, \ldots, b_k. После каждого хода игрок не должен оказаться ни в одной из плохих точек. Стартует игрок в точке 00, эта стартовая точка тоже не должна быть плохой (если 0{bj}0 \in \{b_j\}, ответ — 00).

Нужно найти количество различных порядков использования шагов a1,,ana_1, \ldots, a_n, при которых это условие выполняется. Ответ берётся по модулю 109+710^9 + 7.

Ограничения

  • 1n241 \le n \le 24.
  • 0k20 \le k \le 2.
  • 1ai1091 \le a_i \le 10^9.
  • 0bj10150 \le b_j \le 10^{15}.

Формат ввода

n
a_1 a_2 ... a_n
k
b_1 b_2 ... b_k

Формат вывода

Одно целое число — количество допустимых перестановок a1,,ana_1, \ldots, a_n по модулю 109+710^9 + 7.

Пример A

3
2 3 5
2
5 7

Перебор всех 3!=63! = 6 перестановок:

перестановкапрефиксыпрошла?
[2,3,5][2, 3, 5]2, 5, 10нет (5 ∈ bad)
[2,5,3][2, 5, 3]2, 7, 10нет (7 ∈ bad)
[3,2,5][3, 2, 5]3, 5, 10нет (5 ∈ bad)
[3,5,2][3, 5, 2]3, 8, 10да
[5,2,3][5, 2, 3]5, 7, 10нет (5 ∈ bad)
[5,3,2][5, 3, 2]5, 8, 10нет (5 ∈ bad)

Ответ — 11.

Пример B

4
1 2 3 4
2
3 7

Из 24 перестановок проходят 8 (полный список — в разделе «Проверка на примерах»). Ответ — 88.


Идея решения

Когда из условия в принципе непонятно, как атаковать, помогает упражнение «зафиксируй, какую информацию реально требует ограничение». Здесь ограничение работает после каждого хода: «текущая координата не равна ни одному из bjb_j». Текущая координата — это сумма уже сделанных шагов; она зависит только от множества уже использованных индексов, не от их порядка.

Это и есть ключевое наблюдение. Если у нас две частичные перестановки, использующие один и тот же набор индексов (например, {a1,a3}\{a_1, a_3\} против {a3,a1}\{a_3, a_1\}), они приведут игрока в одну и ту же точку. Поэтому достаточно знать подмножество использованных индексов — а не конкретный порядок их использования.

Состояние DP: dp[mask]dp[mask] = число способов прийти в текущую конфигурацию, то есть число различных порядков, в которых можно использовать индексы из maskmask как первые mask|mask| шагов так, чтобы все частичные суммы по пути были «хорошими» (не попадали в {bj}\{b_j\}).

Сразу две приятные особенности:

  1. Состояние одномерное. Маска — это число от 00 до 2n12^n - 1; никакого дополнительного параметра состояния (как «последний использованный элемент» в других bitmask-задачах) здесь не нужно.
  2. Сумма по маске — функция от маски. Точка, в которой стоит игрок после использования индексов maskmask, равна S[mask]=imaskaiS[mask] = \sum_{i \in mask} a_i. Эту сумму можно посчитать заранее за O(2n)O(2^n) — независимо от dp.

Состояние и переход

Состояние. dp[mask]dp[mask] — число различных порядков уложить элементы maskmask как первые mask|mask| ходов так, чтобы все промежуточные суммы (после 1-го, 2-го, …, mask|mask|-го хода) не лежали в {bj}\{b_j\}.

База. dp[]=dp[0]=1dp[\emptyset] = dp[0] = 1 — единственный способ ничего не сделать. Стартовая точка 00 предполагается «хорошей» — отдельная проверка перед запуском DP покрывает случай 0{bj}0 \in \{b_j\}.

Переход. Зафиксируем маску maskmask \neq \emptyset. Если S[mask]{bj}S[mask] \in \{b_j\}, то после прохода через maskmask игрок попадёт в «плохую» точку — таких порядков нет, dp[mask]=0dp[mask] = 0.

Иначе посмотрим, какой элемент пришёл последним в текущей перестановке. Это какой-то imaski \in mask. После его удаления из маски остаётся подмножество mask{i}mask \setminus \{i\} — предыдущая конфигурация. Перебираем все imaski \in mask как «последний» и суммируем:

dp[mask]=imaskdp[mask{i}].dp[mask] = \sum_{i \in mask} dp[mask \setminus \{i\}].

Идея перехода — перебор по последнему действию. Это стандартный приём для bitmask DP в задачах, где порядок важен.

Ответ. dp[(2n1)]dp[(2^n - 1)] — все nn элементов использованы, все промежуточные суммы хорошие.

Почему «по последнему действию», а не «по первому»

Можно было бы определить переход через первое действие: dp[mask]=imaskdp[mask{i}]dp'[mask] = \sum_{i \in mask} dp'[mask \setminus \{i\}], что выглядит так же. Но смысл состояния тогда другой: dp[mask]dp'[mask] — число способов разместить maskmask как последние mask|mask| ходов. Тогда фильтр по сумме должен проверять не только текущий префикс, но и «сумму до прихода в текущее множество», то есть полную сумму массива минус S[mask]S[mask] — это менее естественно. Перебор по последнему действию — более прямой.


Предподсчёт сумм по маскам

Считать S[mask]S[mask] в момент перехода — означает проход по mask|mask| битам, итого O(2nn)O(2^n \cdot n) только на суммы. Это удвоит и так тесную сложность.

Лучше — рекуррент по младшему биту. Пусть low(mask)=mask & (mask)low(mask) = mask\ \&\ (-mask) — младший установленный бит (например, low(0b1010)=0b0010low(0b1010) = 0b0010). Пусть i=log2low(mask)i = \log_2 low(mask) — позиция этого бита. Тогда

S[mask]=S[masklow(mask)]+ai.S[mask] = S[mask \oplus low(mask)] + a_i.

Это даёт SS за O(2n)O(2^n) — один проход по маскам в порядке возрастания, O(1)O(1) на маску. Аналогично можно перебирать по старшему биту — выбор не влияет на корректность.


Руками на примере B

Возьмём пример: n=4n = 4, a=[1,2,3,4]a = [1, 2, 3, 4], bad={3,7}bad = \{3, 7\}.

Шаг 1. Считаем S[mask]S[mask] для всех 16 масок. Обозначаю битами «использовал ли элемент aia_i» — бит 00 соответствует a1=1a_1 = 1, бит 11 соответствует a2=2a_2 = 2 и т.д. Маска 0b1011=110b1011 = 11 означает «использованы a1,a2,a4a_1, a_2, a_4».

mask (бин)maskS[mask]S[mask]bad\in bad?
000000
000111
001022
001133да
010043да
010154
011065
011176
100084
100195
1010106
1011117да
1100127да
1101138
1110149
11111510

Шаг 2. Считаем dp[mask]dp[mask] в порядке возрастания maskmask (это автоматически гарантирует, что все подмаски уже посчитаны).

maskбитыbad\in bad?переходdpdp
0\emptysetбаза1
1{1}\{1\}нетdp[0]dp[0]1
2{2}\{2\}нетdp[0]dp[0]1
3{1,2}\{1,2\}да0
4{3}\{3\}да0
5{1,3}\{1,3\}нетdp[4]+dp[1]=0+1dp[4] + dp[1] = 0 + 11
6{2,3}\{2,3\}нетdp[4]+dp[2]=0+1dp[4] + dp[2] = 0 + 11
7{1,2,3}\{1,2,3\}нетdp[6]+dp[5]+dp[3]=1+1+0dp[6] + dp[5] + dp[3] = 1 + 1 + 02
8{4}\{4\}нетdp[0]dp[0]1
9{1,4}\{1,4\}нетdp[8]+dp[1]=1+1dp[8] + dp[1] = 1 + 12
10{2,4}\{2,4\}нетdp[8]+dp[2]=1+1dp[8] + dp[2] = 1 + 12
11{1,2,4}\{1,2,4\}да0
12{3,4}\{3,4\}да0
13{1,3,4}\{1,3,4\}нетdp[12]+dp[9]+dp[5]=0+2+1dp[12] + dp[9] + dp[5] = 0 + 2 + 13
14{2,3,4}\{2,3,4\}нетdp[12]+dp[10]+dp[6]=0+2+1dp[12] + dp[10] + dp[6] = 0 + 2 + 13
15{1,2,3,4}\{1,2,3,4\}нетdp[14]+dp[13]+dp[11]+dp[7]=3+3+0+2dp[14] + dp[13] + dp[11] + dp[7] = 3 + 3 + 0 + 28

Ответ — dp[15]=8dp[15] = 8. Совпадает с прямым подсчётом 8 валидных перестановок из 24 (полный список в разделе «Проверка на примерах»).

Обратите внимание на «занулённые» маски 3,4,11,123, 4, 11, 12: они участвуют в переходах дальше, но всегда вносят 0. Это правильно: если на пути в любую конфигурацию мы попадём в «плохую» сумму — этот порядок отбраковывается, что и делает занулённый член.


Алгоритм целиком

прочитать n, a[1..n], k, bad[1..k]
если 0 ∈ bad:
    вывести 0; выйти

MOD ← 10^9 + 7
size ← 2^n
S[0]   ← 0
для mask от 1 до size - 1:
    low ← mask & -mask
    i   ← log2(low)
    S[mask] ← S[mask ⊕ low] + a[i]

dp[0]  ← 1
для mask от 1 до size - 1:
    если S[mask] ∈ bad:
        dp[mask] ← 0
        продолжить
    cur ← 0
    m ← mask
    пока m ≠ 0:
        low ← m & -m
        cur ← cur + dp[mask ⊕ low]
        m ← m ⊕ low
    dp[mask] ← cur mod MOD

вывести dp[size - 1]

Циклы по mask и по битам внутри mask — самая горячая часть. На n=24n = 24: внешний цикл — 2241.61072^{24} \approx 1.6 \cdot 10^7 итераций, внутренний — в среднем n/2=12n / 2 = 12. Итого порядка 21082 \cdot 10^8 операций тела цикла. В C++ это 2–3 секунды на современном CF-сервере, в Python — десятки секунд, не помещается в TL без перехода на нативные расширения.


Код решения

Сразу важное замечание про языки: эта задача в Python почти не проходит даже с агрессивными оптимизациями. Чистый Python на n=24n = 24 занимает 30\sim 306060 секунд, что превышает типичный CF TL (2233 секунды). Python-версия ниже корректна и проходит локальные тесты до n=20n = 20 за секунду; для CF-сабмита нужен C++. Включаю обе версии для целей разбора — Python читается легче и помогает разобраться в идее.

Комментарии по реализации

  • Тип для сумм. aia_i до 10910^9, nn до 2424 — сумма до 2.410102.4 \cdot 10^{10}. В C++ long long обязателен; int переполнится. В Python целые числа произвольной точности.
  • Тип для dp. Значения по модулю 109+710^9 + 7 — помещаются в int (32 бита). В C++ важно делать cur отдельно как long long, потому что nn слагаемых по 10910^9 дают сумму до 2.410102.4 \cdot 10^{10} до взятия модуля. Если хранить cur как int, на n=24n = 24 переполнение случится примерно на середине внутреннего цикла.
  • Память на n=24n = 24. В C++: S16M8=12816M \cdot 8 = 128 МБ, dp16M4=6416M \cdot 4 = 64 МБ. Итого 192\sim 192 МБ. CF обычно даёт 256256 МБ — впритык, но проходит. В Python list of int — 28\sim 28 байт на элемент, итого 900\sim 900 МБ; не помещается. Если упирается — array.array('q', ...) сокращает до 8 байт.
  • Извлечение младшего бита. mask & -mask работает в обоих языках. В C++ позиция бита — через __builtin_ctz за O(1)O(1); в Python — (low).bit_length() - 1, тоже O(1)O(1) для интов в нужном диапазоне.
  • Цикл по битам через m &= m - 1. Это идиома «снять младший установленный бит». В C++ компактнее, чем XOR через low. В Python обе идиомы дают одинаковую производительность.
  • Множество bad в Python. При k2k \le 2 выбор между set и list не критичен (проверка членства за O(k)O(k) против O(1)O(1) — на 2 элементах разница нулевая). Я оставил set для общности; для kk большего масштаба разница будет.

Проверка на примерах

Пример A

n=3n = 3, a=[2,3,5]a = [2, 3, 5], bad={5,7}bad = \{5, 7\}.

maskбитыSSbad\in bad?dpdp
0\emptyset01
1{1}\{1\}2нет1
2{2}\{2\}3нет1
3{1,2}\{1,2\}5да0
4{3}\{3\}5да0
5{1,3}\{1,3\}7да0
6{2,3}\{2,3\}8нетdp[4]+dp[2]=0+1=1dp[4]+dp[2]=0+1=1
7{1,2,3}\{1,2,3\}10нетdp[6]+dp[5]+dp[3]=1+0+0=1dp[6]+dp[5]+dp[3]=1+0+0=1

Ответ — dp[7]=1dp[7] = 1. ✓

Пример B

Полный список 8 валидных перестановок (для контроля):

#перестановкапрефиксы
1[1,3,2,4][1, 3, 2, 4]1, 4, 6, 10
2[1,3,4,2][1, 3, 4, 2]1, 4, 8, 10
3[1,4,3,2][1, 4, 3, 2]1, 5, 8, 10
4[2,3,1,4][2, 3, 1, 4]2, 5, 6, 10
5[2,3,4,1][2, 3, 4, 1]2, 5, 9, 10
6[2,4,3,1][2, 4, 3, 1]2, 6, 9, 10
7[4,1,3,2][4, 1, 3, 2]4, 5, 8, 10
8[4,2,3,1][4, 2, 3, 1]4, 6, 9, 10

Все 8 перестановок свободны от значений 3 и 7 во всех префиксах. Прямой подсчёт совпал с DP: dp[15]=8dp[15] = 8. ✓


Крайние случаи

  • k=0k = 0 (нет плохих точек). Все n!n! перестановок проходят. DP вернёт n!mod(109+7)n! \bmod (10^9 + 7). На n=24n = 24: 24!mod(109+7)24! \bmod (10^9 + 7) — корректно за O(2nn)O(2^n \cdot n), шаблон не упрощается, но и не ломается.
  • 0bad0 \in bad. Стартовая точка — плохая, ответ 00. Эту проверку нужно делать до DP, потому что в самом DP мы не проверяем «до первого хода»: dp[0] = 1 без учёта bad. Без явной проверки шаблон вернёт неправильный n!n! вместо 00.
  • aibad\sum a_i \in bad. Финальная точка плохая, ответ 00. DP это поймает сам: S[(2n1)]=aiS[(2^n - 1)] = \sum a_i попадает в bad, ветка «продолжить» обнулит dp[(2n1)]dp[(2^n - 1)].
  • Все маски «плохие». Например, n=1n = 1, a=[5]a = [5], bad={5}bad = \{5\}. Ответ 00 — единственная маска {1}\{1\} имеет сумму 5, попадает в bad.
  • n=1n = 1, bad=bad = \emptyset. Ответ 11 — единственная перестановка из одного шага.
  • Очень близкие к лимиту bjb_j. bjb_j до 101510^{15}, S[mask]S[mask] до 241092.4101024 \cdot 10^9 \approx 2.4 \cdot 10^{10}. Все «плохие» значения, бо́льшие ai\sum a_i, никогда не достигаются — формально DP их игнорирует, корректно. В коде сравнение S[mask] == bad[j] работает на одинаковом 64-битном типе, переполнения нет.

Типичные ошибки

  1. Забытая проверка 0bad0 \in bad. Без неё dp[0] = 1 без условий, и шаблон вернёт неверный n!n! вместо 00 для случая «старт уже плохой».
  2. cur как int в C++. На n=24n = 24 сумма nn значений 1091\le 10^9 - 1 до взятия модуля переполнит int. Накапливать обязательно в long long.
  3. Использование int для S[mask]S[mask] в C++. Сумма до 2.410102.4 \cdot 10^{10}, переполнение int гарантировано. long long для SS.
  4. Перестановка не в той форме. Естественный неверный вариант: dp[mask][i]=dp[mask][i] = «маска и последний элемент». Это даёт правильный ответ, но память O(2nn)O(2^n \cdot n) — на n=24n = 24 это 400\sim 400 МБ, не помещается. Корректный шаблон обходится без i, потому что сумма по маске не зависит от порядка — нет смысла раздваивать состояние.
  5. Проверка bad через линейный поиск по большому kk. В этой задаче k2k \le 2, и линейная проверка for (int j = 0; j < k; ++j) — даже быстрее, чем unordered_set. В похожих задачах с большим kk нужен set / unordered_set или предварительная сортировка с бинпоиском.
  6. Итерация по маскам в неправильном порядке. Возрастающий порядок maskmask гарантирует, что все подмаски (с меньшим числом битов и меньшим значением) уже посчитаны. Идти, например, по числу установленных битов и внутри — по значению — тоже корректно, но это лишняя сложность реализации; обычный for mask in 1..size-1 достаточен.
  7. Переполнение при S[mask]badS[mask] \in bad для огромных bjb_j. Если хранить bjb_j в int, сравнение S[mask] == bad[j] при bj>231b_j > 2^{31} даст неопределённое поведение из-за неявных приведений. long long везде убирает риск.
  8. «Оптимизация» с пропуском занулённых масок. Желание не считать dp[mask]dp[mask] для «плохих» масок отдельным шагом — корректное, но удалять их из массива нельзя: они нужны как нули в переходах для бо́льших масок (см. пример B, маски 3, 4, 11, 12 нужны для переходов в 7, 13, 14, 15).

Сложность

  • Время. Внешний цикл по маскам — 2n2^n. Внутренний цикл по битам в маске — в худшем случае nn, в среднем n/2n/2. Сумма по всем маскам числа установленных битов — maskpopcount(mask)=n2n1\sum_{mask} \text{popcount}(mask) = n \cdot 2^{n-1}. Итого O(n2n1)=O(n2n)O(n \cdot 2^{n-1}) = O(n \cdot 2^n). На n=24n = 24: 2108\sim 2 \cdot 10^8 — в C++ 2–3 секунды.
  • Память. O(2n)O(2^n) для массива SS и O(2n)O(2^n) для dpdp. На n=24n = 24192\sim 192 МБ в C++, не помещается в Python без array.array.
  • Предподсчёт сумм. O(2n)O(2^n) — линейно по числу масок, по O(1)O(1) операций каждая.

Альтернативное состояние dp[mask][i]dp[mask][i] (последний элемент) при той же асимптотике даёт O(n2n)O(n \cdot 2^n) времени, но O(n2n)O(n \cdot 2^n) памяти. На n=24n = 24 это уже не проходит по памяти. Поэтому для задач, где порядок внутри маски не важен для перехода, версия с одномерным состоянием — единственная разумная.


Связанные задачи

  • Гамильтонов путь / цикл за O(n22n)O(n^2 \cdot 2^n). Здесь состояние двумерное — dp[mask][last]dp[mask][last]. «Последний» нужен, потому что переход — это «добавить ребро last → next», и стоимость зависит от пары вершин. На n18n \le 18 — терпимо (107\sim 10^7).
  • DP по подмножествам подмножества (subset sum DP). dp[mask]=min(ans(masksub)+f(sub))dp[mask] = \min(\text{ans}(mask \setminus sub) + f(sub)), где sub пробегает все подмаски mask. Тонкость — порядок перебора подмасок: sub = (sub - 1) & mask — стандартная идиома, даёт суммарную сложность O(3n)O(3^n).
  • DP по битовой маске с дополнительным числовым параметром. Когда состояние требует «маска + значение в каком-то диапазоне» — это смесь двух парадигм; на полосе CF 2300+.
  • Counting/permutations через включения-исключения. Альтернатива bitmask DP для задач со счётом перестановок с ограничениями. Часто даёт лучший асимптотически результат — O(poly(n))O(\text{poly}(n)) против O(2nn)O(2^n \cdot n) — но требует более тонкого формулирования.
  • Тема в этой серии. Bitmask DP — часть третьей ступени серии «Стратегия победы: DP — от очевидного шаблона к ёмкому состоянию»: переход к подмножеству-состоянию — содержательный шаг от линейного DP и от двумерного DP.

Итого

  • Идея: dp[mask]dp[mask] — число допустимых порядков пройти через подмножество maskmask как первые mask|mask| шагов. Сумма по маске = координата игрока после mask|mask| ходов.
  • Ключевое наблюдение: сумма imaskai\sum_{i \in mask} a_i не зависит от порядка → второй параметр состояния не нужен.
  • Переход: перебор «последнего» использованного элемента — dp[mask]=imaskdp[mask{i}]dp[mask] = \sum_{i \in mask} dp[mask \setminus \{i\}] при S[mask]badS[mask] \notin bad, иначе 00.
  • Предподсчёт сумм: S[mask]=S[masklow(mask)]+alog2low(mask)S[mask] = S[mask \oplus \text{low}(mask)] + a_{\log_2 \text{low}(mask)} за O(2n)O(2^n).
  • Сложность: O(n2n)O(n \cdot 2^n) времени, O(2n)O(2^n) памяти. На n=24n = 24 — 2–3 секунды в C++.
  • Главные ловушки: проверка 0bad0 \in bad до DP, long long для сумм и cur, не раздваивать состояние по «последнему элементу» (лишняя память).

Источник задачи: Codeforces 327E «Axis Walking».

Попробуй разобрать похожие задачи

В CodePal AI-партнёр подсказывает идею, а не ответ. Разбор в диалоге, код проверяется в браузере.