Определение. Целое положительное число называется простым, если оно имеет ровно два различных натуральных делителя — единицу и самого себя. Единица простым числом не считается.
Решето Эратосфена (англ. sieve of Eratosthenes) — алгоритм нахождения всех простых чисел от
Основная идея соответствует названию алгоритма: запишем ряд чисел
-
сначала числа, делящиеся на
$2$ , кроме самого числа$2$ , -
потом числа, делящиеся на
$3$ , кроме самого числа$3$ , -
с числами, делящимися на
$4$ , ничего делать не будем — мы их уже вычёркивали, -
потом продолжим вычеркивать числа, делящиеся на
$5$ , кроме самого числа$5$ ,
…и так далее.
Самая простая реализация может выглядеть так:
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)
prime[j] = false;
return is_prime;
}
Этот код сначала помечает все числа, кроме нуля и единицы, как простые, а затем начинает процесс отсеивания составных чисел. Для этого мы перебираем в цикле все числа от
Если память позволяет, то для оптимизации скорости лучше использовать не вектор bool
, а вектор char
— но он займёт в 8 раз больше места. Компьютер не умеет напрямую оперировать с битами, и поэтому при индексации к vector<bool>
он сначала достаёт нужный байт, а затем битовыми операциями получает нужное значение, что занимает приличное количество времени.
Довольно легко показать, что асимптотическое время работы алгоритма хотя бы не хуже, чем
Здесь мы воспользовались асимптотикой гармонического ряда.
У исходного алгоритма асимптотика должна быть ещё лучше. Чтобы найти её точнее, нам понадобятся два факта про простые числа:
-
Простых чисел от
$1$ до$n$ примерно$\frac{n}{\ln n}$ . -
Простые числа распределены без больших «разрывов» и «скоплений», то есть
$k$ -тое простое число примерно равно$k \ln k$ .
Мы можем упрощённо считать, что число
Асимптотику алгоритма можно улучшить и дальше, до
Основная проблема решета Эратосфена состоит в том, что некоторые числа мы будем помечать как составные несколько раз — а именно столько раз, сколько у них различных простых делителей. Чтобы достичь линейного времени работы, нам нужно придумать способ, как рассматривать все составные числа ровно один раз.
Обозначим за
Идея оптимизации состоит в том, чтобы перебирать этот
Немного обобщим задачу — теперь мы хотим посчитать для каждого числа
Изначально массив
Теперь будем перебирать число
Дальше, вне зависимости от простоты
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 байта) вместо одного бита на каждое число. Линейное решето хоть и имеет лучшую асимптотику, но на практике проигрывает также и по скорости оптимизированному варианту решета Эратосфена.
Массив
Знание факторизации всех чисел — очень полезная информация для некоторых задач. Ленейное решето интересно не своим временем работы, а именно этим массивом минимальных простых делителей.