«Бинпоиск по ответу» — техника, у которой каркас один (см. серию «Алгоритмические основы» и тему про инвариант). Растущая сложность задач — это не сложность шаблона, а сложность предиката can(x). На полосе CF 1200 проверка укладывается в одну строку. На полосе 1500 — в жадный обход массива с обоснованием. На полосе 2000 — в нетривиальный подсчёт с участием сортировки и анализа сразу нескольких элементов.
Эта статья — траектория тренировки на бинпоиск по ответу. Берём три задачи разной полосы, для каждой подробно разбираем: как переформулировать условие, как сформулировать предикат, какие отсечения границ и какие особенности проверки.
Ядро техники
Базовая форма (см. соответствующую тему серии «Алгоритмические основы»):
lo, hi ← границы кандидатов
пока lo < hi:
mid ← (lo + hi + 1) / 2 # для max-true
или
mid ← (lo + hi) / 2 # для min-true
если can(mid):
сдвигаем границу к mid
иначе:
сдвигаем границу от mid
вернуть lo # = hi
Где can(mid) — бинарный предикат «достижим ли ответ mid». Содержательная часть техники — именно can.
Ступень 1 (~CF 1200): Aggressive Cows
Условие
Дано позиций на прямой (все различны, целые, отсортированы). Нужно расставить объектов в эти позиции () так, чтобы минимум попарных расстояний между расставленными объектами был максимален. Найти это максимальное минимальное расстояние.
Ограничения. , .
Пример. , . Возможные тройки и их минимумы попарных расстояний:
- : .
- : .
- : .
- : .
- … и т.д.
Ответ — .
Выбор инварианта
Переформулировка: «при фиксированном , можно ли расставить объектов так, чтобы каждая пара соседних была на расстоянии ?». Это can(d).
Монотонность: при — если достижимо, то тем более достижимо (то же расположение). Точка перехода единственная. Форма (A) max-true, ищем максимальное с can(d) = true.
Границы: (хотя всегда достижимо тривиально, нам важен «непрерывный» инвариант — отсечём вырожденный и стартуем с ). (больше максимально возможного расстояния между крайними точками).
Проверка
Жадная: ставим первый объект в , далее идём по позициям и ставим следующий объект в первую позицию, отстоящую от последнего поставленного на . В конце сравниваем число поставленных с .
cnt ← 1
last ← a[0]
для x из a[1..]:
если x - last ≥ d:
cnt ← cnt + 1
last ← x
вернуть cnt ≥ k
Почему жадно — оптимально? Допустим, существует расстановка из объектов с расстояниями , где первый объект стоит не в . Сдвинем его в — расстояния от первого до второго не уменьшатся (потому что мы сдвинули влево). Аналогично для второго: если он стоит правее «первой позиции с расстоянием » — сдвинем влево; расстояние ко второму увеличится, расстояние к третьему не уменьшится. По индукции — жадный выбор всегда даёт ровно столько же или больше объектов, чем любая другая расстановка.
Код
Что характерно для ступени 1
- Проверка — одна линейная пробежка. Никакой структуры данных, просто счётчик.
- Жадность очевидна. Доказательство exchange argument умещается в одну фразу.
- Сложность. на сортировку + на бинпоиск. Всё дешёво.
Ступень 2 (~CF 1500): минимизация времени с проверкой по сумме делений
Условие
В сервисе операторов с темпами — оператор обрабатывает один запрос за секунд (последовательно). В очереди запросов. Найти минимальное целое время , за которое все запросов будут обработаны, при условии, что операторы работают параллельно и каждый берёт следующий запрос сразу, как освободится.
Ограничения. , , .
Пример. , . За время оператор обработает запросов:
- : < 5 — нет.
- : ≥ 5 — да.
Ответ — .
Выбор инварианта
Переформулировка: «при фиксированном , можно ли обработать запросов?». Это can(T) = (sum(T // t_i) >= m).
Монотонность: при — каждое слагаемое не уменьшается, сумма не уменьшается. Если can(T_1), то can(T_2). Форма (B) min-true.
Границы: , (один самый быстрый оператор сам справится за это время).
Проверка
sum ← 0
для t_i из t:
sum ← sum + T // t_i
если sum ≥ m:
вернуть истина
вернуть истина если sum ≥ m, иначе ложь
Простая, но требует внимания: может переполнять int при . В этой задаче ограничения мягче, но привычка использовать long long — полезна.
Код
Что характерно для ступени 2
- Проверка — сумма по элементам, но требует осторожности с переполнением. может суммироваться в большое число, обязательно
long long. - Граница — продуктовая. Не «удвоить , пока не достигнет», а сразу
min(t) * m— упирается в самого быстрого оператора. Тонкое наблюдение, отличающее ступень 1 от ступени 2. - Сложность. — лог от это , итого операций при .
Ступень 3 (~CF 2000): максимизация медианы через k операций
Условие
Дан массив из положительных целых чисел, нечётное. Дано целое — количество доступных операций. За одну операцию можно увеличить любой один элемент массива на . Найти максимальное возможное значение медианы массива (медиана — элемент на позиции в отсортированном порядке) после применения операций.
Ограничения. , нечётное, , .
Пример. , . Медиана — второй элемент в отсортированном порядке. Чтобы максимизировать, увеличиваем средний элемент: медиана . (Можно перейти в за 1 операцию, в за 2 операции — медианы соответственно 4 и 5.) Ответ — .
Пример 2. , . Медиана — третий элемент в отсортированном порядке. Нужно поднять верхние три элемента: позиции в отсортированном — . Чтобы медиана была , должно быть элементов . При : (4 операции), (2 операции), (0) — итого операций, всё ровно. Ответ — .
Выбор инварианта
Переформулировка: «при фиксированном , можно ли за операций добиться, чтобы элементов было ?». Это can(x).
Монотонность: при — если для нужно столько-то операций, для нужно не больше (любое поднятие до покрывает поднятие до ). Если can(x_2), то can(x_1). Форма (A) max-true.
Границы: (любое уже достигнутое значение, не превышающее), (даже потратив все операций на один элемент, не получим больше).
Проверка — содержательная часть
Сколько операций минимально нужно, чтобы элементов было ? Жадный ответ: отсортировать массив; верхнюю половину (включая медиану) — это позиции от до — поднять до . Поднимать ниже-стоящие элементы бесполезно: они меньше, операций потратили бы больше.
half ← (n + 1) / 2
sorted_a ← отсортированный a
cost ← 0
для i от n - half до n - 1:
если sorted_a[i] < x:
cost ← cost + (x - sorted_a[i])
если cost > k:
вернуть ложь
вернуть истина
Почему именно верхняя половина — оптимально? Допустим, есть допустимый план, в котором мы подняли не верхнюю половину, а какие-то другие элементов до . Тогда среди них есть хотя бы один элемент с индексом — он в отсортированном меньше каждого элемента из верхней половины. Заменим его в плане на любой нижний из «верхней половины, не достигший »: стоимость замены не вырастет (новый элемент уже не меньше старого, до ему «ближе»). По цепочке таких замен — приходим к плану, поднимающему именно верхнюю половину. Доказано оптимальность жадного — exchange argument.
Сложность проверки: после однократной сортировки.
Применение шаблона
Сортируем массив один раз, потом запускаем бинпоиск. Это важно: если сортировать на каждой проверке, — потенциально не пройдёт TL.
a ← отсортированный массив
lo, hi ← a[0], a[n-1] + k
пока lo < hi:
mid ← (lo + hi + 1) / 2
если can(mid):
lo ← mid
иначе:
hi ← mid - 1
вернуть lo
Код
Что характерно для ступени 3
- Проверка — не одна формула, а жадный план + обоснование. Это уже не «сумма по элементам», а «оптимальная стратегия использования операций».
- Подготовка данных вне
can. Сортировка один раз перед бинпоиском — важно для сложности. На ступени 1 сортировка тоже была, но тривиальная; здесь её роль — основа корректности жадного. - Доказательство оптимальности — exchange argument на 2–3 строки. Без него жадный — догадка.
- Сложность. .
Сводная таблица
| Ступень | CF-полоса | Что проверяем can(x) | Сложность одной проверки | Что добавилось по сравнению с предыдущей |
|---|---|---|---|---|
| 1 | ~1200 | Можно ли расставить объектов с попарным расстоянием | Базовый шаблон, жадная проверка-пробежка | |
| 2 | ~1500 | Содержательный выбор границы , аккуратность с переполнением | ||
| 3 | ~2000 | За операций сделать элементов | после сортировки | Подготовка данных вне can, обоснование жадного через exchange argument |
Сама форма цикла — одинаковая на всех трёх ступенях. Меняется только can. И именно can — фокус внимания при росте сложности.
Типичные ловушки по ступеням
Ступень 1. Зацикливание из-за неправильного округления mid (см. тему про бинпоиск по ответу). На массиве позиций — забыть отсортировать перед жадной проверкой.
Ступень 2. Переполнение при умножении для границы . Слишком тесный TL при — нужно sys.stdin.buffer.read в Python, иначе ввод съест половину времени.
Ступень 3. Сортировать массив внутри can — на каждую проверку, итого ≈ , потенциальный TL в Python. Поднимать неправильную половину массива (нижнюю, центральную, всю) — типичный WA до того, как обосновал, какую именно половину поднимать. Забыть, что нечётное и медиана — это позиция (0-индексация: ), а не середина в обычном смысле.
План тренировки
Если только знакомитесь с бинпоиском по ответу — порядок такой:
- Прочесть тему «Бинпоиск по ответу: когда применять и как выбирать инвариант» (серия «Алгоритмические основы»). Освоить базовый шаблон, обе формы цикла и принцип монотонности.
- Решить 2–3 задачи уровня ступени 1 — Aggressive Cows, распил досок (см. тему), минимизация с простой проверкой. Цель — научиться формулировать
canза 5 минут от прочтения условия. - Перейти к ступени 2 — задачи, где граница требует продуктового рассуждения, или где проверка требует аккуратной арифметики (
long long,ceil/floor). - Ступень 3 — задачи, где
canсама нетривиальна: содержит жадный, требует exchange argument, или вообще DP внутри. На этой полосе техника бинпоиск по ответу перестаёт быть отдельной — она становится каркасом, в который вкладывается отдельный алгоритм.
Признак, что ступень освоена: при виде нового условия в соответствующей полосе CF — за 5 минут формулируете can, доказываете монотонность, выбираете границы. Дальше — техника.
Итого
- Каркас один: цикл бинпоиска по диапазону кандидатов с предикатом
can(x). - Растёт сложность
can. От «сумма по массиву» до «жадный план с обоснованием через exchange argument». От до после -подготовки. - Растёт сложность выбора границ. От « и » до « как продуктовая оценка».
- Содержательная часть — не цикл, а
can. Освоить технику — значит освоить не четырёхстрочный цикл, а сотни возможных способов записатьcanдля разных задач.
После этой стратегии полезно вернуться к теме «Бинпоиск по ответу: когда применять и как выбирать инвариант» и пройти её ещё раз — многие тонкости станут понятнее именно после трёх практик.