Решето Эратосфена
Статья написана для Сборника Олпрогера.
Достаточно часто в задачах бывает полезно - уметь факторизировать числа. В большинстве таких задач как раз используется решето Эратосфена.
Вспомним несколько определений.
Простое число - число, которое имеет ровно два различных натуральных делителя — единицу и самого себя.
Если число не простое, то оно составное.
Единица не является ни простым, ни составным числом.
Давайте сначала решим задачу капельку полегче, чем исходная: для каждого числа от 1 до n
поймем является ли оно простым или нет.
Список простых чисел: 2, 3, 5, 7, 11, 13, 17, ...
Заметим, что если число x
делится на какое-либо из чисел от 2 до x - 1
, то оно составное, иначе простое. Сделаем какие-то замечания:
- Все числа, которые делятся на
2
, кроме самойдвойки
точно не простые:4, 6, 8, 10, 12, ...
- Все числа, которые делятся на
3
, кроме самойтройки
точно не простые:6, 9, 12, 15, 18, ...
- Все числа, которые делятся на
4
, кроме самойчетверки
точно не простые:8, 12, 16, 20, ...
- Все числа, которые делятся на
5
, кроме самойпятерки
точно не простые:10, 15, 20, 25, ...
- И так далее
Из этого замечания вырисовывается интересный алгоритм: давайте перебирать число x от 2 до n
, и помечать, что числа 2x, 3x, 4x, ...
- составные. Если мы ни разу не пометили число, то оно является простым. Это и есть решето Эратосфена.
// isPrime[i] - хранит bool в зависимости от того, что i простое или нет // Изначального IsPrime - хранит n значений true isPrime[1] = false; // 1 - не простое число for (int x = 2; x <= n; x++) { // перебираем x for (int y = 2 * x; y <= n; y += x) { // перебираем числа вида 2x, 3x, 4x isPrime[y] = false; } } // после этого массив isPrime хранит корректные значения
Сперва может показаться, что этот алгоритм работает долго, но это не так. Оценка временной сложности алгоритма:
Это так, потому что сумма гармонического ряда сходится к O(log n).
Давайте сделаем небольшую оптимазацию нашего алгоритма, а именно достаточно перебирать только простые x
, и для них помечать 2x, 3x, 4x, ...
// isPrime[i] - хранит bool в зависимости от того, что i простое или нет // Изначального IsPrime - хранит n значений true isPrime[1] = false; // 1 - не простое число for (int x = 2; x <= n; x++) { // перебираем x if (isPrime[x]) { // берем только простые x for (int y = 2 * x; y <= n; y += x) { // перебираем числа вида 2x, 3x, 4x isPrime[y] = false; } } } // после этого массив isPrime хранит корректные значения
Используя математику можно доказать, что это работает за O(n log (logn))
. При желании с доказательсвом можно ознакомиться в статьях, отмеченных внизу.
Вернемся к исходной задаче. Мне нужно факторизовать все числа от 1 до n
.
Пусть я для каждого числа x знаю его минимальный простой делитель. То есть для 4 - это 2
, 21 - это 3
, 17 - это 17
. Единицу проигнорируем. Обозначим минимальный простой делитель x - minPrimeDivisor[x]
.
Тогда, чтобы разложить на простые множители число x
, сделаем следующее: добавим в список его простых множителей minPrimeDivisor[x]
, и разложим число x / minPrimeDivisor[x]
. Будем повторять этот процесс, пока не получим единицу. Достаточно понятно, что алгоритм корректен. Код:
for (int x = 2; x <= n; x++) { // перебираем число x, которое хотим разложить vector<int> factors; // список простых множителей int current = x; // текущее значение x while (current != 1) { factors.push_back(minPrimeDivisor[current]); current /= minPrimeDivisor[current]; } // factor - корректный список множителей }
Тогда, чтобы решить исходную задачу, нужно научиться строить массив minPrimeDivisor
.
Заметим, что этот массив можно поддерживать с помощью решета Эратосфена. Действительно, давайте изначально заполним массив minPrimeDivisor[i] = i
. Теперь, когда я перебираю простое x
и перебираю y = 2x, 3x, 4x, ...
, прорелаксируем minPrimeDivisor[y]
с x
. Достаточно понятно, что для числа я переберу все его простые делители, а значит минимум будет посчитан корректно. Код:
// isPrime[i] - хранит bool в зависимости от того, что i простое или нет // Изначального IsPrime - хранит n значений true // minPrimeDivisor[i] - хранит минимальный простой делитель i // Изначально minPrimeDivisor[i] = i isPrime[1] = false; // 1 - не простое число for (int x = 2; x <= n; x++) { // перебираем x if (isPrime[x]) { // берем только простые x for (int y = 2 * x; y <= n; y += x) { // перебираем числа вида 2x, 3x, 4x isPrime[y] = false; // релаксируем minPrimeDivisor[y] с x minPrimeDivisor[y] = min(minPrimeDivisor[y], x); } } } // после этого массивы isPrime, minPrimeDivisor хранят корректные значения
for (int x = 2; x <= n; x++) { if (minPrimeDivisor[x] == x) { for (int y = 2 * x; y <= n; y += x) { minPrimeDivisor[y] = min(minPrimeDivisor[y], x); } } }
Итого мы научились решать задачу, которую хотели за O(nlog(logn))
.
Другие статьи:
E-maxx - описание и реализация
Алгоритмика - в статье также содержится оптимазация алгоритма O(n).