Перейти до основного вмісту

Перебір підмасок

Перебір усіх підмасок заданої маски

Маємо бітову маску mm і хочемо ефективно перебрати всі її підмаски, тобто маски ss, у яких встановлені лише ті біти, що входять до маски mm.

Коли підходить цей алгоритм?

Розгляньмо реалізацію цього алгоритму, що ґрунтується на трюках із бітовими операціями:

int s = m;
while (s > 0) {
... тут можна використати s ...
s = (s-1) & m;
}

або, використовуючи компактніший запис через for:

for (int s=m; s; s=(s-1)&m)
... тут можна використати s ...

В обох варіантах коду підмаска, що дорівнює нулю, не буде оброблена. Ми можемо або обробити її поза циклом, або скористатися менш елегантною конструкцією, наприклад:

for (int s=m; ; s=(s-1)&m) {
... тут можна використати s ...
if (s==0) break;
}

Розгляньмо, чому наведений вище код відвідує всі підмаски mm без повторень і в спадному порядку.

Припустимо, що в нас є поточна бітова маска ss, і ми хочемо перейти до наступної бітової маски. Віднявши від маски ss одиницю, ми приберемо найправіший встановлений біт, а всі біти праворуч від нього стануть рівними 1. Потім ми прибираємо всі «зайві» одиничні біти, що не входять до маски mm і тому не можуть бути частиною підмаски. Це прибирання ми виконуємо за допомогою побітової операції (s-1) & m. У результаті ми «обрізаємо» маску s1s-1, щоб визначити найбільше значення, якого вона може набути, тобто наступну підмаску після ss у спадному порядку.

Отже, цей алгоритм генерує всі підмаски даної маски у спадному порядку, виконуючи лише дві операції за ітерацію.

Окремий випадок — це коли s=0s = 0. Після виконання s1s-1 ми отримаємо маску, у якій встановлені всі біти (бітове представлення -1), а після (s-1) & m матимемо, що ss дорівнюватиме mm. Тому з маскою s=0s = 0 слід бути обережними — якщо цикл не завершується на нулі, алгоритм може потрапити в нескінченний цикл.

Перебір усіх масок разом з їхніми підмасками. Складність O(3n)O(3^n)

У багатьох задачах, особливо в тих, що використовують динамічне програмування на бітових масках, потрібно перебрати всі бітові маски і для кожної маски перебрати всі її підмаски:

for (int m=0; m<(1<<n); ++m)
for (int s=m; s; s=(s-1)&m)
... s та m ...

Доведімо, що внутрішній цикл виконається загалом за O(3n)O(3^n) ітерацій.

Перше доведення: Розгляньмо ii-й біт. Для нього є рівно три можливості:

  1. він не входить до маски mm (а отже, не входить і до підмаски ss),
  2. він входить до mm, але не входить до ss, або
  3. він входить як до mm, так і до ss.

Оскільки всього є nn бітів, то буде 3n3^n різних комбінацій.

Друге доведення: Зауважимо, що якщо маска mm має kk увімкнених бітів, то вона матиме 2k2^k підмасок. Оскільки в нас усього є (nk)\binom{n}{k} масок з kk увімкненими бітами (див. біноміальні коефіцієнти), то загальна кількість комбінацій для всіх масок дорівнюватиме:

k=0n(nk)2k\sum_{k=0}^n \binom{n}{k} \cdot 2^k

Щоб обчислити це число, зауважимо, що сума вище дорівнює розкладу (1+2)n(1+2)^n за біноміальною теоремою. Отже, ми маємо 3n3^n комбінацій, що й треба було довести.

Задачі для практики