Определение. Целое положительное число называется простым, если оно имеет ровно два различных натуральных делителя — единицу и самого себя. Единица простым числом не считается.
Решето Эратосфена (англ. sieve of Eratosthenes) — алгоритм нахождения всех простых чисел от до .
Основная идея соответствует названию алгоритма: запишем ряд чисел , а затем будем вычеркивать
- сначала числа, делящиеся на , кроме самого числа ,
- потом числа, делящиеся на , кроме самого числа ,
- с числами, делящимися на , ничего делать не будем — мы их уже вычёркивали,
- потом продолжим вычеркивать числа, делящиеся на , кроме самого числа ,
…и так далее.
Самая простая реализация может выглядеть так:
vector<bool> sieve(int n) {
vector<bool> is_prime(n + 1, true);
for (int i = 2; i <= n; i++)
if (is_prime[i])
for (int j = 2 * i; j <= n; j += i)
is_prime[j] = false;
return is_prime;
}
Этот код сначала помечает все числа, кроме нуля и единицы, как простые, а затем начинает процесс отсеивания составных чисел. Для этого мы перебираем в цикле все числа от до , и, если текущее число простое, то помечаем все числа, кратные ему, как составные.
Если память позволяет, то для оптимизации скорости лучше использовать не вектор bool
, а вектор char
— но он займёт в 8 раз больше места. Компьютер не умеет напрямую оперировать с битами, и поэтому при индексации к vector<bool>
он сначала достаёт нужный байт, а затем битовыми операциями получает нужное значение, что занимает приличное количество времени.
#Время работы
Довольно легко показать, что асимптотическое время работы алгоритма хотя бы не хуже, чем : даже если бы мы входили в цикл вычёркиваний для каждого числа, не проверяя его сначала на простоту, суммарно итераций было бы
Здесь мы воспользовались асимптотикой гармонического ряда.
У исходного алгоритма асимптотика должна быть ещё лучше. Чтобы найти её точнее, нам понадобятся два факта про простые числа:
- Простых чисел от до примерно .
- Простые числа распределены без больших «разрывов» и «скоплений», то есть -тое простое число примерно равно .
Мы можем упрощённо считать, что число является простым с «вероятностью» . Тогда, время работы алгоритма можно более точнее оценить как
Асимптотику алгоритма можно улучшить и дальше, до .
#Линейное решето
Основная проблема решета Эратосфена состоит в том, что некоторые числа мы будем помечать как составные несколько раз — столько, сколько у них различных простых делителей. Чтобы достичь линейного времени работы, нам нужно придумать способ, как рассматривать все составные числа ровно один раз.
Обозначим за минимальный простой делитель числа и заметим следующий факт: у составного числа есть единственное представление , и при этом у числа нет простых делителей меньше .
Идея оптимизации состоит в том, чтобы перебирать этот , и для каждого перебирать только нужные множители — а именно, все от до включительно.
#Алгоритм
Немного обобщим задачу — теперь мы хотим посчитать для каждого числа на отрезке его минимальный простой делитель , а не только определить его простоту.
Изначально массив заполним нулями, что означает, что все числа предполагаются простыми. В ходе работы алгоритма этот массив будет постепенно заполняться. Помимо этого, будем поддерживать список всех найденных на текущий момент простых чисел.
Теперь будем перебирать число от до . Если это число простое, то есть , то присвоим и добавим в список .
Дальше, вне зависимости от простоты , начнём процесс расстановки значений в массиве — переберем найденные простые числа , не превосходящие , и сделаем присвоение .
const int n = 1e6;
int d[n + 1];
vector<int> p;
for (int k = 2; k <= n; k++) {
if (p[k] == 0) {
d[k] = k;
p.push_back(k);
}
for (int x : p) {
if (x > d[k] || x * d[k] > n)
break;
d[k * x] = x;
}
}
Алгоритм требует как минимум в 32 раза больше памяти, чем обычное решето, потому что требуется хранить делитель (int
, 4 байта) вместо одного бита на каждое число. Линейное решето хоть и имеет лучшую асимптотику, но на практике проигрывает также и по скорости оптимизированному варианту решета Эратосфена.
#Применения
Массив позволяет искать факторизацию любого числа за время порядка размера этой факторизации:
Знание факторизации всех чисел — очень полезная информация для некоторых задач. Линейное решето интересно не своим временем работы, а именно этим массивом минимальных простых делителей.