Сравнения и китайская теорема
Когда уравнение ax ≡ b (mod m) разрешимо и сколько у него корней. Восстановление числа по остаткам и то, зачем это на самом деле нужно.
6 мин
Запись читается « сравнимо с по модулю » и означает, что делится на . Со сравнениями можно обращаться почти как с равенствами: складывать, вычитать, умножать. Проблемы начинаются на делении — и вот с них всё интересное и начинается.
Линейное сравнение
Задача: найти все , для которых
Это в точности диофантово уравнение , только записанное короче. Значит, работает расширенный Евклид, и сразу известен критерий:
Решение существует тогда и только тогда, когда делится на .
Дальше — то, чего в обычном уравнении не было. Решений не одно, а ровно штук по модулю , и идут они с шагом :
// все решения a*x = b (mod m); пустой вектор, если решений нет
vector<long long> solveCongruence(long long a, long long b, long long m) {
long long x, y;
long long g = gcdExt(((a % m) + m) % m, m, x, y);
if (b % g != 0) return {};
long long x0 = ((__int128)x * (b / g)) % m;
x0 = ((x0 % m) + m) % m;
vector<long long> answer;
for (long long k = 0; k < g; k++) answer.push_back((x0 + k * (m / g)) % m);
return answer;
}
Два места, где легко ошибиться. Первое — __int128 при домножении: может быть порядка , и произведение с переполнит long long. Второе — двойная нормализация ((x % m) + m) % m: остаток от отрицательного числа в C++ отрицателен, а нам нужен представитель из .
Частный случай — это ровно обратный элемент: решение единственно и равно .
Система сравнений
Теперь несколько условий сразу:
Классическая формулировка — «число при делении на 3 даёт остаток 2, при делении на 5 — остаток 3; найдите его». Ответ: система либо не имеет решений, либо её решения образуют один класс вычетов по модулю .
Критерий разрешимости: .
Логика простая. Ищем в виде . Подставляем во второе условие: , то есть . Это опять линейное сравнение — и оно разрешимо ровно тогда, когда делится на .
// x = r1 (mod m1), x = r2 (mod m2) -> x = r (mod m)
bool crt(long long r1, long long m1, long long r2, long long m2,
long long &r, long long &m) {
long long p, q;
long long g = gcdExt(m1, m2, p, q);
if ((r2 - r1) % g != 0) return false;
m = m1 / g * m2; // это lcm(m1, m2)
__int128 t = (__int128)((r2 - r1) / g) * p % (m2 / g);
__int128 value = r1 + t * m1;
r = (long long)(((value % m) + m) % m);
return true;
}
Больше двух сравнений склеиваются по очереди: свернули первые два в одно, к результату приклеили третье, и так далее. Если на каком-то шаге вернулся false — вся система несовместна.
flowchart LR
A["x = 2 (mod 3)"] --> M1{"склейка"}
B["x = 3 (mod 5)"] --> M1
M1 --> C["x = 8 (mod 15)"]
C --> M2{"склейка"}
D["x = 2 (mod 7)"] --> M2
M2 --> E["x = 23 (mod 105)"]
Китайская теорема об остатках
Когда модули попарно взаимно просты, условие разрешимости выполняется автоматически, и утверждение приобретает красивую форму.
Пусть , модули попарно взаимно просты. Тогда отображение
является взаимно однозначным между и наборами остатков. Иначе говоря, число до полностью определяется своими остатками — и любой набор остатков достижим.
Считать это можно тем же склеиванием, а можно прямой формулой: , где , а обратный берётся по модулю . На практике склеивание удобнее — оно не требует взаимной простоты и не переполняется так легко.
Зачем это нужно
Обход переполнения. Задача просит ответ по модулю , а промежуточные произведения не помещаются даже в __int128. Считаем ответ по нескольким простым модулям около и склеиваем — на каждом шаге умножение честно влезает в 64 бита.
Хеширование строк. Один модуль ломается подобранным тестом. Два независимых модуля через КТО эквивалентны одному модулю порядка их произведения — вероятность коллизии падает до квадрата.
Периодичность. «Событие A происходит раз в дней, событие B — раз в ; когда они совпадут» — это система сравнений, и ответ живёт с периодом .
Разложение задачи по простым. Уравнение по составному модулю решается отдельно по каждому , а решения потом склеиваются. Число решений при этом перемножается.
Про малую теорему Ферма и Эйлера
Рядом стоят два факта, которые постоянно нужны вместе со сравнениями.
Малая теорема Ферма: если простое и не делится на , то . Отсюда обратный элемент как .
Теорема Эйлера обобщает: если , то .
Практическое следствие — понижение показателя. Считая по модулю при гигантском (например, заданном строкой из миллиона цифр), показатель берут по модулю , а не по модулю . Брать показатель по модулю — распространённая и молчаливая ошибка: формула выглядит симметрично, но она неверна.