Простота и разложение больших чисел
Что делать, когда число до 10^18 и перебор до корня уже не проходит: тест Миллера—Рабина и ро-алгоритм Полларда.
6 мин
Перебор делителей до корня проверяет простоту за . Для до это миллион операций — прекрасно. Для до это миллиард на каждый запрос — уже нет.
Существуют алгоритмы, которые справляются с 64-битными числами практически мгновенно. Оба вероятностные по природе, но для нашего диапазона их удаётся сделать полностью детерминированными.
Тест Ферма и почему он не работает
Начнём с наивной идеи. Малая теорема Ферма говорит: если простое, то для любого , не кратного .
Значит, можно взять несколько случайных и проверить. Не выполнилось — число точно составное. Выполнилось — вроде бы простое.
Ловушка в том, что существуют числа Кармайкла — составные, для которых сравнение выполняется при всех взаимно простых . Наименьшее из них , дальше , . Их бесконечно много, и тест Ферма на них ошибается всегда, сколько баз ни бери.
Миллер—Рабин
Тест Миллера—Рабина усиливает проверку одним наблюдением: у единицы по простому модулю ровно два квадратных корня, и . Составной модуль обычно даёт лишние корни, и на этом попадается.
Представим с нечётным . Для честного простого последовательность
обязана либо начинаться с , либо где-то содержать . Если ни того, ни другого — составное, и это доказано, а не предположено.
long long mulmod(long long a, long long b, long long m) {
return (__int128)a * b % m;
}
long long powmod(long long a, long long e, long long m) {
long long r = 1; a %= m;
while (e) { if (e & 1) r = mulmod(r, a, m); a = mulmod(a, a, m); e >>= 1; }
return r;
}
bool isPrime(long long n) {
if (n < 2) return false;
for (long long p : {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37})
if (n % p == 0) return n == p;
long long d = n - 1;
int s = 0;
while (d % 2 == 0) { d /= 2; s++; }
for (long long a : {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37}) {
long long x = powmod(a, d, n);
if (x == 1 || x == n - 1) continue;
bool composite = true;
for (int i = 1; i < s; i++) {
x = mulmod(x, x, n);
if (x == n - 1) { composite = false; break; }
}
if (composite) return false;
}
return true;
}
Про набор баз стоит сказать отдельно. Первые двенадцать простых — — доказанно дают верный ответ для всех . То есть для всего диапазона long long этот тест не вероятностный, а точный. Случайные базы брать не нужно.
Меньшие наборы тоже известны: до хватает баз ; до — всего . А вот наивный набор на бо́льших числах ошибается: наименьший контрпример — .
mulmod через __int128 обязателен. Произведение двух чисел около — это , никакой unsigned long long этого не удержит.
Ро-алгоритм Полларда
Простоту проверили. Если число составное, его ещё нужно разложить — и перебор до корня опять слишком медленный.
Идея Полларда красива. Возьмём псевдослучайную последовательность . Она обязана зациклиться, и если нарисовать её как путь, получится греческая буква ρ: хвост и петля.
Пусть — какой-то делитель . Та же последовательность по модулю зацикливается раньше — примерно через шагов по парадоксу дней рождения. Значит, найдётся пара по модулю , но по модулю . Тогда — нетривиальный делитель.
Пару ищут алгоритмом «черепаха и заяц»: один указатель делает шаг, другой два.
long long pollard(long long n) {
if (n % 2 == 0) return 2;
for (long long c = 1;; c++) {
auto f = [&](long long x) { return (mulmod(x, x, n) + c) % n; };
long long x = 2, y = 2, d = 1;
while (d == 1) {
x = f(x);
y = f(f(y));
d = __gcd(llabs(x - y), n);
}
if (d != n) return d;
}
}
void factor(long long n, map<long long, int> &out) {
if (n == 1) return;
if (isPrime(n)) { out[n]++; return; }
long long d = pollard(n);
factor(d, out);
factor(n / d, out);
}
Внешний цикл по нужен, потому что при неудачном выборе константы алгоритм упирается в и ничего не находит. Тогда берут следующую и пробуют снова.
Ожидаемое время — . Для около это порядка тридцати тысяч операций: разложение произведения двух девятизначных простых занимает миллисекунды.
В factor важен порядок: сначала проверка на простоту, потом Поллард. Запускать ро-алгоритм на простом числе бессмысленно — он будет крутиться до бесконечности, перебирая .
Когда что применять
| ситуация | инструмент |
|---|---|
| много чисел до | линейное решето, |
| одно число до | перебор до корня |
| одно число до , нужна простота | Миллер—Рабин |
| одно число до , нужно разложение | Миллер—Рабин + Поллард |
| простые в отрезке , до | сегментное решето |
Отдельно стоит сказать: на школьных олимпиадах Поллард встречается редко. Но знать про его существование полезно — иначе задача с ограничением выглядит нерешаемой, хотя решается в двадцать строк.