В комментариях к одному из прошлых постов о решете Эратосфена был упомянут этот короткий алгоритм из Википедии:
Алгоритм 1:
Алгоритм простой, но не всем он показался очевидным. Главная же проблема в том, что на Википедии нет доказательства, а ссылка на первоисточник (pdf) содержит довольно сильно отличающийся от приведенного выше алгоритм.
В этом посте я попытаюсь, надеюсь, доступно доказать, что этот алгоритм не только работает, но и делает это за линейную сложность.
— множество простых чисел.
— минимальный простой делитель числа: ![$lp(x) = min \{p \in \mathcal{P}\ \vert\ p\vert x \}$](https://habrastorage.org/getpro/habr/formulas/d13/8e2/791/d138e279182c2dcc08e8cfdeaa830b3c.svg)
— остальные множители: ![$rd(x) = x / lp(x)$](https://habrastorage.org/getpro/habr/formulas/060/3bf/7c8/0603bf7c8b26a4e9be3cca2090c2c2b9.svg)
Обратите внимание, все определения выше даны для
.
Некоторые очевидные свойства введенных выше функций, которые будут использоваться дальше:
![$lp(a\times b) = min(lp(a), lp(b))$](https://habrastorage.org/getpro/habr/formulas/fcd/95b/733/fcd95b733d8b0645f163bed9d9d1a626.svg)
![$rd(x) < x$](https://habrastorage.org/getpro/habr/formulas/5c0/cc4/159/5c0cc41597d610d0db1298a731c73a52.svg)
![$p \in \mathcal{P} => lp(p) = p$](https://habrastorage.org/getpro/habr/formulas/560/a32/7a8/560a327a85cd15cd5587eb69b602d69d.svg)
Лемма 1:![$lp(x) \le lp(rd(x)), \forall x \notin \mathcal{P}, x>1$](https://habrastorage.org/getpro/habr/formulas/2bf/c1d/137/2bfc1d137c3fb56e53eefe664353856b.svg)
Доказательство: Т.к. любой делитель
также является делителем
, а
не превосходит любой делитель
, то
не превосходит и любой делитель
, включая наименьший из них. Единственная проблема в этом утверждении, если
не имеет простых делителей, но это возможно только в случае
, который исключен в условии леммы.
![$\square$](https://habrastorage.org/getpro/habr/formulas/3a3/086/d5b/3a3086d5be532b93301afea91574367d.svg)
Пусть![$E = \{(lp(x), rd(x)) \vert \forall x \notin \mathcal{P}, 2\le x \le n\}$](https://habrastorage.org/getpro/habr/formulas/44b/ef8/f19/44bef8f19a206ae65b72550770098b72.svg)
Т.к.
(по определению
), если бы нам было дано множество
, то мы смогли бы вычислить
для всех составных чисел до n. Это делает, например, следующий алгоритм:
Алгоритм 2:
Заметим, что
, поэтому алгоритм 2 выше работает за линейную сложность.
Далее я докажу, что алгоритм 1 из Википедии на самом деле просто перебирает все элементы этого множества, ведь его можно параметризовать и по-другому.
Пусть![$E' = \{(p, r) \vert p \in \mathcal{P}, 2\le r < n, p \le lp(r), p \times r \le n\}$](https://habrastorage.org/getpro/habr/formulas/a65/3e7/3a5/a653e73a542d1c7f56a8d74552d5738a.svg)
Лемма 2:![$E = E'$](https://habrastorage.org/getpro/habr/formulas/7f8/e7f/333/7f8e7f3332a123755040c13e771d0ff0.svg)
Доказательство:
Пусть
=>
.
По определению
:
,
. По лемме 1,
.
т.к.
, то
.
Поскольку
,
.
Так же, по определению
,
, следовательно,
.
Все 4 условия
выполнены, значит,
=>
.
Пусть
. Пусть
.
По определению
,
. Значит,
— простой делитеть
.
Т.к
, то
Значит,
По определению,
Также, очевидно, ![$x = a \times b \ge 2.$](https://habrastorage.org/getpro/habr/formulas/dd8/d63/095/dd8d630951e6ff67fbbb16ade3f02497.svg)
Все условия для
выполнены => ![$E' \subset E.$](https://habrastorage.org/getpro/habr/formulas/4eb/bf4/c94/4ebbf4c940a0375af2698eafc467e9a0.svg)
Следовательно,![$E = E'.$](https://habrastorage.org/getpro/habr/formulas/894/07f/4f7/89407f4f7addfcad6ad85de37a39aea7.svg)
![$\square$](https://habrastorage.org/getpro/habr/formulas/3a3/086/d5b/3a3086d5be532b93301afea91574367d.svg)
Теперь осталось перебрать правильные
и
из определения множества
и применить алгоритм 2. Именно это и делает Алгоритм 1 (только вместо r используется переменная i). Но есть проблема! Что бы перебрать все элементы множества
для вычисления
нам надо знать
ведь в определении есть условие
.
К счастью, эта проблема обходится правильным порядком перебора. Надо перебирать
во внешнем цикле, а
— во внутреннем. Тогда
уже будет вычислено. Этот факт доказывает следующая теорема.
Теорема 1:
Алгоритм 1 поддерживает следующий инвариант: После выполнения итерации внешнего цикла при i=k, все простые числа до k включительно будут выделены в список pr. Также будет подсчитано
для всех
в массиве lp.
Доказательство:
Докажем по индукции. Для k=2 инвариант проверяется вручную. Единственное простое число 2 будет добавлено в список pr, потому что массив lp[] изначально заполнен нулями. Также, единственное составное число у которого
— это 4. Несложно убедиться, что внутренний цикл выполнится ровно один раз (при n>3) и правильно выполнит lp[4] := 2.
Теперь допустим, что инвариант выполняется для итерации i=k-1. Докажем, что он будет выполнятся и для итерации i=k.
Для этого достаточно проверить что число i, если оно простое, будет добавлено в список pr и что
будет подсчитано для всех составных чисел
т.ч.
Именно эти числа из инварианта для k не покрыты уже инвариантом для k-1.
Если i простое, то lp[i] будет равно 0, ведь единственная операция записи в массив lp, которая теоретически могла бы подсчитать lp[i] (в строке 6), всегда пишет в составные индексы, ведь p*i (для i > 1) — всегда составное. Поэтому число i будет добавлено в список простых. Также, в строке 3 будет подсчитано lp[i].
Если же i составное, то на начало итерации lp[i] уже будет подсчитано по инварианту для i=k-1, ведь
или
следовательно i попадает под условие инварианта в предыдущей итерации. Поэтому составное число i не будет добавлено в список простых чисел.
Далее, имея корректное lp[i] и все простые числа до i включительно цикл в строках 5-6 переберет все элементы
, у которых вторая часть равна i:
Все нужные простые числа в списке pr есть, т.к. нужны только числа до
. Поэтому будут подсчитаны значения lp[] для всех составных чисел
, у которых
. Это ровно все те числа, которых не хватало при переходе от инварианта для k-1 к инварианту для k.
Следовательно, инавриант выполняется для любых i = 2..n.
![$\square$](https://habrastorage.org/getpro/habr/formulas/3a3/086/d5b/3a3086d5be532b93301afea91574367d.svg)
По инварианту из Теоремы 1 для i=n получается, что все простые числа до n и все lp[] будут подсчитаны алгоритмом 1.
Более того, поскольку в строках 5-6 перебираются различные элементы множества
, то суммарно внутренний цикл выполнит не более
операций. Операция проверки в цикле выполнится ровно
раз (
раз вернет true и n-1 раз, для каждого i, вернет false). Все оставшиеся операции вложены в один цикл по i от 2 до n.
Отсюда следует, что сложность алгоритма 1 — O(n).
Алгоритм 1:
1: для i := 2, 3, 4, ..., до n:
2: если lp[i] = 0:
3: lp[i] := i
4: pr[] += {i}
5: для p из pr пока p ≤ lp[i] и p*i ≤ n:
6: lp[p*i] := p
Результат:
lp - минимальный простой делитель для кажого числа до n
pr - список всех простых до n.
Алгоритм простой, но не всем он показался очевидным. Главная же проблема в том, что на Википедии нет доказательства, а ссылка на первоисточник (pdf) содержит довольно сильно отличающийся от приведенного выше алгоритм.
В этом посте я попытаюсь, надеюсь, доступно доказать, что этот алгоритм не только работает, но и делает это за линейную сложность.
Примечание о сложности алгоритма
Хоть этот алгоритм и асимптотически быстрее стандартного решета Эратосфена за O(n log log n), ему требуется гораздо больше памяти. Поэтому для по-настоящему больших n, где бы этот алгоритм засиял во всей красе, он не применим. Однако, он очень полезен даже для небольших n, если вам надо не только найти все простые числа, но и быстро раскладывать на множители все числа до n.
Определения
Обратите внимание, все определения выше даны для
Некоторые очевидные свойства введенных выше функций, которые будут использоваться дальше:
Доказательство
Лемма 1:
Доказательство: Т.к. любой делитель
Пусть
Т.к.
Алгоритм 2:
1: Для всех (l,r) из E:
2: lp[l*r] := l;
Заметим, что
Далее я докажу, что алгоритм 1 из Википедии на самом деле просто перебирает все элементы этого множества, ведь его можно параметризовать и по-другому.
Пусть
Лемма 2:
Доказательство:
Пусть
По определению
т.к.
Поскольку
Так же, по определению
Все 4 условия
Пусть
По определению
Т.к
По определению,
Все условия для
Следовательно,
Теперь осталось перебрать правильные
К счастью, эта проблема обходится правильным порядком перебора. Надо перебирать
Теорема 1:
Алгоритм 1 поддерживает следующий инвариант: После выполнения итерации внешнего цикла при i=k, все простые числа до k включительно будут выделены в список pr. Также будет подсчитано
Доказательство:
Докажем по индукции. Для k=2 инвариант проверяется вручную. Единственное простое число 2 будет добавлено в список pr, потому что массив lp[] изначально заполнен нулями. Также, единственное составное число у которого
Теперь допустим, что инвариант выполняется для итерации i=k-1. Докажем, что он будет выполнятся и для итерации i=k.
Для этого достаточно проверить что число i, если оно простое, будет добавлено в список pr и что
Если i простое, то lp[i] будет равно 0, ведь единственная операция записи в массив lp, которая теоретически могла бы подсчитать lp[i] (в строке 6), всегда пишет в составные индексы, ведь p*i (для i > 1) — всегда составное. Поэтому число i будет добавлено в список простых. Также, в строке 3 будет подсчитано lp[i].
Если же i составное, то на начало итерации lp[i] уже будет подсчитано по инварианту для i=k-1, ведь
Далее, имея корректное lp[i] и все простые числа до i включительно цикл в строках 5-6 переберет все элементы
потому что оно из списка pr
по условию останова цикла
по условию останова цикла
следует из
Все нужные простые числа в списке pr есть, т.к. нужны только числа до
Следовательно, инавриант выполняется для любых i = 2..n.
По инварианту из Теоремы 1 для i=n получается, что все простые числа до n и все lp[] будут подсчитаны алгоритмом 1.
Более того, поскольку в строках 5-6 перебираются различные элементы множества
Отсюда следует, что сложность алгоритма 1 — O(n).