Факторіал за модулем
У деяких випадках доводиться розглядати складні формули за модулем деякого простого числа , які містять факторіали і в чисельнику, і в знаменнику, як, наприклад, у формулі для біноміальних коефіцієнтів. Ми розглянемо випадок, коли відносно невелике. Ця задача має сенс лише тоді, коли факторіали присутні і в чисельнику, і в знаменнику дробів. Інакше та наступні доданки перетворяться на нуль. Але в дробах множники можуть скоротитися, і результат буде ненульовим за модулем .
Отже, формально задача звучить так: ви хочете обчислити , не враховуючи всіх множників, кратних , які з'являються у факторіалі. Уявіть, що ви виписуєте розклад на прості множники, прибираєте всі множники і обчислюєте добуток за модулем . Цей модифікований факторіал ми позначатимемо як . Наприклад, .
Уміння ефективно обчислювати цей модифікований факторіал дозволяє нам швидко обчислювати значення різноманітних комбінаторних формул (наприклад, біноміальних коефіцієнтів).
- Чи потрібен для простого , причому факторіали стоять і в чисельнику, і в знаменнику формули (інакше множники занулять результат)?
- Чи модуль відносно малий ( на блок прийнятне), а — велике? Складність .
- Якщо вам потрібен степінь, у якому входить у (а не модифікований факторіал) — (→ Степінь дільника факторіала)?
Алгоритм
Запишемо цей модифікований факторіал у явному вигляді.
Чітко видно, що факторіал розбивається на кілька блоків однакової довжини, окрім останнього.
Основну частину блоків легко порахувати — це просто . Ми можемо обчислити це програмно або просто застосувати теорему Вілсона, яка стверджує, що для будь-якого простого .
Таких блоків у нас рівно , тому нам потрібно піднести до степеня . Це можна зробити за логарифмічний час за допомогою бінарного піднесення до степеня; проте можна також помітити, що результат чергуватиметься між і , тож нам достатньо лише поглянути на парність показника й помножити на , якщо парність непарна. А замість множення можна просто відняти поточний результат від .
Значення останнього часткового блока можна обчислити окремо за .
Залишається лише останній елемент кожного блока. Якщо приховати вже опрацьовані елементи, то можна побачити такий патерн:
Це знову модифікований факторіал, лише значно меншої розмірності. Це .
Отже, під час обчислення модифікованого факторіала ми виконали операцій, і нам залишилося обчислити . Маємо рекурентну формулу. Глибина рекурсії становить , а отже, повна асимптотична поведінка алгоритму — .
Зауважимо, що якщо попередньо обчислити факторіали за модулем , то складність становитиме лише .
Реалізація
Нам не потрібна рекурсія, бо це випадок хвостової рекурсії, а отже, її легко реалізувати через ітерацію. У наведеній нижче реалізації ми попередньо обчислюємо факторіали , а отже, час роботи становить . Якщо вам потрібно викликати функцію кілька разів, то попередні обчислення можна винести за межі функції й виконувати обчислення за час .
- C++
- Python
- TypeScript
- Go
int factmod(int n, int p) {
vector<int> f(p);
f[0] = 1;
for (int i = 1; i < p; i++)
f[i] = f[i-1] * i % p;
int res = 1;
while (n > 1) {
if ((n/p) % 2)
res = p - res;
res = res * f[n%p] % p;
n /= p;
}
return res;
}
def factmod(n: int, p: int) -> int:
# Попередньо обчислюємо 0!, 1!, ..., (p-1)! за модулем p
f = [1] * p
for i in range(1, p):
f[i] = f[i - 1] * i % p
res = 1
while n > 1:
# Знак (-1)^(n//p) за теоремою Вілсона: чергуємо віднімання від p
if (n // p) % 2:
res = p - res
res = res * f[n % p] % p
n //= p
return res
function factmod(n: number, p: number): number {
// Попередньо обчислюємо 0!, 1!, ..., (p-1)! за модулем p
const f = new Array<number>(p);
f[0] = 1;
for (let i = 1; i < p; i++) {
f[i] = (f[i - 1] * i) % p;
}
let res = 1;
while (n > 1) {
// Знак (-1)^(n/p) за теоремою Вілсона: чергуємо віднімання від p
if (Math.floor(n / p) % 2) {
res = p - res;
}
res = (res * f[n % p]) % p;
n = Math.floor(n / p);
}
return res;
}
func factmod(n, p int) int {
// Попередньо обчислюємо 0!, 1!, ..., (p-1)! за модулем p
f := make([]int, p)
f[0] = 1
for i := 1; i < p; i++ {
f[i] = f[i-1] * i % p
}
res := 1
for n > 1 {
// Знак (-1)^(n/p) за теоремою Вілсона: чергуємо віднімання від p
if (n/p)%2 == 1 {
res = p - res
}
res = res * f[n%p] % p
n /= p
}
return res
}
Як альтернатива, якщо у вас обмаль пам'яті й ви не можете дозволити собі зберігати всі факторіали, ви також можете просто запам'ятати ті факторіали, які вам потрібні, відсортувати їх, а потім обчислити їх за один прохід, обчислюючи факторіали у циклі без явного їх збереження.
Кратність
Якщо ми хочемо обчислити біноміальний коефіцієнт за модулем , то нам додатково потрібна кратність у , тобто кількість разів, скільки зустрічається у розкладі на прості множники, або кількість разів, скільки ми викреслювали під час обчислення модифікованого факторіала.
Формула Лежандра дає нам спосіб обчислити це за час . Ця формула дає кратність як:
Отже, отримуємо таку реалізацію:
- C++
- Python
- TypeScript
- Go
int multiplicity_factorial(int n, int p) {
int count = 0;
do {
n /= p;
count += n;
} while (n);
return count;
}
def multiplicity_factorial(n: int, p: int) -> int:
count = 0
while True:
n //= p
count += n
if not n: # аналог циклу do-while
break
return count
function multiplicityFactorial(n: number, p: number): number {
let count = 0;
do {
n = Math.floor(n / p);
count += n;
} while (n);
return count;
}
func multiplicityFactorial(n, p int) int {
count := 0
for { // аналог циклу do-while
n /= p
count += n
if n == 0 {
break
}
}
return count
}
Цю формулу можна дуже легко довести, використовуючи ті самі ідеї, що й у попередніх розділах. Приберемо всі елементи, які не містять множника . Залишиться елементів. Якщо прибрати множник з кожного з них, ми отримаємо добуток , і ми знову маємо рекурсію.