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

Факторіал за модулем pp

У деяких випадках доводиться розглядати складні формули за модулем деякого простого числа pp, які містять факторіали і в чисельнику, і в знаменнику, як, наприклад, у формулі для біноміальних коефіцієнтів. Ми розглянемо випадок, коли pp відносно невелике. Ця задача має сенс лише тоді, коли факторіали присутні і в чисельнику, і в знаменнику дробів. Інакше p!p! та наступні доданки перетворяться на нуль. Але в дробах множники pp можуть скоротитися, і результат буде ненульовим за модулем pp.

Отже, формально задача звучить так: ви хочете обчислити n!modpn! \bmod p, не враховуючи всіх множників, кратних pp, які з'являються у факторіалі. Уявіть, що ви виписуєте розклад n!n! на прості множники, прибираєте всі множники pp і обчислюєте добуток за модулем pp. Цей модифікований факторіал ми позначатимемо як n!%pn!_{\%p}. Наприклад, 7!%p1213452672mod37!_{\%p} \equiv 1 \cdot 2 \cdot \underbrace{1}_{3} \cdot 4 \cdot 5 \underbrace{2}_{6} \cdot 7 \equiv 2 \bmod 3.

Уміння ефективно обчислювати цей модифікований факторіал дозволяє нам швидко обчислювати значення різноманітних комбінаторних формул (наприклад, біноміальних коефіцієнтів).

Коли підходить цей алгоритм?
  • Чи потрібен n!modpn! \bmod p для простого pp, причому факторіали стоять і в чисельнику, і в знаменнику формули (інакше множники pp занулять результат)?
  • Чи модуль pp відносно малий (O(p)O(p) на блок прийнятне), а nn — велике? Складність O(plogpn)O(p \log_p n).
  • Якщо вам потрібен степінь, у якому pp входить у n!n! (а не модифікований факторіал) — (→ Степінь дільника факторіала)?

Алгоритм

Запишемо цей модифікований факторіал у явному вигляді.

n!%p=123(p2)(p1)1p(p+1)(p+2)(2p1)22p (2p+1)(p21)1p2(p2+1)n(modp)=123(p2)(p1)1p12(p1)22p12 (p1)1p212(nmodp)(modp)\begin{align} n!_{\%p} &= 1 \cdot 2 \cdot 3 \cdot \ldots \cdot (p-2) \cdot (p-1) \cdot \underbrace{1}_{p} \cdot (p+1) \cdot (p+2) \cdot \ldots \cdot (2p-1) \cdot \underbrace{2}_{2p} \\\ & \quad \cdot (2p+1) \cdot \ldots \cdot (p^2-1) \cdot \underbrace{1}_{p^2} \cdot (p^2 +1) \cdot \ldots \cdot n \pmod{p} \\\\ &= 1 \cdot 2 \cdot 3 \cdot \ldots \cdot (p-2) \cdot (p-1) \cdot \underbrace{1}_{p} \cdot 1 \cdot 2 \cdot \ldots \cdot (p-1) \cdot \underbrace{2}_{2p} \cdot 1 \cdot 2 \\\ & \quad \cdot \ldots \cdot (p-1) \cdot \underbrace{1}_{p^2} \cdot 1 \cdot 2 \cdot \ldots \cdot (n \bmod p) \pmod{p} \end{align}

Чітко видно, що факторіал розбивається на кілька блоків однакової довжини, окрім останнього.

n!%p=123(p2)(p1)11st123(p2)(p1)22nd123(p2)(p1)1pth12(nmodp)tail(modp).\begin{align} n!_{\%p}&= \underbrace{1 \cdot 2 \cdot 3 \cdot \ldots \cdot (p-2) \cdot (p-1) \cdot 1}_{1\text{st}} \cdot \underbrace{1 \cdot 2 \cdot 3 \cdot \ldots \cdot (p-2) \cdot (p-1) \cdot 2}_{2\text{nd}} \cdot \ldots \\\\ & \cdot \underbrace{1 \cdot 2 \cdot 3 \cdot \ldots \cdot (p-2) \cdot (p-1) \cdot 1}_{p\text{th}} \cdot \ldots \cdot \quad \underbrace{1 \cdot 2 \cdot \cdot \ldots \cdot (n \bmod p)}_{\text{tail}} \pmod{p}. \end{align}

