Линейное решето
Решето, в котором каждое составное вычёркивается ровно один раз. С доказательством — и с честным ответом, почему на практике оно часто медленнее Эратосфена.
6 мин
Решето Эратосфена работает за . Логарифм логарифма — это почти константа, и придираться вроде бы не к чему. Но у алгоритма есть заметный изъян: одно и то же число вычёркивается много раз. Число 60 будет помечено при , при и при — три раза вместо одного.
Линейное решето убирает эту избыточность. Каждое составное число помечается ровно однажды, поэтому суммарная работа — честное .
Идея: помечать своим наименьшим простым
Ключ к единственности — договориться, кто именно имеет право вычеркнуть число.
Пусть — наименьший простой делитель числа (least prime). Любое составное единственным образом представляется как
Раз такое представление одно, то, если каждое число вычёркивать только в этой паре, каждое вычеркнётся ровно один раз. Вся задача — устроить перебор так, чтобы пара встречалась ровно однажды.
Код
vector<int> lp(n + 1, 0); // наименьший простой делитель
vector<int> primes; // все найденные простые по возрастанию
for (int i = 2; i <= n; i++) {
if (lp[i] == 0) { // i не вычеркнули — значит, оно простое
lp[i] = i;
primes.push_back(i);
}
for (int p : primes) {
if (p > lp[i] || (long long)i * p > n) break;
lp[i * p] = p;
}
}
Внутренний цикл идёт по уже найденным простым от меньших к большим и обрывается по двум условиям. Второе — просто «не вылезли за границу». Первое, p > lp[i], и есть весь алгоритм.
Обратите внимание на (long long)i * p. При произведение доходит до и в int не помещается — это классическое место, где решето молча ломается.
Почему каждое число помечается
Возьмём составное и его наименьший простой делитель . Положим .
Все простые делители не меньше — иначе у нашёлся бы простой делитель меньше . Значит, , условие p > lp[i] на этом ещё не сработало, и цикл до него дойдёт. Также , так что второе условие тоже выполнено.
Итог: на итерации мы обязательно запишем lp[x] = p.
Почему ровно один раз
Предположим, пометили дважды: как и как .
Условие p <= lp[i] означает, что не превосходит ни одного простого делителя . Но тогда — наименьший простой делитель всего произведения . То есть в любой помечающей паре , откуда , а значит и .
Пар оказалось не две, а одна. Суммарное число операций равно числу составных чисел до , то есть .
Пример: как заполняется таблица
Первые шаги для :
| что помечаем | почему остановились | ||
|---|---|---|---|
| 2 | 2 (простое) | ||
| 3 | 3 (простое) | , | |
| 4 | 2 | ||
| 5 | 5 (простое) | ||
| 6 | 2 |
Видно главное: на мы не пометили — не потому, что вышли за границу, а потому, что 18 достанется паре , где двойка и есть наименьший делитель.
Что ещё считается тем же циклом
Ценность линейного решета не столько в асимптотике, сколько в том, что вместе с ним почти бесплатно считаются мультипликативные функции. Достаточно знать, как функция ведёт себя на в двух случаях: когда и когда .
Функция Эйлера — количество чисел от 1 до , взаимно простых с :
phi[1] = 1;
// внутри основного цикла, вместо простого присваивания lp:
if (lp[i] == 0) phi[i] = i - 1; // i простое
// ...
lp[i * p] = p;
phi[i * p] = (p == lp[i]) ? phi[i] * p : phi[i] * (p - 1);
Так же считаются число делителей, сумма делителей, функция Мёбиуса. Все — за один проход, без отдельного решета на каждую.
И, конечно, массив сам по себе даёт разложение любого числа до за : делим на , пока не дойдём до единицы.
Честное сравнение с Эратосфеном
И вот неожиданный результат: линейное решето на практике часто медленнее решета Эратосфена, хотя асимптотика у него лучше.
Причина — кэш. Эратосфен идёт по массиву строго по возрастанию с постоянным шагом: процессор угадывает такой доступ и подгружает данные заранее. Линейное решето прыгает по адресам для разных и вдобавок читает массив primes, то есть работает с памятью в двух местах сразу. Константа получается больше, и на до выигрыш асимптотики её не окупает.
Практический вывод:
- нужен только список простых — берите Эратосфена, он проще и обычно быстрее;
- нужен или мультипликативная функция — берите линейное;
- знать нужно оба: на собеседовании и на олимпиаде спрашивают именно про второе.
Память у линейного решета тоже больше: int на число вместо bool, то есть в 4 раза (а с учётом того, что vector<bool> упакован по битам, — в 32). При это уже решает.