Skip to main content

Біноміальні коефіцієнти

Визначення

Біномиальним коефіцієнтом CnkC_n^k називається кількість способів вибрати набір kk елементів з nn різних елементів без врахування порядку розташування цих елементів (тобто кількість невпрорядкованих наборів).

Також біноміальні коефіцієнти - це кофіцієнти в розкладі (a+b)n(a+b)^n (біном Ньютона):

(a+b)n=Cn0an+Cn1an1b+Cn2an2b2++Cnkankbk++Cnnbn(a+b)^n = C_n^0 a^n + C_n^1 a^{n-1} b + C_n^2 a^{n-2} b^2 + \ldots + C_n^k a^{n-k} b^k + \ldots + C_n^n b^n

Вважається, що цю формулу, як і трикутник, що дозволяє ефективно знаходити ці коефіцієнти, відкрив Блез Паскаль (Blaise Pascal), що жив у 17 ст. Тим не менш, вона була відома ще китайскому математику Яну Хуею (Yang Hui), що жив у 13 ст. Можливо, її відкрив перський вчений Омар Хаям (Omar Khayyam). Більше того, індійський математик Пінгала (Pingala), що жив ще у 3 ст. до н.е., отримав близькі результати. Заслуга ж Ньютона полягає в тому, що він узагальнив цю формулу для степенів, що не є натуральними.

Обчислення

Аналітична формула для обчислення:

Cnk=n!k!(nk)!C_n^k = \frac{n!}{k! (n-k)!}

Цю формулу легко вивести із задачі про невпорядковану вибірку (кількість способів невпрорядковано вибрати kk елементів з nn елементів). Спочатку порахуємо кількість впрорядкованих вибірок. Вибрати перший елемент є nn способів, другий - n1n-1, третій - n2n-2, і так далі. В результаті для кількості впорядкованих вибірок отримуємо формулу: n(n1)(n2)(nk+1)=n!(nk)!n(n-1)(n-2) \ldots (n-k+1) = \frac{n!}{(n-k)!}. До невпрорядкованих вибірок легко перейти, якщо замітити, що кожній невпорядкованій вибірці відповідає рівно k!k! впорядкованих (тобто це кількість різних перестановок kk елементів). В результаті, при діленні n!(nk)!\frac{n!}{(n-k)!} на k!k!, ми і отримуємо шукану формулу.

Рекурсивна формула (з якою вз'язаний славнозвісний "трикутник Паскаля"):

Cnk=Cn1k1+Cn1kC_n^k = C_{n-1}^{k-1} + C_{n-1}^k

Її легко вивести через попередню формулу.

Варто замітити, що при n<kn<k значення CnkC_n^k завжди рівне нулю.

Властивості

Біноміальні коефіцієнти мають безліч різних властивостей, наведемо найбільш прості з них:

  • Правило симетрії:
Cnk=CnnkC_n^k = C_n^{n-k}
  • Вніс-виніс:
Cnk=nkCn1k1C_n^k = \frac{n}{k} C_{n-1}^{k-1}
  • Сума по kk:
k=0nCnk=2n\sum_{k=0}^n C_n^k = 2^n
  • Сума по nn:
m=0nCmk=Cn+1k+1\sum_{m=0}^n C_m^k = C_{n+1}^{k+1}
  • Сума по nn і kk:
k=0mCn+kk=Cn+m+1m\sum_{k=0}^{m} C_{n+k}^k = C_{n+m+1}^m
  • Сума квадратів:
(Cn0)2+(Cn1)2++(Cnn)2=C2nn(C_n^0)^2 + (C_n^1)^2 + \ldots + (C_n^n)^2 = C_{2n}^n
  • Зважена сума:
1Cn1+2Cn2++nCnn=n2n11 C_n^1 + 2 C_n^2 + \ldots + n C_n^n = n 2^{n-1}
Cn0+Cn11++Cnkk++C0n=Fn+1C_n^0 + C_{n-1}^1 + \ldots + C_{n-k}^k + \ldots + C_0^n = F_{n+1}

Реалізація

Обчислення за допомогою аналітичної формули

Обчислення за допомогою аналітичної формули легко програмується, однак цей спосіб схильний до переповнення навіть при відносно невеликих значениях nn та kk (навіть якщо відповідь цілком поміщається в деякий тип даних, обчислення проміжних факторіалів може привести до переповненню). Тому дуже часто цей спосіб можна застосовувати тільки разом з довгою арифметикою:

int binomial_coefficient(int n, int k) {
int res = 1;
for (int i = n-k+1; i <= n; i++) {
res *= i;
}
for (int i = 2; i <= k; i++) {
res /= i;
}
}

Покращена реалізація

Можна помітити, що в наведеній вище реалізації у чисельнику і знаменнику однакова кількість множників (kk), кожний з яких не менший одиниці. Тому можна замінити наш дріб на добуток kk дробів. Однак, можна замітити, що після домножения поточної відповіді на кожен наступний дріб все рівно отримуватимемо ціле число (випливає з властивості "вніс-виніс"). Тому отримуємо таку реалізацію:

int binomial_coefficient(int n, int k) {
double res = 1;
for (int i = 1; i <= k; i++) {
res = res * (n-k+i) / i;
}
return (int) (res + 0.01);
}

TODO: rewrite without double?

Вкінці перетворюємо дробове число до цілого, враховуючи, що через накопичення похибок воно може виявитися трохи меншим шуканого значення (наприклад, 2.999992.99999 замість 33).

Трикутник Паскаля

Використавши рекуррентне співвідношення можна побудувати таблицю біноміальних коефіцієнтів (фактично, трикутник Паскаля), і з неї брати результат. Перевага цього методу в тому, що проміжні результати ніколи не є більшими за шукане значення, і для обчислення кожного нового елементу таблиці треба всього лише одне додавання. Недоліком є повільна робота при великих nn та kk, коли насправді таблица не потрібна, а потрібне лише одне шукане значення (тому що для обчислення CnkC_n^k знадобиться побудувати таблицю для всіх Cij,  1in,  1jnC_i^j,\ \ 1 \le i \le n,\ \ 1 \le j \le n, або хоча б до 1jmin(i,2k)1 \le j \le \min(i,2k)).

const int maxn = ...;
int C[maxn+1][maxn+1];
for (int n = 0; n <= maxn; n++) {
C[n][0] = C[n][n] = 1;
for (int k = 1; k < n; k++) {
C[n][k] = C[n-1][k-1] + C[n-1][k];
}
}

Якщо вся таблица значень не потрібна, то достатньо зберігати від неї тільки два рядки (поточний - nn-ий і попередній - n1n-1-ий).

Обчислення за O(1)O(1)

У деяких ситуациях виявляється вигідно заздалегідь обрахувати значення всіх факторіалів для того, щоб будь-який необхідний біноміальний коефіцієнт можна було обрахувати за допомогою лише двох ділень. Такий підхід може бути корисним при використанні довгої арифметики, коли пам'ять не дозваляє обрахувати зазделегідь весь трикутник Паскаля, або ж коли потрібно проводити розрахунки за деяким простим модулем (якщо модуль не простий, то виникають труднощі при діленні чисельника на знаменник; їх можна побороти, якщо факторизувати модуль і зберігати всі числа у вигляді векторів із степенями цих простих; див. розділ про довгу арифметику у факторизованому вигляді.

Застосування

TODO: add applications

Задачі

TODO: add problems