Перебір підмасок
Перебір усіх підмасок заданої маски
Маємо бітову маску і хочемо ефективно перебрати всі її підмаски, тобто маски , у яких встановлені лише ті біти, що входять до маски .
- Чи задача зводиться до перебору підмножин фіксованої множини бітів (динамічне програмування на масках, розбиття на групи)?
- Чи кількість елементів у масці мала (), щоб сумарні ітерацій по всіх масках з їхніми підмасками вкладалися в ліміт часу? (якщо ні → Перебір підмасок надто дорогий — шукайте поліноміальний підхід, напр. бінарні коефіцієнти)
- Чи потрібно перебрати саме підмаски заданої маски, а не всі масок поспіль?
Розгляньмо реалізацію цього алгоритму, що ґрунтується на трюках із бітовими операціями:
- C++
- Python
- TypeScript
- Go
int s = m;
while (s > 0) {
... тут можна використати s ...
s = (s-1) & m;
}
s = m
while s > 0:
# ... тут можна використати s ...
s = (s - 1) & m
// Бітові оператори в JS працюють із 32-бітними цілими, тож m < 2^31
let s = m;
while (s > 0) {
// ... тут можна використати s ...
s = (s - 1) & m;
}
s := m
for s > 0 {
// ... тут можна використати s ...
s = (s - 1) & m
}
або, використовуючи компактніший запис через for:
- C++
- Python
- TypeScript
- Go
for (int s=m; s; s=(s-1)&m)
... тут можна використати s ...
# У Python немає компактного for із кроком (s-1)&m, тож лишаємо while
s = m
while s:
# ... тут можна використати s ...
s = (s - 1) & m
// Бітові оператори в JS 32-бітні, тож m < 2^31
for (let s = m; s; s = (s - 1) & m) {
// ... тут можна використати s ...
}
for s := m; s != 0; s = (s - 1) & m {
// ... тут можна використати s ...
}
В обох варіантах коду підмаска, що дорівнює нулю, не буде оброблена. Ми можемо або обробити її поза циклом, або скористатися менш елегантною конструкцією, наприклад:
- C++
- Python
- TypeScript
- Go
for (int s=m; ; s=(s-1)&m) {
... тут можна використати s ...
if (s==0) break;
}
# do-while у Python імітуємо через while True з умовою виходу всередині
s = m
while True:
# ... тут можна використати s ...
if s == 0:
break
s = (s - 1) & m
// Бітові оператори в JS 32-бітні, тож m < 2^31
for (let s = m; ; s = (s - 1) & m) {
// ... тут можна використати s ...
if (s === 0) break;
}
for s := m; ; s = (s - 1) & m {
// ... тут можна використати s ...
if s == 0 {
break
}
}
Розгляньмо, чому наведений вище код відвідує всі підмаски без повторень і в спадному порядку.
Припустимо, що в нас є поточна бітова маска , і ми хочемо перейти до наступної бітової маски. Віднявши від маски одиницю, ми приберемо найправіший встановлений біт, а всі біти праворуч від нього стануть рівними 1. Потім ми прибираємо всі «зайві» одиничні біти, що не входять до маски і тому не можуть бути частиною підмаски. Це прибирання ми виконуємо за допомогою побітової операції (s-1) & m. У результаті ми «обрізаємо» маску , щоб визначити найбільше значення, якого вона може набути, тобто наступну підмаску після у спадному порядку.
Отже, цей алгоритм генерує всі підмаски даної маски у спадному порядку, виконуючи лише дві операції за ітерацію.
Окремий випадок — це коли . Після виконання ми отримаємо маску, у якій встановлені всі біти (бітове представлення -1), а після (s-1) & m матимемо, що дорівнюватиме . Тому з маскою слід бути обережними — якщо цикл не завершується на нулі, алгоритм може потрапити в нескінченний цикл.
Перебір усіх масок разом з їхніми підмасками. Складність
У багатьох задачах, особливо в тих, що використовують динамічне програмування на бітових масках, потрібно перебрати всі бітові маски і для кожної маски перебрати всі її підмаски:
- C++
- Python
- TypeScript
- Go
for (int m=0; m<(1<<n); ++m)
for (int s=m; s; s=(s-1)&m)
... s та m ...
for m in range(1 << n):
s = m
while s:
# ... s та m ...
s = (s - 1) & m
// Бітові оператори в JS 32-бітні, тож n < 31
for (let m = 0; m < (1 << n); ++m) {
for (let s = m; s; s = (s - 1) & m) {
// ... s та m ...
}
}
for m := 0; m < (1 << n); m++ {
for s := m; s != 0; s = (s - 1) & m {
// ... s та m ...
}
}
Доведімо, що внутрішній цикл виконається загалом за ітерацій.
Перше доведення: Розгляньмо -й біт. Для нього є рівно три можливості:
- він не входить до маски (а отже, не входить і до підмаски ),
- він входить до , але не входить до , або
- він входить як до , так і до .
Оскільки всього є бітів, то буде різних комбінацій.
Друге доведення: Зауважимо, що якщо маска має увімкнених бітів, то вона матиме підмасок. Оскільки в нас усього є масок з увімкненими бітами (див. біноміальні коефіцієнти), то загальна кількість комбінацій для всіх масок дорівнюватиме:
Щоб обчислити це число, зауважимо, що сума вище дорівнює розкладу за біноміальною теоремою. Отже, ми маємо комбінацій, що й треба було довести.