Что тренируем сегодня
На прошлой тренировке мы искали ответ по прямой формуле и префиксным суммам на отсортированном массиве. Сегодня — следующий шаг: что делать, когда массив исходно не отсортирован, но из него можно построить вспомогательный массив, который отсортирован всегда — и по нему уже искать ответ бинарным поиском. Вторая задача сессии — про решето Эратосфена, но не в чистом виде «найди все простые до N», а как быстрый способ отвечать на десятки тысяч вопросов о числах до . Это предпоследняя тренировка «разогрева»: после сегодняшней сессии останется всего одна, которая станет мостиком к более длинным сессиям «плато».
Теория
Бинарный поиск по вспомогательному массиву, а не по исходному. Если сам массив не отсортирован (и сортировать нельзя — порядок важен для ответа), из него можно построить другой массив, который отсортирован всегда, и искать уже по нему.
Когда применять:
- вопрос звучит как «до какого места можно дойти» или «докуда достижимо»;
- исходные данные идут в произвольном порядке, но их накопительная характеристика (например, максимум слева направо) растёт монотонно.
Как решать:
- Строим вспомогательный массив за один проход — например, префиксный максимум: на позиции храним наибольшее значение среди первых элементов.
- Такой массив всегда неубывающий (максимум не может уменьшиться при добавлении элементов).
- Применяем бинарный поиск уже к вспомогательному массиву, а не к исходному.
Где ошибаются: пытаются бинарно искать прямо по исходному неотсортированному массиву — без построения монотонного вспомогательного массива результат непредсказуем.
Решето Эратосфена — что это такое. Способ за один проход отметить сразу все простые числа от 2 до заданной границы . Идее больше двух тысяч лет, и формулируется она без единой формулы.
Выпишем подряд все числа от 2 до . Берём первое невычеркнутое число — это 2. Оно простое: слева от него ничего нет, значит, делителей меньше себя у него не нашлось. Вычёркиваем все кратные двойки — : каждое из них делится на 2, значит, составное. Переходим к следующему невычеркнутому — это 3. Оно уцелело, то есть не делится ни на одно меньшее число, — значит, простое. Вычёркиваем . Повторяем, пока не дойдём до конца. Всё, что осталось невычеркнутым, — простые числа.
Прогоним руками до :
| Шаг | Что вычёркиваем | Осталось |
|---|---|---|
| старт | — | 2 3 4 5 6 7 8 9 10 … 30 |
| 4, 6, 8, 10, 12, …, 30 | 2 3 5 7 9 11 13 15 17 19 21 23 25 27 29 | |
| 9, 15, 21, 27 (6, 12, 18, 24, 30 уже вычеркнуты) | 2 3 5 7 11 13 17 19 23 25 29 | |
| 25 (10, 15, 20, 30 уже вычеркнуты) | 2 3 5 7 11 13 17 19 23 29 | |
| — вычёркивать нечего, останавливаемся | 2 3 5 7 11 13 17 19 23 29 |
Почему это верно. Число вычёркивается ровно тогда, когда у него нашёлся делитель строго между 1 и им самим, — то есть ровно тогда, когда оно составное. И ни одно составное не пропущено: у любого составного есть простой делитель , а значит, на шаге этого число обязательно попадёт под вычёркивание.
Базовая реализация:
from math import isqrt
def sieve(n: int) -> list[bool]:
"""is_prime[i] == True <=> i простое, для i от 0 до n."""
is_prime = [True] * (n + 1)
is_prime[0] = is_prime[1] = False # 0 и 1 простыми не считаются
for i in range(2, isqrt(n) + 1): # внешний цикл — только до sqrt(n)
if is_prime[i]: # i уцелело => i простое
for j in range(i * i, n + 1, i): # вычёркиваем с i*i, а не с 2*i
is_prime[j] = False
return is_prime
vector<char> sieve(int n) {
vector<char> isPrime(n + 1, 1);
isPrime[0] = isPrime[1] = 0; // 0 и 1 — не простые
for (int i = 2; (long long)i * i <= n; i++) { // внешний цикл до sqrt(n)
if (isPrime[i]) { // i уцелело => i простое
for (int j = i * i; j <= n; j += i) // вычёркиваем с i*i
isPrime[j] = 0;
}
}
return isPrime;
}
Две детали в коде выглядят как микрооптимизации, но обе следуют из доказательства выше — их стоит понимать, а не запоминать:
- Внешний цикл идёт только до . Если бы у составного не было простого делителя , то раскладывалось бы как , где оба множителя больше , а тогда — противоречие. Значит, все составные вычеркнутся уже на шагах .
- Внутренний цикл стартует с , а не с . Все меньшие кратные вида при уже вычеркнуты раньше — на шаге минимального простого делителя числа .
Сложность. Внутренний цикл делает шагов для каждого простого , а сумма асимптотически равна . Итого по времени — практически линейно: решето до строится за доли секунды. Память — байт (bytearray в Python, vector<char> в C++; vector<bool> даст в 8 раз меньше памяти, но медленнее из-за битовой упаковки).
Границы применимости. Решето отвечает на вопрос «какие числа до простые» — оно про весь диапазон сразу, а не про одно число. Если нужно проверить единственное большое , дешевле перебрать делители до за , а для действительно больших чисел — взять тест Миллера — Рабина. Решето окупается, когда проверок много и все они укладываются в один диапазон.
Обобщения: решето — это не только про простоту. Каркас «для каждого пройтись по его кратным» переиспользуется под целое семейство задач.
Решето минимального простого делителя (SPF). Вместо флага «простое / не простое» храним для каждого числа его наименьший простой делитель. Строится тем же проходом, а даёт факторизацию любого за делений — без перебора делителей:
def smallest_prime_factor(n: int) -> list[int]:
spf = list(range(n + 1)) # изначально spf[x] = x
for i in range(2, isqrt(n) + 1):
if spf[i] == i: # i не помечено меньшим делителем => простое
for j in range(i * i, n + 1, i):
if spf[j] == j: # ставим метку только первый раз
spf[j] = i
return spf
def factorize(x: int, spf: list[int]) -> dict[int, int]:
"""Разложение x на простые множители: {простое: степень}."""
result: dict[int, int] = {}
while x > 1:
p = spf[x]
while x % p == 0:
x //= p
result[p] = result.get(p, 0) + 1
return result
Решето по любой «функции от делителей». Если нужно для всех чисел до посчитать количество делителей, сумму делителей или функцию Эйлера — идём не по простым, а по всем от 1 до и обновляем кратные. Стоимость такого прохода — (гармоническая сумма):
divisors = [0] * (n + 1)
for d in range(1, n + 1):
for m in range(d, n + 1, d): # d — делитель каждого своего кратного
divisors[m] += 1 # divisors[m] = количество делителей m
Сегментированное решето. Когда простые нужны в отрезке с большими границами (до ), но узким окном ( до ): строим обычное решето до , а потом этими простыми вычёркиваем кратные внутри окна. Массив размера вместо — вот и весь фокус:
def primes_in_range(L: int, R: int) -> list[int]:
"""Простые в [L, R] при 2 <= L <= R и небольшой ширине окна."""
base = sieve(isqrt(R)) # решето до sqrt(R)
mark = bytearray([1]) * (R - L + 1) # окно, индекс j <-> число L + j
for p in range(2, isqrt(R) + 1):
if not base[p]:
continue
start = max(p * p, (L + p - 1) // p * p) # первое кратное p внутри окна
for j in range(start, R + 1, p):
mark[j - L] = 0
return [L + j for j, ok in enumerate(mark) if ok]
Линейное решето. Модификация, в которой каждое составное вычёркивается ровно один раз — своим минимальным простым делителем, что даёт честное . На олимпиадных ограничениях выигрыш относительно редко решает исход, но приём полезно знать: он же попутно строит SPF-массив.
Ключевой приём сегодняшней задачи: строить решето до нужной границы, а не до границы входных чисел. В задаче 2 числа доходят до , но интересует нас простота их квадратного корня — а корень не превышает . Решето до строится мгновенно, решето до не построится никогда.
Когда применять:
- числа в условии большие (например, до );
- но интересующее свойство (например, простота корня числа) сводится к диапазону, который решетом реально покрыть (до , если исходные числа — квадраты).
Как решать:
- Строим решето один раз — но только до той границы, которая реально нужна (например, до ), а не до границы исходных чисел.
- Каждая проверка простоты дальше — это просто чтение готового значения за .
Где ошибаются:
- строят решето до границы исходных чисел вместо границы, которая реально нужна — это избыточно по памяти и времени, а часто и вовсе невозможно;
- забывают явно обнулить
is_prime[0]иis_prime[1]— массив инициализирован единицами, и единица ошибочно считается простой; - строят решето заново внутри цикла по запросам вместо одного построения до начала обработки.
Задача 1 — CF ~1200 — «Scuza»
Что дано
Лестница из ступеней. Ступень выше предыдущей на метров (первая ступень выше земли на метров, земля — на высоте 0).
Дано независимых вопросов. В вопросе задана длина ног . Взойти на ступень можно только если длина ног не меньше прироста высоты именно этой ступени: . Подниматься можно только последовательно, начиная с первой ступени — как только встречается ступень с приростом больше , подъём останавливается. Нужно для каждого вопроса вывести максимальную высоту, которой можно достичь.
Ограничения: количество тестов . В каждом тесте (суммарно по всем тестам и не превышают каждое). , . Ответ может не поместиться в 32-битный тип — нужен 64-битный (long long).
Формат ввода: первая строка — . Для каждого теста: строка с и ; строка с значениями ; строка с значениями .
Формат вывода: для каждого теста — строка из чисел, ответ на каждый вопрос.
Разберём на примере
Первый тест: , вопросы .
Обратите внимание: массив приростов не отсортирован ( — не по возрастанию). Наивная мысль «раз ноги длиной , поднимемся, пока прирост ступени » работает, но подниматься нужно последовательно с первой ступени, а не выбирая ступени в удобном порядке.
- : ступень 1 — прирост 1, ОК (высота 1). Ступень 2 — прирост 2 > 1, стоп. Ответ — 1.
- или : ступени 1, 2, 3 — приросты , все , ОК. Ступень 4 — прирост 5 > 4, стоп. Высота 4.
- или : все приросты . Высота 9.
Появляется наблюдение: чтобы дойти до ступени , нужно, чтобы ВСЕ приросты были , а не только . Иначе подъём остановился бы раньше. То есть условие достижимости ступени — это max(a_1, ..., a_j) <= k.
Идея решения
Ключевое наблюдение: обозначим — префиксный максимум. Эта последовательность всегда неубывающая, независимо от того, как выглядит исходный массив . Ступень достижима при длине ног тогда и только тогда, когда .
Раз неубывает, множество достижимых индексов — это всегда префикс для наибольшего , при котором (или , если даже ). А раз отсортирован (неубывает) — искать такое можно бинарным поиском за на запрос, вместо перебора ступеней для каждого вопроса.
Ответ на вопрос — это префиксная сумма (или 0, если ).
Почему это корректно (доказательство): если ступень достижима, то по определению подъёма все ступени пройдены без остановки, то есть — а значит и их максимум . Обратное тоже верно: если , то каждый отдельный для , значит ни на одной из ступеней подъём не прервётся. Индукция по замыкает доказательство: достижимость ступеней — это ровно префикс, определяемый условием на префиксный максимум.
Строим два вспомогательных массива за один проход — (префиксный максимум) и (префиксная сумма) — а дальше каждый запрос обрабатывается независимым бинарным поиском по .
Псевдокод
для каждого теста:
прочитать n, q, массив a[1..n], массив k[1..q]
M[0] = -бесконечность (или 0, т.к. a_i >= 1)
S[0] = 0
для i от 1 до n:
M[i] = max(M[i-1], a[i])
S[i] = S[i-1] + a[i]
для каждого запроса k_i:
m = наибольший индекс j такой, что M[j] <= k_i
(бинарный поиск upper_bound по неубывающему массиву M)
если такого j нет:
ответ = 0
иначе:
ответ = S[m]
вывести ответ
Код решения
Комментарии по реализации. Оба варианта строят два вспомогательных массива за один линейный проход, а дальше каждый запрос обрабатывается независимым бинарным поиском: Python — bisect_right из стандартной библиотеки на списке prefix_max; C++ — std::upper_bound на vector<long long>. В обоих случаях bisect_right/upper_bound возвращают индекс первого элемента, который строго больше искомого значения — то есть количество элементов , что совпадает с наибольшим достижимым напрямую, без ручной коррекции границ.
Проверка на примерах
Тест 1: → , .
| элементов () | ответ | ожидание | |
|---|---|---|---|
| 1 | 1 | 1 | |
| 2 | 3 | 4 | |
| 4 | 3 | 4 | |
| 9 | 4 | 9 | |
| 10 | 4 | 9 |
Совпадает: 1 4 4 9 9.
Тест 2: → , .
| ответ | ожидание | ||
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 2 | 2 |
Совпадает: 0 2.
Тест 3: → , .
При : , ответ . Совпадает с ожиданием.
Все три теста сошлись.
Крайние случаи
- . Поскольку по ограничениям , ни одна ступень не достижима — ответ всегда 0.
- Все приросты равны и равны . Достижимы все ступени, ответ — сумма всего массива.
- Массив не отсортирован произвольно (как в примере): алгоритм не требует сортировки исходного — работает именно предвычисленный префиксный максимум.
- Большая сумма. При у верхней границы сумма высоты может достигать — не помещается в 32-битный
int, нуженlong long/64-битный тип.
Типичные ошибки
intвместоlong longв C++. Суммы до переполняют 32-битныйint.- Попытка бинарного поиска прямо по исходному массиву , который не отсортирован — работает только по префиксному максимуму .
bisect_left/lower_boundвместоbisect_right/upper_bound. Первый вариант ищет первый элемент , что даёт не то количество при повторяющихся значениях в — нужен именно вариант «строго больше», считающий все элементы .- Забыть обработать случай (ни одна ступень не достижима) — индексация
S[m-1]приm=0уйдёт в отрицательный индекс или undefined behaviour. - Считать вопросы не независимо — условие в статье явно требует отвечать на каждый вопрос отдельно, без накопления состояния между вопросами одного теста.
Сложность
Время: на тест (линейный проход для построения , и бинарных поисков). Память: .
Задача 2 — CF ~1300 — «T-primes»
Что дано
Простое число — число ровно с двумя различными положительными делителями. По аналогии определим T-простое число — число ровно с тремя различными положительными делителями.
Дан массив из чисел . Для каждого нужно определить, является ли оно T-простым.
Ограничения: , .
Формат ввода: первая строка — ; вторая строка — чисел .
Формат вывода: строк: YES, если число T-простое, иначе NO.
Разберём на примере
Дан массив .
- : делители — ровно три. YES.
- : делители — два (обычное простое). NO.
- : делители — четыре. NO.
Замечаем: единственное число из трёх, у которого ровно три делителя, — это , квадрат простого числа .
Идея решения
Утверждение: число имеет ровно три различных делителя тогда и только тогда, когда для некоторого простого .
Доказательство. В одну сторону: если ( — простое), делители — это , и все три различны (так как ). Значит, ровно три делителя.
В обратную сторону: делители числа обычно разбиваются на пары , кроме случая , когда пара «схлопывается» в один делитель. Нечётное количество делителей (три — нечётно) возможно только если — точный квадрат, . Далее раскладываем на простые множители: если , то , и число делителей равно — произведение нечётных чисел. Это произведение равно ровно 3 только когда в произведении один множитель равен 3 (то есть один простой множитель с ), а остальных множителей нет вовсе (иначе произведение было бы больше 3, так как каждый множитель , а при ). Значит, состоит из ровно одного простого множителя в первой степени, то есть — само простое число, и .
Алгоритм: для каждого проверить, является ли оно точным квадратом , и если да — проверить, простое ли . Поскольку , то . Значит, достаточно построить решето Эратосфена для чисел до один раз — и дальше каждая проверка простоты занимает .
Псевдокод
MAXR = 10^6
построить решето Эратосфена is_prime[0..MAXR]
(is_prime[0] = is_prime[1] = ложь, дальше стандартное решето)
прочитать n, массив x[1..n]
для каждого x_i:
s = целочисленный корень из x_i (округление вниз, точное)
если s * s == x_i и is_prime[s]:
вывести YES
иначе:
вывести NO
Код решения
Комментарии по реализации. Python — math.isqrt считает целочисленный корень точно, без ошибок округления floating point, поэтому дополнительная коррекция не нужна. C++ — sqrtl на long double для чисел до обычно даёт верный результат, но у плавающей точки на границах точных квадратов возможна погрешность в 1, поэтому добавлена явная коррекция s в обе стороны перед сравнением s * s == x. Решето строится один раз до , а не до — этого достаточно, так как .
Проверка на примерах
| (целое) | ? | is_prime[s]? | Ответ | Ожидание | |
|---|---|---|---|---|---|
| 4 | 2 | да | да (2 — простое) | YES | YES |
| 5 | 2 | нет () | — | NO | NO |
| 6 | 2 | нет () | — | NO | NO |
Совпадает.
Крайние случаи
- . , — точный квадрат, но не является простым числом (у него всего один делитель) — решето должно явно пометить
is_prime[1] = 0. Ответ — NO. - Точный квадрат составного числа. Например, — квадрат, но не простое → NO. Важно не путать « — точный квадрат» с « — T-простое»: нужны оба условия.
- Верхняя граница . Если , то — граничное значение решета (включительно).
- Простое число рядом с границей решета, например (простое, близкое к ): — должно дать YES.
Типичные ошибки
intвместоlong long/64-бит для . Значения до не помещаются в 32-битныйint(максимум около ).- Погрешность плавающей точки при извлечении корня.
int(sqrt(x))в языках без встроенного целочисленного корня может дать результат на 1 меньше или больше истинного из-за округления — обязательна коррекция (или использование точной целочисленной функции вродеmath.isqrt). - Решето построено только до , а не до — забыли, что решето нужно строить до границы корня, а не до границы исходных чисел.
is_prime[1]не помечен как «не простое» — по умолчанию некоторые массивы инициализируются единицами, и без явногоis_prime[1]=0число 1 ошибочно посчитается простым.- Путаница «точный квадрат» и «T-простое». Проверка только
s*s==xбез проверки простотыsдаёт неверный ответ на составных квадратах вроде 36, 100, 144.
Сложность
Построение решета: , один раз. Обработка каждого числа: после решета (плюс на извлечение корня). Итого: .
Самостоятельная тренировка
Все четыре — на сегодняшнюю связку: решето для быстрого доступа к простым числам и бинарный поиск по готовому отсортированному массиву.
- Codeforces 271B «Prime Matrix» (~CF 1300) — дана матрица; за один ход разрешается увеличить любой её элемент на единицу, нужно минимальным числом ходов добиться, чтобы в матрице появилась строка или столбец целиком из простых чисел.
- Codeforces 776B «Sherlock and his girlfriend» (~CF 1200) — нужно раскрасить числа от 2 до в минимальное количество цветов так, чтобы ни одно число не оказалось одного цвета со своим простым делителем.
- Codeforces 492B «Vanya and Lanterns» (~CF 1200) — дан набор фонарей на улице длины ; нужно найти минимальный радиус освещения, при котором вся улица оказывается покрыта светом.
- Codeforces 123A «Prime Permutation» (~CF 1300) — по строке из строчных латинских букв определить, можно ли переставить символы так, чтобы для каждого простого числа все позиции, кратные , содержали одинаковый символ.
Прорешай их самостоятельно — если застрянешь, разбери с AI-партнёром на codepal.ru: он не даёт готовый ответ, а подводит к решению вопросами.
Что дальше
На следующей тренировке — последняя сессия «разогрева»: сортировка в связке с бинарным поиском и хэш-таблица, снова около CF ~1300, а после неё — переход на «плато» с более длинными и составными задачами.