Что тренируем сегодня
В прошлой сессии мы разбирали первую жадность и сортировку по остаткам — сегодня остаёмся на сортировке, но добавляем к ней второй инструмент, без которого дальше никуда: бинарный поиск по уже отсортированным данным. Это не тот бинарный поиск «по ответу», с которым вы познакомитесь в сессии 5 — здесь всё проще и приземлённее: у нас есть отсортированный массив, и мы ищем в нём границу (первый элемент больше X, последний элемент не больше X и так далее) вместо линейного перебора.
Вторая идея сегодня — суффиксный массив предвычислений: вместо того чтобы на каждый запрос пересчитывать что-то с нуля за , мы один раз проходим по массиву справа налево и запоминаем ответ для каждой позиции. Это простой, но фундаментальный приём: если запросов много ( и больше), one-pass-предвычисление превращает в — при разница на несколько порядков.
Обе задачи сегодня — на уровне CF ~1100, оперируют похожей структурой «дан массив + много запросов», но решаются принципиально разными техниками. Разбор именно этой пары должен закрепить, что первый вопрос при виде фразы «ответьте на запросов» — это не «как решить один запрос», а «как один проход предвычислений закрыть все запросы разом».
Теория
Бинарный поиск по отсортированному массиву. Массив отсортирован один раз, а дальше приходит много одинаковых по смыслу запросов — вместо перебора каждый раз ищем границу за .
Когда применять:
- массив не меняется между запросами;
- запросов много, и вопрос один и тот же для разных значений («сколько элементов », «где граница между меньшими и большими»).
Как решать:
- Сортируем массив один раз — .
- На каждый запрос ищем границу бинарным поиском — .
- Используем готовые функции:
upper_bound/bisect_rightдля «»,lower_bound/bisect_leftдля «» — вручную цикл писать не нужно.
Где ошибаются: путают, какая из двух функций нужна — нестрогое и строгое условие дают разный ответ при повторяющихся значениях в массиве, и разница видна только на тестах с дубликатами.
Суффиксное (или префиксное) предвычисление за один проход. Когда запрос звучит как «что происходит от позиции и до конца» — не считаем это заново на каждый запрос, а один раз проходим по массиву и запоминаем ответ для каждой позиции.
Когда применять:
- в условии — «суффикс»/«начиная с позиции» или «префикс»/«до позиции»;
- запросов много, и наивный пересчёт на каждый запрос дал бы на запрос, итого .
Как решать:
- Замечаем: соседние суффиксы (или префиксы) отличаются ровно одним элементом.
- Идём по массиву один раз в нужную сторону, накапливая ответ.
- Дальше каждый запрос — просто чтение готового значения за .
Направление прохода должно совпадать с направлением накопления: слева направо для префиксов, справа налево для суффиксов.
Где ошибаются: перепутывают направление прохода и получают «зеркальный» результат.
Задача 1 — CF ~1100 — «Interesting drink»
Что дано
В городе есть магазинов, продающих один и тот же напиток. В -м магазине бутылка стоит монет. Участник планирует покупать напиток дней подряд — в -й день у него есть ровно монет. Для каждого дня нужно вывести, в скольких магазинах напиток по карману (цена в магазине доступной суммы в этот день).
Ограничения:
- — число магазинов;
- — цена бутылки в каждом магазине;
- — число дней;
- — сумма денег в -й день.
Формат ввода:
n
x_1 x_2 ... x_n
q
m_1
m_2
...
m_q
Формат вывода: целых чисел — для каждого дня количество магазинов, где напиток по карману.
Разберём на примере
Возьмём пять магазинов с ценами и четыре запроса: .
Отсортируем цены по возрастанию: .
Для первого запроса () — ни одна цена не , ответ .
Для второго запроса () — в отсортированном массиве элементы — это , их четыре. Заметьте: искать «сколько элементов » в отсортированном массиве — это ровно то же самое, что найти позицию, где кончается «блок маленьких» и начинается «блок больших», то есть найти границу за вместо перебора всех цен.
Для третьего запроса () — элементы : только сама тройка, ответ .
Для четвёртого () — все пять элементов , ответ .
Итоговый ответ по всем дням: 0 4 1 5 — это в точности официальный пример условия задачи.
Идея решения
Алгоритм в одну фразу: отсортировать цены один раз, а для каждого запроса бинарным поиском найти количество цен (в терминах стандартных библиотек — upper_bound).
Почему это работает. Ключевое наблюдение: как только массив цен отсортирован, вопрос «сколько элементов ?» превращается в вопрос «где заканчивается префикс элементов ?» — а это ровно то, что ищет бинарный поиск по монотонному условию «» (условие «истинно» для всех элементов слева от границы и «ложно» для всех элементов справа, потому что массив отсортирован). Позиция этой границы и есть искомое количество — не нужно ничего складывать или явно проверять каждый элемент.
Сложность одного запроса — вместо при переборе. При разница принципиальна: операций (перебор) против операций (бинарный поиск) — на несколько порядков быстрее, укладывается в лимит времени с огромным запасом.
Важно не путать upper_bound и lower_bound. upper_bound(x, m) находит первую позицию, где элемент строго больше — и эта позиция как раз равна количеству элементов (все элементы до неё по определению отсортированности). lower_bound(x, m) даёт первую позицию, где элемент — это количество элементов строго меньше , что здесь неверно: условие задачи — «цена не выше суммы», то есть нестрогое .
Псевдокод
прочитать n
прочитать массив x[1..n]
отсортировать x по возрастанию
прочитать q
для каждого запроса m:
ans = позиция первого элемента x, строго большего m
(эквивалентно: количество элементов x, которые <= m)
вывести ans
Код решения
Комментарии по реализации.
- Python —
bisect_rightиз стандартной библиотекиbisectработает так же, как C++upper_bound: возвращает позицию первого элемента, строго большего искомого значения. - C++ —
upper_boundтребует отсортированного диапазона (уже обеспечено сортировкой выше) и работает за ; результат — итератор, разница сbegin()даёт нужный индекс-количество. - Оба решения читают весь ввод целиком (
sys.stdin.buffer.read()в Python, буферизованныйcinсsync_with_stdio(false)в C++) — при до построчное чтение сinput()в Python было бы заметно медленнее.
Проверка на примерах
| Вход | Ожидание | Наш ответ |
|---|---|---|
| , цены , запросы | 0 4 1 5 | отсортированные цены : для — 0 элементов ; для — 4 элемента (); для — 1 элемент; для — все 5 ✓ |
Оба решения (Python и C++) прогнаны на этом входе и дают 0 4 1 5 — совпадает с ожиданием.
Крайние случаи
- меньше самой дешёвой цены. Ответ должен быть —
upper_bound/bisect_rightкорректно возвращает позицию , если искомое значение меньше всех элементов массива. - больше или равно самой дорогой цене. Ответ должен быть (все магазины по карману) — граница поиска возвращает позицию за концом массива, что и есть .
- Повторяющиеся цены. Если несколько магазинов продают по одинаковой цене, все они корректно учитываются:
upper_boundсчитает все вхождения значения, равного , как «», не пропуская дубликаты. - или . Вырожденные размеры не требуют отдельной обработки — сортировка массива из одного элемента и один бинарный поиск работают без исключений.
Типичные ошибки
lower_boundвместоupper_bound. Даёт количество элементов строго меньше вместо «» — на входах без повторов цены, равной , ошибка незаметна, но при повторяющихся ценах ответ занижается.- Забыть отсортировать массив перед бинарным поиском. Без сортировки
upper_bound/bisect_rightдают неопределённый, чаще всего неверный результат — предпосылка бинарного поиска (монотонность/отсортированность) должна быть явно обеспечена в коде, а не предполагаться из условия. - Линейный перебор всех цен на каждый запрос. Технически корректно, но при даёт операций суммарно — превышение лимита времени. Отсортировать один раз и искать бинарно — обязательное требование по объёму данных, а не просто оптимизация.
- Построчное чтение большого объёма ввода в Python через
input(). При строк запросов это заметно медленнее, чем чтение всего потока целиком и разбор по пробельным символам.
Сложность
Время: — сортировка плюс бинарных поисков по каждый. Память: на хранение массива цен и ответов.
Задача 2 — CF ~1100 — «Sereja and Suffixes»
Что дано
Дан массив из целых чисел . Дано запросов вида (): для каждого нужно вывести количество различных чисел среди (то есть в суффиксе массива, начинающемся с позиции ).
Ограничения:
- ;
- .
Формат ввода:
n m
a_1 a_2 ... a_n
l_1
l_2
...
l_m
Формат вывода: строк — для каждого запроса количество различных чисел в соответствующем суффиксе.
Разберём на примере
Массив: (10 элементов), запросы — все позиции от 1 до 10.
Наивный подход — на каждый запрос пройтись по суффиксу и посчитать различные числа через множество. Это работает, но за на запрос, суммарно — при это операций, слишком медленно.
Ключевое наблюдение: суффикс, начинающийся с позиции , отличается от суффикса, начинающегося с позиции , ровно одним дополнительным элементом — . Значит, если мы уже знаем количество различных чисел в суффиксе с позиции , то ответ для позиции получается почти бесплатно: это то же самое количество, плюс единица, если ещё не встречался в суффиксе с позиции (то есть встречается здесь впервые, если идти справа налево).
Пройдём по примеру руками справа налево, храня множество уже увиденных чисел и счётчик различных:
| Позиция (справа налево) | Уже видели? | Счётчик различных на этой позиции | |
|---|---|---|---|
| 10 | 99999 | нет | 1 |
| 9 | 100000 | нет | 2 |
| 8 | 4 | нет | 3 |
| 7 | 3 | нет | 4 |
| 6 | 2 | нет | 5 |
| 5 | 1 | нет | 6 |
| 4 | 4 | да (уже было на позиции 8) | 6 |
| 3 | 3 | да | 6 |
| 2 | 2 | да | 6 |
| 1 | 1 | да | 6 |
Ответы для запросов : 6 6 6 6 6 5 4 3 2 1 — совпадает с официальным примером условия.
Идея решения
Алгоритм в одну фразу: пройти по массиву один раз справа налево, поддерживая множество/булев массив «встречалось ли значение», и на каждой позиции сохранить текущее количество различных чисел в суффиксе — это и есть ответ на запрос с этой позицией.
Почему это работает. Определим — количество различных чисел в суффиксе, начинающемся с позиции . По определению (пустой суффикс). Переход: , если не встречается среди (то есть добавление увеличивает множество различных значений), и , если уже встречался в этом суффиксе (тогда добавление не меняет множество различных значений — оно уже там). Проверка «встречался ли в суффиксе с » делается быстро, если идти именно справа налево и поддерживать множество уже увиденных значений: к моменту обработки позиции множество как раз и содержит все значения из суффикса .
Это классический пример суффиксного предвычисления: раз мы всё равно должны просмотреть весь массив хотя бы один раз, чтобы ответить хоть на один запрос корректно, — выгоднее сделать это один раз для всех позиций сразу, а не по отдельности для каждого запроса. После предвычисления массива каждый запрос отвечается за простым обращением по индексу.
Псевдокод
прочитать n, m
прочитать массив a[1..n]
seen = пустое множество (или булев массив размера maxValue+1)
d[n+1] = 0
для i от n до 1 (включительно, по убыванию):
если a[i] не в seen:
добавить a[i] в seen
d[i] = d[i+1] + 1
иначе:
d[i] = d[i+1]
для каждого запроса l:
вывести d[l]
Код решения
Комментарии по реализации.
- Python — булев массив
seenфиксированного размера (по ограничению ) быстрее, чемset(), за счёт отсутствия хеширования; при желанииset()тоже работает корректно, но чуть медленнее на больших . - C++ —
vector<char>вместоvector<bool>дляseen:vector<bool>в C++ — специализация с побитовой упаковкой, которая имеет более высокую константу на операциях чтения/записи,charбыстрее и понятнее для простого флага. - Массив имеет размер , а не , чтобы безопасно обращаться к (база рекурсии для последней позиции) без выхода за границы.
Проверка на примерах
| Вход | Ожидание | Наш ответ |
|---|---|---|
| , , запросы | 6 6 6 6 6 5 4 3 2 1 | построение справа налево даёт ровно эту последовательность (см. таблицу разбора выше) ✓ |
Оба решения (Python и C++) прогнаны на этом входе и дают 6 6 6 6 6 5 4 3 2 1 — совпадает с ожиданием.
Крайние случаи
- (запрос на последний элемент). Суффикс состоит из одного элемента — ответ всегда . В таблице выше это позиция 10, значение .
- (запрос на весь массив). Ответ равен общему количеству различных значений во всём массиве — в примере это .
- Все элементы массива одинаковые. Тогда для любой позиции (кроме случая, когда массив пуст, что по ограничениям не происходит) — каждый суффикс содержит ровно одно различное значение.
- Все элементы массива различны. Тогда — суффикс с позиции содержит ровно столько различных значений, сколько в нём элементов, потому что повторов нет вовсе.
Типичные ошибки
- Проход слева направо вместо справа налево. Задача явно про суффиксы («от до конца массива») — переход определён именно в направлении справа налево; если считать слева направо, придётся хранить совсем другую структуру (префиксы), не отвечающую на вопрос напрямую.
- Забыть очистить/обнулить
seenмежду тестами, если решение оборачивается в цикл по нескольким независимым тестовым случаям (в данной задаче тестовый случай один на весь ввод, но при переносе техники на многотестовые задачи — частая причина неверного ответа во втором и последующих тестах). - Наивный пересчёт множества различных значений на каждый запрос заново. Даёт на запрос и суммарно — при это операций, превышение лимита времени. Одно предвычисление за обязательно.
- Индексация массива без учёта, что запросы 1-индексированные. В условии отсчитывается от до — массив предвычислений должен быть согласован с этой же индексацией (в приведённом коде и оба явно 1-индексированы,
a[i]соответствует из условия).
Сложность
Время: — один проход по массиву для построения , плюс на каждый из запросов. Память: — массив плюс булев массив «встречалось».
Самостоятельная тренировка
Обязательный раздел — 4 задачи того же уровня, которые нужно прорешать самостоятельно, без разбора здесь:
- Codeforces 313B «Ilya and Queries» (~CF 1100) — дана строка из символов
.и#и несколько запросов вида «сколько соседних одинаковых пар символов на отрезке [l, r)». - Codeforces 1327A «Sum of Odd Integers» (~CF 1100) — для нескольких пар чисел определить, можно ли представить как сумму различных нечётных положительных чисел.
- Codeforces 166A «Rank List» (~CF 1100) — по списку результатов команд (число решённых задач и штрафное время) найти, сколько команд делят между собой заданное место в итоговой таблице.
- Codeforces 82A «Double Cola» (~CF 1100) — пятеро друзей стоят в очереди, и после каждого напитка человек «раздваивается» и оба его дубля встают в конец очереди; нужно определить, кто выпьет банку номер .
Все четыре задачи используют идею предвычисления префиксов/сумм за один проход или прямого счёта позиции — тот же принцип, что в разобранной сегодня «Sereja and Suffixes», только в другой форме. Прорешай их самостоятельно — если застрянешь, разбери с AI-партнёром на codepal.ru: он не даёт готовый ответ, а подводит к решению вопросами.
Что дальше
На следующей тренировке остаёмся на уровне CF ~1100, но переходим от бинарного поиска по готовому массиву к принципиально другой идее — бинарному поиску по ответу, когда мы перебираем не элемент массива, а сам ответ, и проверяем его монотонным предикатом.