«Деление» по модулю - Алгоритмика
«Деление» по модулю

«Деление» по модулю

Часто в олимпиадных задачах требуется посчитать какие-то большие комбинаторные величины по простому модулю (чаще всего 109+710^9 + 7). Это делают для того, чтобы участникам не приходилось использовать длинную арифметику, и они могли сосредоточиться на самой задаче.

Обычные арифметические операции по модулю выполняются не сильно сложнее — просто нужно брать модули и заботиться о переполнении. Например:

c = (a + b) % mod;
c = (a - b + mod) % mod;
c = a * b % mod;

Но вот с делением возникают проблемы — мы не можем просто взять и поделить.

Например, 82=4\frac{8}{2} = 4, но

8mod52mod5=324 \frac{8 \bmod 5}{2 \bmod 5} = \frac{3}{2} \neq 4

Нужно найти некоторый элемент, который будет себя вести как 1a=a1\frac{1}{a} = a^{-1}, и вместо «деления» домножать на него. Такой элемент называется обратным по модулю mm. Для a=0a = 0 обратный по модулю элемент не определён, как и при обычном делении.

#Через бинарное возведение в степень

Малая теорема Ферма говорит, что для любого простого числа pp и любого целого числа aa,

apa(modp) a^p \equiv a \pmod p Теперь два раза «поделим» этот известный результат на aa: apa    ap11    ap2a1 a^p \equiv a \implies a^{p-1} \equiv 1 \implies a^{p-2} \equiv a^{-1}

Получается, что ap2a^{p-2} ведет себя как a1a^{-1} относительно умножения по модулю, что нам и нужно.

Посчитать ap2a^{p-2} можно за O(logp)O(\log p) бинарным возведением в степень.

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

Этот подход простой и быстрый, однако следует помнить, что он работает только для простых модулей.

В случае составных модулей, по теореме Эйлера, число aa нужно возводить в степень (ϕ(m)1)(\phi(m)-1), для чего нужно искать факторизацию.

#Через расширенный алгоритм Евклида

Расширенный алгоритм Евклида можно использовать для решения в целых числах уравнений вида

Ax+By=1 Ax + By = 1 Подставим в качестве AA и BB соответственно aa и mm: ax+my=1 ax + my = 1 Одним из решений уравнения и будет a1a^{-1}, потому что если взять уравнение по модулю mm, то получим ax+my=1    ax1    xa1(modm) ax + my = 1 \iff ax \equiv 1 \iff x \equiv a^{-1} \pmod m

Преимущества этого метода над возведением в степень:

  • Если обратное существует, то оно найдется даже если модуль не простой.
  • Алгоритм проще выполнять руками.
  • Алгоритм чуть быстрее, если его соптимизировать

Но лично автор почти всегда использует возведение в степень.

#Упрощенная реализация

Сначала приведем реализацию, а потом поймем, почему она работает:

int inv(int a, int m) {
    if (a == 1)
        return 1;
    return (1 - 1ll * inv(m % a, a) * m) / a + m;
}

Докажем по индукции, что функция действительно возвращает обратный элемент.

Базовый случай очевиден: 1111 \cdot 1 \equiv 1.

Во втором случае проверим правильность формулы:

  • (1f(mmoda,a)m)(1 - f(m \bmod a, a) \cdot m) делится на aa, так как f(mmoda,a)m1(moda)f(m \bmod a, a) \equiv m^{-1} \pmod a.
  • f(mmoda,a)ma\frac{f(m \bmod a, a) \cdot m}{a} делится на mm, так что итоговое выражение сравнимо с 1a=a1\frac{1}{a} = a^{-1} по модулю mm.

Почему ответ будет получаться в диапазоне от 00 до (m1)(m - 1), мы оставим читателю в качестве упражнения.

#Предподсчет обратных элементов

Чаще всего нам нужно искать обратный элемент в контексте комбинаторики.

Например, особенно часто нужно считать биномиальные коэффициенты, для чего в свою очередь нужно уметь обращать факториалы:

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

Простой способ — это предпосчитать обычные факториалы и каждый раз вызывать 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, то нам не жалко потратить лишние O(logm)O(\log m) операций, посчитав (a!)1(a!)^{-1}.

После этого обратный к (a1)!(a-1)! можно посчитать за O(1)O(1) по формуле:

(a1)!1=(a!)1a112(a1)(modp) (a-1)!^{-1} = (a!)^{-1} \cdot a \equiv \frac{1}{1 \cdot 2 \cdot \ldots \cdot (a-1)} \pmod p

Все остальные обратные факториалы можно таким же образом итеративно подсчитать из предыдущего.

// обычные факториалы:
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;

Также существует метод нахождения обратных для всех чисел от 11 до (p1)(p - 1), но так как обычно модули большие, он не часто применим.

#Почему 109+710^9+7?

Несколько причин:

  1. Это выражение довольно легко вбивать (1e9+7).
  2. Простое число.
  3. Достаточно большое.
  4. int не переполняется при сложении.
  5. long long не переполняется при умножении.

Кстати, 109+910^9 + 9 обладает всеми теми же свойствами. Иногда используют и его.

Иногда можно встретить 998244353998244353. Оно обладает всеми свойствами кроме первого, но зато имеет применение в одном из вариантов быстрого преобразования Фурье. Его иногда добавляют даже в задачи, которые к нему не относятся, чтобы не раскрывать участникам тему.