Основну частину блоків легко порахувати — це просто (p1)! mod p(p-1)!\ \mathrm{mod}\ p. Ми можемо обчислити це програмно або просто застосувати теорему Вілсона, яка стверджує, що (p1)!modp=1(p-1)! \bmod p = -1 для будь-якого простого pp.

Таких блоків у нас рівно np\lfloor \frac{n}{p} \rfloor, тому нам потрібно піднести 1-1 до степеня np\lfloor \frac{n}{p} \rfloor. Це можна зробити за логарифмічний час за допомогою бінарного піднесення до степеня; проте можна також помітити, що результат чергуватиметься між 1-1 і 11, тож нам достатньо лише поглянути на парність показника й помножити на 1-1, якщо парність непарна. А замість множення можна просто відняти поточний результат від pp.

Значення останнього часткового блока можна обчислити окремо за O(p)O(p).

Залишається лише останній елемент кожного блока. Якщо приховати вже опрацьовані елементи, то можна побачити такий патерн:

n!%p=12(p1)112n!_{\%p} = \underbrace{ \ldots \cdot 1 } \cdot \underbrace{ \ldots \cdot 2} \cdot \ldots \cdot \underbrace{ \ldots \cdot (p-1)} \cdot \underbrace{ \ldots \cdot 1 } \cdot \underbrace{ \ldots \cdot 1} \cdot \underbrace{ \ldots \cdot 2} \cdots

Це знову модифікований факторіал, лише значно меншої розмірності. Це n/p!%p\lfloor n / p \rfloor !_{\%p}.

Отже, під час обчислення модифікованого факторіала n ⁣%pn\!_{\%p} ми виконали O(p)O(p) операцій, і нам залишилося обчислити n/p!%p\lfloor n / p \rfloor !_{\%p}. Маємо рекурентну формулу. Глибина рекурсії становить O(logpn)O(\log_p n), а отже, повна асимптотична поведінка алгоритму — O(plogpn)O(p \log_p n).

Зауважимо, що якщо попередньо обчислити факторіали 0!, 1!, 2!, , (p1)!0!,~ 1!,~ 2!,~ \dots,~ (p-1)! за модулем pp, то складність становитиме лише O(logpn)O(\log_p n).

Реалізація

Нам не потрібна рекурсія, бо це випадок хвостової рекурсії, а отже, її легко реалізувати через ітерацію. У наведеній нижче реалізації ми попередньо обчислюємо факторіали 0!, 1!, , (p1)!0!,~ 1!,~ \dots,~ (p-1)!, а отже, час роботи становить O(p+logpn)O(p + \log_p n). Якщо вам потрібно викликати функцію кілька разів, то попередні обчислення можна винести за межі функції й виконувати обчислення n!%pn!_{\%p} за час O(logpn)O(\log_p n).

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;
}

Як альтернатива, якщо у вас обмаль пам'яті й ви не можете дозволити собі зберігати всі факторіали, ви також можете просто запам'ятати ті факторіали, які вам потрібні, відсортувати їх, а потім обчислити їх за один прохід, обчислюючи факторіали 0!, 1!, 2!, , (p1)!0!,~ 1!,~ 2!,~ \dots,~ (p-1)! у циклі без явного їх збереження.

Кратність pp

Якщо ми хочемо обчислити біноміальний коефіцієнт за модулем pp, то нам додатково потрібна кратність pp у nn, тобто кількість разів, скільки pp зустрічається у розкладі nn на прості множники, або кількість разів, скільки ми викреслювали pp під час обчислення модифікованого факторіала.

Формула Лежандра дає нам спосіб обчислити це за час O(logpn)O(\log_p n). Ця формула дає кратність νp\nu_p як:

νp(n!)=i=1npi\nu_p(n!) = \sum_{i=1}^{\infty} \left\lfloor \frac{n}{p^i} \right\rfloor

Отже, отримуємо таку реалізацію:

int multiplicity_factorial(int n, int p) {
int count = 0;
do {
n /= p;
count += n;
} while (n);
return count;
}

Цю формулу можна дуже легко довести, використовуючи ті самі ідеї, що й у попередніх розділах. Приберемо всі елементи, які не містять множника pp. Залишиться n/p\lfloor n/p \rfloor елементів. Якщо прибрати множник pp з кожного з них, ми отримаємо добуток 12n/p=n/p!1 \cdot 2 \cdots \lfloor n/p \rfloor = \lfloor n/p \rfloor !, і ми знову маємо рекурсію.

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