Алгоритм 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) содержит довольно сильно отличающийся от приведенного выше алгоритм.
В этом посте я попытаюсь, надеюсь, доступно доказать, что этот алгоритм не только работает, но и делает это за линейную сложность.
Определения
Обратите внимание, все определения выше даны для
Некоторые очевидные свойства введенных выше функций, которые будут использоваться дальше:
Доказательство
Лемма 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).

