Часто в олимпиадных задачах требуется посчитать какие-то большие комбинаторные величины по простому модулю (чаще всего ). Это делают для того, чтобы участникам не приходилось использовать длинную арифметику, и они могли сосредоточиться на самой задаче.
Обычные арифметические операции по модулю выполняются не сильно сложнее — просто нужно брать модули и заботиться о переполнении. Например:
c = (a + b) % mod;
c = (a - b + mod) % mod;
c = a * b % mod;
Но вот с делением возникают проблемы — мы не можем просто взять и поделить.
Например, , но
Нужно найти некоторый элемент, который будет себя вести как , и вместо «деления» домножать на него. Такой элемент называется обратным по модулю . Для обратный по модулю элемент не определён, как и при обычном делении.
#Через бинарное возведение в степень
Малая теорема Ферма говорит, что для любого простого числа и любого целого числа ,
Теперь два раза «поделим» этот известный результат на :Получается, что ведет себя как относительно умножения по модулю, что нам и нужно.
Посчитать можно за бинарным возведением в степень.
const int mod = 1e9 + 7;
// бинарное возведение в степень по модулю
int binpow(int a, int n) {
int res = 1;
while (n != 0) {
if (n & 1)
res = res * a % mod;
a = a * a % mod;
n >>= 1;
}
return res;
}
// находит обратный элемент как a^(p-2)
int inv(int x) {
return binpow(x, mod - 2);
}
Этот подход простой и быстрый, однако следует помнить, что он работает только для простых модулей.
В случае составных модулей, по теореме Эйлера, число нужно возводить в степень , для чего нужно искать факторизацию.
#Через расширенный алгоритм Евклида
Расширенный алгоритм Евклида можно использовать для решения в целых числах уравнений вида
Подставим в качестве и соответственно и : Одним из решений уравнения и будет , потому что если взять уравнение по модулю , то получимПреимущества этого метода над возведением в степень:
- Если обратное существует, то оно найдется даже если модуль не простой.
- Алгоритм проще выполнять руками.
- Алгоритм чуть быстрее, если его соптимизировать
Но лично автор почти всегда использует возведение в степень.
#Упрощенная реализация
Сначала приведем реализацию, а потом поймем, почему она работает:
int inv(int a, int m) {
if (a == 1)
return 1;
return (1 - 1ll * inv(m % a, a) * m) / a + m;
}
Докажем по индукции, что функция действительно возвращает обратный элемент.
Базовый случай очевиден: .
Во втором случае проверим правильность формулы:
- делится на , так как .
- делится на , так что итоговое выражение сравнимо с по модулю .
Почему ответ будет получаться в диапазоне от до , мы оставим читателю в качестве упражнения.
#Предподсчет обратных элементов
Чаще всего нам нужно искать обратный элемент в контексте комбинаторики.
Например, особенно часто нужно считать биномиальные коэффициенты, для чего в свою очередь нужно уметь обращать факториалы:
Простой способ — это предпосчитать обычные факториалы и каждый раз вызывать inv
один или два раза:
int t[maxn]; // факториалы, можно предподсчитать простым циклом
int c(int n, int k) {
return t[n] * inv(t[k]) % mod * inv(t[n - k]) % mod;
}
// или, почти в два раза быстрее:
int c(int n, int k) {
return t[n] * inv(t[k] * t[n - k] % mod) % mod;
}
Однако это добавит лишний логарифм в асимптотику в нередком случае, когда какая-то комбинаторная формула лежит внутри горячего цикла. Поэтому имеет смысл предподсчитать и частые обратные элементы.
#Обратные факториалы
Если у нас уже написан inv
, то нам не жалко потратить лишние операций, посчитав .
После этого обратный к можно посчитать за по формуле:
Все остальные обратные факториалы можно таким же образом итеративно подсчитать из предыдущего.
// обычные факториалы:
int f[maxn];
f[0] = 1;
for (int i = 1; i < maxn; i++)
f[i] = i * f[i - 1] % mod;
// обратные факториалы:
int r[maxn];
r[maxn - 1] = inv(f[maxn - 1])
for (int i = maxn - 1; i >= 1; i--)
r[i-1] = r[i] * i % mod;
Также существует метод нахождения обратных для всех чисел от до , но так как обычно модули большие, он не часто применим.
#Почему ?
Несколько причин:
- Это выражение довольно легко вбивать (
1e9+7
). - Простое число.
- Достаточно большое.
int
не переполняется при сложении.long long
не переполняется при умножении.
Кстати, обладает всеми теми же свойствами. Иногда используют и его.
Иногда можно встретить . Оно обладает всеми свойствами кроме первого, но зато имеет применение в одном из вариантов быстрого преобразования Фурье. Его иногда добавляют даже в задачи, которые к нему не относятся, чтобы не раскрывать участникам тему.