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

Генерування всіх KK-комбінацій

У цій статті ми обговоримо задачу генерування всіх KK-комбінацій. Дано натуральні числа NN і KK, і розглядаємо множину чисел від 11 до NN. Потрібно отримати всі підмножини розміру KK.

Коли підходить цей алгоритм?
  • Чи треба перелічити (вивести/перебрати) самі підмножини розміру KK, а не лише дізнатися їхню кількість? (якщо потрібна лише кількість → Біноміальні коефіцієнти)
  • Чи кількість комбінацій (NK)\binom{N}{K} достатньо мала, щоб перебір був реальним за часом?
  • Чи потрібен конкретний порядок перебору (лексикографічний) або генерування через код Грея?

Генерування наступної комбінації в лексикографічному порядку

Спершу ми згенеруємо їх у лексикографічному порядку. Алгоритм для цього простий. Першою комбінацією буде 1,2,...,K{1, 2, ..., K}. Тепер подивимося, як знайти комбінацію, що йде безпосередньо за нею в лексикографічному порядку. Для цього розглянемо нашу поточну комбінацію і знайдемо найправіший елемент, який ще не досяг свого найбільшого можливого значення. Знайшовши цей елемент, ми збільшуємо його на 11 і присвоюємо всім наступним елементам найменші допустимі значення.

bool next_combination(vector<int>& a, int n) {
int k = (int)a.size();
for (int i = k - 1; i >= 0; i--) {
if (a[i] < n - k + i + 1) {
a[i]++;
for (int j = i + 1; j < k; j++)
a[j] = a[j - 1] + 1;
return true;
}
}
return false;
}

Генерування всіх KK-комбінацій так, щоб сусідні комбінації відрізнялися одним елементом

Цього разу ми хочемо згенерувати всі KK-комбінації в такому порядку, щоб сусідні комбінації відрізнялися рівно одним елементом.

Це можна розв'язати за допомогою коду Грея: якщо ми зіставимо кожній підмножині бітову маску, то, генеруючи й перебираючи ці бітові маски за допомогою кодів Грея, можемо отримати нашу відповідь.

Задачу генерування KK-комбінацій також можна розв'язати за допомогою кодів Грея іншим способом: згенеруємо коди Грея для чисел від 00 до 2N12^N - 1 і залишимо лише ті коди, що містять KK одиниць. Дивовижний факт полягає в тому, що в отриманій послідовності з KK одиничних бітів будь-які дві сусідні маски (включно з першою й останньою маскою — сусідніми в циклічному сенсі) — відрізнятимуться рівно двома бітами, що і є нашою метою (вилучити число, додати число).

Доведемо це:

Для доведення пригадаємо той факт, що послідовність G(N)G(N) (яка представляє NNй код Грея) можна отримати так:

G(N)=0G(N1)1G(N1)RG(N) = 0G(N-1) \cup 1G(N-1)^\text{R}

Тобто розглянемо послідовність кодів Грея для N1N-1 і допишемо 00 перед кожним членом. А також розглянемо обернену послідовність кодів Грея для N1N-1, допишемо 11 перед кожною маскою і сконкатенуємо ці дві послідовності.

Тепер ми можемо навести наше доведення.

Спершу доведемо, що перша й остання маски відрізняються рівно двома бітами. Для цього достатньо зауважити, що перша маска послідовності G(N)G(N) матиме вигляд: NKN-K нулів, після яких іде KK одиниць. Оскільки перший біт встановлено як 00, після чого йдуть (NK1)(N-K-1) нулів, після чого йдуть KK одиничних бітів, а остання маска матиме вигляд: 11, потім (NK)(N-K) нулів, потім K1K-1 одиниць. Застосовуючи принцип математичної індукції і використовуючи формулу для G(N)G(N), завершуємо доведення.

Тепер наше завдання — показати, що будь-які два сусідні коди також відрізняються рівно двома бітами; ми можемо зробити це, розглянувши наше рекурсивне рівняння для генерування кодів Грея. Припустимо, що для вмісту двох половин, утворених із G(N1)G(N-1), твердження істинне. Тепер нам потрібно довести, що нова послідовна пара, утворена на стику (через конкатенацію цих двох половин), теж є допустимою, тобто вони відрізняються рівно двома бітами.

Це можна зробити, оскільки ми знаємо останню маску першої половини і першу маску другої половини. Остання маска першої половини мала б вигляд 11, потім (NK1)(N-K-1) нулів, потім K1K-1 одиниць. А перша маска другої половини мала б вигляд 00, потім ішли б (NK2)(N-K-2) нулів, а потім KK одиниць. Отже, порівнюючи дві маски, ми знаходимо рівно два біти, що відрізняються.

Нижче наведено наївну реалізацію, що працює шляхом генерування всіх 2n2^{n} можливих підмножин і знаходження підмножин розміру KK.

int gray_code (int n) {
return n ^ (n >> 1);
}

int count_bits (int n) {
int res = 0;
for (; n; n >>= 1)
res += n & 1;
return res;
}

void all_combinations (int n, int k) {
for (int i = 0; i < (1 << n); i++) {
int cur = gray_code (i);
if (count_bits(cur) == k) {
for (int j = 0; j < n; j++) {
if (cur & (1 << j))
cout << j + 1;
}
cout << "\n";
}
}
}

Варто згадати, що існує ефективніша реалізація, яка вдається лише до побудови допустимих комбінацій і тому працює за O(N(NK))O\left(N \cdot \binom{N}{K}\right); однак вона є рекурсивною за своєю природою, і для менших значень NN вона, ймовірно, має більшу константу, ніж попередній розв'язок.

Реалізація виводиться з формули:

G(N,K)=0G(N1,K)1G(N1,K1)RG(N, K) = 0G(N-1, K) \cup 1G(N-1, K-1)^\text{R}

Ця формула отримується модифікацією загального рівняння для визначення коду Грея і працює, обираючи підпослідовність із відповідних елементів.

Її реалізація така:

vector<int> ans;

void gen(int n, int k, int idx, bool rev) {
if (k > n || k < 0)
return;

if (!n) {
for (int i = 0; i < idx; ++i) {
if (ans[i])
cout << i + 1;
}
cout << "\n";
return;
}

ans[idx] = rev;
gen(n - 1, k - rev, idx + 1, false);
ans[idx] = !rev;
gen(n - 1, k - !rev, idx + 1, true);
}

void all_combinations(int n, int k) {
ans.resize(n);
gen(n, k, 0, false);
}

Відеоматеріали