Біноміальні коефіцієнти
Біноміальні коефіцієнти — це кількість способів вибрати множину з елементів серед різних елементів без урахування порядку розташування цих елементів (тобто кількість невпорядкованих множин).
Біноміальні коефіцієнти також є коефіцієнтами в розкладі (так звана біноміальна теорема):
Вважають, що цю формулу, а також трикутник, який дозволяє ефективно обчислювати коефіцієнти, відкрив Блез Паскаль у XVII столітті. Проте вона була відома китайському математику Ян Хуею, який жив у XIII столітті. Можливо, її відкрив перський учений Омар Хайям. Більше того, індійський математик Пінгала, який жив раніше, у III ст. до н. е., отримав схожі результати. Заслуга Ньютона полягає в тому, що він узагальнив цю формулу на показники степеня, які не є натуральними.
- Чи треба порахувати кількість способів вибрати об'єктів з без урахування порядку (підмножини, сполучення)?
- Чи відповідь — комбінаторний підрахунок за модулем простого числа, де потрібні факторіали й обернені елементи?
- Чи об'єкти однакові й розподіляються по «скриньках»? (якщо так → Зірки і смуги; а для підрахунку з обмеженнями-перетинами → Принцип включення-виключення)
Обчислення
Аналітична формула для обчислення:
Цю формулу легко вивести із задачі про впорядковане розташування (кількість способів вибрати різних елементів серед різних елементів). Спочатку порахуймо кількість впорядкованих виборів елементів. Є способів вибрати перший елемент, спосіб вибрати другий елемент, способи вибрати третій елемент і так далі. У результаті отримуємо формулу для кількості впорядкованих розташувань: . Ми легко переходимо до невпорядкованих розташувань, помітивши, що кожному невпорядкованому розташуванню відповідає рівно впорядкованих ( — це кількість можливих перестановок елементів). Остаточну формулу отримуємо, поділивши на .
Рекурентна формула (яка пов'язана зі знаменитим «трикутником Паскаля»):
Її легко вивести за допомогою аналітичної формули.
Зауважимо, що для значення вважається рівним нулю.
Властивості
Біноміальні коефіцієнти мають багато різних властивостей. Ось найпростіші з них:
-
Правило симетрії:
-
Винесення множника:
-
Сума за :
-
Сума за :
-
Сума за і :
-
Сума квадратів:
-
Зважена сума:
-
Зв'язок із числами Фібоначчі:
Обчислення
Пряме обчислення за аналітичною формулою
Першу, пряму формулу дуже легко закодувати, але цей метод, найімовірніше, призведе до переповнення навіть для відносно невеликих значень і (навіть якщо відповідь повністю вміщується в якийсь тип даних, обчислення проміжних факторіалів може спричинити переповнення). Тому цей метод часто можна застосовувати лише з довгою арифметикою:
- C++
- Python
- TypeScript
- Go
int C(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;
return res;
}
def C(n: int, k: int) -> int:
# У стандартній бібліотеці є math.comb(n, k); тут відтворюємо алгоритм.
res = 1
for i in range(n - k + 1, n + 1):
res *= i
for i in range(2, k + 1):
res //= i # цілочислове ділення — проміжний результат завжди ділиться націло
return res
// Python-цілі необмежені, тож для відповідності використовуємо BigInt,
// бо проміжні факторіали швидко переповнюють number.
function C(n: bigint, k: bigint): bigint {
let res = 1n;
for (let i = n - k + 1n; i <= n; i++) res *= i;
for (let i = 2n; i <= k; i++) res /= i;
return res;
}
func C(n, k int64) int64 {
var res int64 = 1
for i := n - k + 1; i <= n; i++ {
res *= i
}
for i := int64(2); i <= k; i++ {
res /= i
}
return res
}
Покращена реалізація
Зауважимо, що в наведеній вище реалізації чисельник і знаменник мають однакову кількість множників (), кожен з яких більший або рівний 1. Тому ми можемо замінити наш дріб на добуток дробів, кожен з яких є дійсним числом. Проте на кожному кроці після множення поточної відповіді на кожен з наступних дробів відповідь усе одно залишатиметься цілою (це випливає з властивості винесення множника).
Реалізація на C++:
- C++
- Python
- TypeScript
- Go
int C(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);
}
def C(n: int, k: int) -> int:
res = 1.0
for i in range(1, k + 1):
res = res * (n - k + i) / i
# Додаємо 0.01 для компенсації накопиченої похибки чисел з рухомою комою.
return int(res + 0.01)
function C(n: number, k: number): number {
let res = 1;
for (let i = 1; i <= k; i++) res = (res * (n - k + i)) / i;
// 0.01 компенсує накопичену похибку чисел з рухомою комою.
return Math.trunc(res + 0.01);
}
func C(n, k int) int {
res := 1.0
for i := 1; i <= k; i++ {
res = res * float64(n-k+i) / float64(i)
}
// 0.01 компенсує накопичену похибку чисел з рухомою комою.
return int(res + 0.01)
}
Тут ми обережно зводимо число з рухомою комою до цілого, враховуючи, що через накопичені похибки воно може бути трохи меншим за справжнє значення (наприклад, замість ).
Трикутник Паскаля
Використовуючи рекурентне співвідношення, ми можемо побудувати таблицю біноміальних коефіцієнтів (трикутник Паскаля) і брати результат із неї. Перевага цього методу в тому, що проміжні результати ніколи не перевищують відповідь, а обчислення кожного нового елемента таблиці потребує лише одного додавання. Недолік — повільне виконання для великих і , якщо вам потрібне лише одне значення, а не вся таблиця (адже щоб обчислити , вам доведеться побудувати таблицю всіх , або принаймні до ). Часову складність можна вважати рівною .
Реалізація на C++:
- C++
- Python
- TypeScript
- Go
const int maxn = ...;
int C[maxn + 1][maxn + 1];
C[0][0] = 1;
for (int n = 1; 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];
}
maxn = ...
C = [[0] * (maxn + 1) for _ in range(maxn + 1)]
C[0][0] = 1
for n in range(1, maxn + 1):
C[n][0] = C[n][n] = 1
for k in range(1, n):
C[n][k] = C[n - 1][k - 1] + C[n - 1][k]
const maxn = /* ... */ 0;
const C: bigint[][] = Array.from({ length: maxn + 1 }, () =>
new Array<bigint>(maxn + 1).fill(0n)
);
C[0][0] = 1n;
for (let n = 1; n <= maxn; n++) {
C[n][0] = C[n][n] = 1n;
for (let k = 1; k < n; k++) C[n][k] = C[n - 1][k - 1] + C[n - 1][k];
}
const maxn = ... // ...
var C [maxn + 1][maxn + 1]int64
C[0][0] = 1
for n := 1; n <= maxn; n++ {
C[n][0], C[n][n] = 1, 1
for k := 1; k < n; k++ {
C[n][k] = C[n-1][k-1] + C[n-1][k]
}
}
Якщо вся таблиця значень не потрібна, достатньо зберігати лише два її останні рядки (поточний -й рядок і попередній -й).
Обчислення за
Нарешті, у деяких ситуаціях вигідно заздалегідь обчислити всі факторіали, щоб згодом отримувати будь-який потрібний біноміальний коефіцієнт лише двома діленнями. Це може бути корисним під час використання довгої арифметики, коли пам'ять не дозволяє попередньо обчислити весь трикутник Паскаля.
Обчислення біноміальних коефіцієнтів за модулем
Доволі часто трапляється задача обчислення біноміальних коефіцієнтів за деяким модулем .
Біноміальний коефіцієнт для малих
Раніше розглянутий підхід із трикутником Паскаля можна застосувати для обчислення всіх значень для досить малих , оскільки він потребує часової складності . Цей підхід працює з будь-яким модулем, бо використовуються лише операції додавання.
Біноміальний коефіцієнт за великим простим модулем
Формула для біноміальних коефіцієнтів така:
тож якщо ми хочемо обчислити її за деяким простим модулем , то отримуємо
Спершу ми попередньо обчислюємо всі факторіали за модулем аж до за час .
- C++
- Python
- TypeScript
- Go
factorial[0] = 1;
for (int i = 1; i <= MAXN; i++) {
factorial[i] = factorial[i - 1] * i % m;
}
factorial = [0] * (MAXN + 1)
factorial[0] = 1
for i in range(1, MAXN + 1):
factorial[i] = factorial[i - 1] * i % m
const factorial: bigint[] = new Array<bigint>(MAXN + 1).fill(0n);
factorial[0] = 1n;
for (let i = 1; i <= MAXN; i++) {
factorial[i] = (factorial[i - 1] * BigInt(i)) % m;
}
factorial := make([]int64, MAXN+1)
factorial[0] = 1
for i := int64(1); i <= MAXN; i++ {
factorial[i] = factorial[i-1] * i % m
}
А потім ми можемо обчислити біноміальний коефіцієнт за час .
- C++
- Python
- TypeScript
- Go
long long binomial_coefficient(int n, int k) {
return factorial[n] * inverse(factorial[k] * factorial[n - k] % m) % m;
}
def binomial_coefficient(n: int, k: int) -> int:
return factorial[n] * inverse(factorial[k] * factorial[n - k] % m) % m
function binomial_coefficient(n: number, k: number): bigint {
return (
(factorial[n] * inverse((factorial[k] * factorial[n - k]) % m)) % m
);
}
func binomial_coefficient(n, k int) int64 {
return factorial[n] * inverse(factorial[k]*factorial[n-k]%m) % m
}
Ми навіть можемо обчислити біноміальний коефіцієнт за час , якщо попередньо обчислимо обернені елементи всіх факторіалів за звичайним методом обчислення оберненого елемента, або навіть за час , використовуючи конгруенцію та метод обчислення всіх обернених елементів за .
- C++
- Python
- TypeScript
- Go
long long binomial_coefficient(int n, int k) {
return factorial[n] * inverse_factorial[k] % m * inverse_factorial[n - k] % m;
}
def binomial_coefficient(n: int, k: int) -> int:
return factorial[n] * inverse_factorial[k] % m * inverse_factorial[n - k] % m
function binomial_coefficient(n: number, k: number): bigint {
return (
(((factorial[n] * inverse_factorial[k]) % m) * inverse_factorial[n - k]) % m
);
}
func binomial_coefficient(n, k int) int64 {
return factorial[n] * inverse_factorial[k] % m * inverse_factorial[n-k] % m
}
Біноміальний коефіцієнт за модулем степеня простого числа
Тут ми хочемо обчислити біноміальний коефіцієнт за модулем деякого степеня простого числа, тобто для деякого простого . Якщо , то ми можемо застосувати той самий метод, що описаний у попередньому розділі. Але якщо , то принаймні один з факторіалів і не є взаємно простим з , а тому ми не можемо обчислити обернені елементи — вони не існують. Проте біноміальний коефіцієнт ми обчислити можемо.
Ідея така: Для кожного ми обчислюємо найбільший показник степеня такий, що ділить , тобто . Нехай — це число. І нехай . Тоді ми можемо записати біноміальний коефіцієнт так:
Цікаво те, що тепер вільне від простого дільника . Тому взаємно просте з m, і ми можемо обчислити обернені елементи за модулем для та .
Після попереднього обчислення всіх значень для і , що можна зробити ефективно за допомогою динамічного програмування за , ми можемо обчислити біноміальний коефіцієнт за час . Або попередньо обчислити всі обернені елементи та всі степені , і тоді обчислювати біноміальний коефіцієнт за .
Зауважимо: якщо , то , і біноміальний коефіцієнт дорівнює .
Біноміальний коефіцієнт за довільним модулем
Тепер обчислимо біноміальний коефіцієнт за деяким довільним модулем .
Нехай розклад на прості множники числа має вигляд . Ми можемо обчислити біноміальний коефіцієнт за модулем для кожного . Це дає нам різних конгруенцій. Оскільки всі модулі взаємно прості, ми можемо застосувати китайську теорему про остачі, щоб обчислити біноміальний коефіцієнт за модулем добутку модулів, що і є шуканим біноміальним коефіцієнтом за модулем .
Біноміальний коефіцієнт для великих та малого модуля
Коли занадто велике, розглянуті вище алгоритми зі складністю стають непрактичними. Проте якщо модуль малий, усе одно є способи обчислити .
Коли модуль простий, є 2 варіанти:
- Можна застосувати теорему Люка, яка розбиває задачу обчислення на задач вигляду , де . Якщо кожен зведений коефіцієнт обчислювати за допомогою попередньо обчислених факторіалів та обернених факторіалів, складність становить .
- Можна використати метод обчислення факторіала за модулем P, щоб отримати потрібні значення і та використати їх, як описано в розділі про модуль — степінь простого числа. Це займає .
Коли не просте, але вільне від квадратів, можна отримати прості множники , обчислити коефіцієнт за модулем кожного простого множника одним із наведених вище методів, а загальну відповідь отримати за китайською теоремою про остачі.
Коли не вільне від квадратів, замість теореми Люка можна застосувати узагальнення теореми Люка для степенів простих чисел.
Задачі для практики
- Codechef - Number of ways
- Codeforces - Curious Array
- LightOj - Necklaces
- HACKEREARTH: Binomial Coefficient
- SPOJ - Ada and Teams
- SPOJ - Greedy Walking
- UVa 13214 - The Robot's Grid
- SPOJ - Good Predictions
- SPOJ - Card Game
- SPOJ - Topper Rama Rao
- UVa 13184 - Counting Edges and Graphs
- Codeforces - Anton and School 2
- Codeforces - Bacterial Melee
- Codeforces - Points, Lines and Ready-made Titles
- SPOJ - The Ultimate Riddle
- CodeChef - Long Sandwich
- Codeforces - Placing Jinas