О чём статья
Бинпоиск по ответу — техника, после освоения которой десятки задач полосы 1400–1900 становятся понятными по одной схеме. Идея в одно предложение: вместо того чтобы конструировать ответ, мы угадываем его половинным делением, и на каждом шаге задаём про кандидата только один вопрос — «достижим или нет?».
Эта статья — не разбор конкретной задачи. Цель — показать, по каким сигналам в условии узнавать технику, как формулировать инвариант проверки, чем различаются две формы шаблона («ищу максимум, при котором проверка ещё проходит» и «ищу минимум, при котором проверка уже проходит»), и какие ловушки границ съедают часы отладки даже у тех, кто уверенно пишет обычный бинпоиск по массиву. Базовый шаблон разбирается на двух задачах нарастающей сложности; глубже техника раскрывается в отдельных разборах серии.
Когда применять: сигналы в условии
Бинпоиск по ответу уместен, когда ответ — это число (часто целое, иногда вещественное), и про него можно сказать предикат «такое значение достижимо», который меняет значение ровно один раз при движении по диапазону кандидатов. Узнаваемые маркеры:
- «Найти максимальный , при котором …» или «найти минимальный , при котором …». Слово «максимальный/минимальный» — прямой сигнал к перебору ответа, а формат «при котором» обычно прячет внутри проверку.
- «Минимизировать максимум» или «максимизировать минимум». Классическая структура: ответ — это значение функции, аргумент которой — план действий, и план оптимизируется по min/max от чего-то локального. Почти всегда сводится к «зафиксируй → проверь, существует ли план, в котором локальный максимум (или локальный минимум )».
- Прямая конструкция «плохо параметризуется». Условие даёт сложную систему ограничений; перебрать все планы — экспонента; но «зафиксировать ответ и проверить план» — линейное или почти линейное.
- Большой диапазон ответа, но дешёвый предикат. Ответ может быть до , а проверка работает за или — тогда от диапазона помещается в TL, а диапазон — нет.
- «За какое минимальное время / минимальное число шагов / минимальный ресурс …» — частный случай. Время/шаги/ресурс — кандидат, бинпоиск по нему.
Если вместо отдельных сигналов в условии видно сразу «минимизируй максимум» или «максимизируй минимум» — стоит начать решение с бинпоиска по ответу, даже если первая мысль была про DP или жадный. Часто эта формулировка превращает «сложную задачу про план» в «простую задачу про проверку».
Базовый шаблон
Идея в одну фразу
Завести диапазон кандидатов для ответа и функцию-предикат can(x). Сужать диапазон половинным делением, опираясь на единственное свойство — монотонность предиката: внутри диапазона есть ровно одна точка перехода false → true (или true → false).
Два варианта формы
Бинпоиск по ответу почти всегда сводится к одной из двух форм:
(A) «Ищу максимальный , при котором can(x) = true». Предикат истинен на префиксе и ложен на суффиксе. Ответ — правая граница истинного префикса.
(B) «Ищу минимальный , при котором can(x) = true». Предикат ложен на префиксе и истинен на суффиксе. Ответ — левая граница истинного суффикса.
Это не разные техники, а одна и та же — но с разными ответами на одинаковые mid. Распознавать форму важно потому, что от неё зависит направление сдвига границ и способ вычисления mid: иначе шаблон зациклится на .
Псевдокод формы (A) — максимум среди удовлетворяющих
lo ← минимально возможный ответ
hi ← максимально возможный ответ
пока lo < hi:
mid ← (lo + hi + 1) / 2 // округление вверх — важно для (A)
если can(mid):
lo ← mid // mid сам кандидат, граница включает его
иначе:
hi ← mid - 1 // mid не подходит, отбрасываем
вернуть lo // = hi
Псевдокод формы (B) — минимум среди удовлетворяющих
lo ← минимально возможный ответ
hi ← максимально возможный ответ
пока lo < hi:
mid ← (lo + hi) / 2 // округление вниз — важно для (B)
если can(mid):
hi ← mid // mid сам кандидат, граница включает его
иначе:
lo ← mid + 1 // mid не подходит, отбрасываем
вернуть lo // = hi
Почему именно так округляется mid
Когда и соседние (), цикл должен завершиться за один шаг. В форме (A) (lo + hi) / 2 с округлением вниз даёт , и при can(lo) = true строка lo = mid оставляет на месте — бесконечный цикл. Округление вверх даёт : при can(hi) = true сразу , и цикл закрывается. Зеркально в форме (B): округление вверх ловит зацикливание, нужно округлять вниз.
Это самая частая ошибка новичка с бинпоиском по ответу. Лекарство — не пытаться вспоминать формулы, а держать в голове картину «соседние границы за один шаг»: для каждой формы проверить в уме, как ведёт себя цикл на .
Сложность шаблона
, где — сложность одной проверки. Для типичных задач: , диапазона (даже при ответе до ). Итого операций при — комфортно в любой TL.
Что делать с вещественным ответом
Если ответ — вещественное число (геометрия, оптимизация плотности), форма та же, но условие выхода — не , а фиксированное число итераций (типично 60–100 — этого хватит на 14–18 значащих цифр) или ширина . В этой статье вещественный случай не разбираем — обе задачи-иллюстрации целочисленные.
Как выбирать инвариант проверки
Это самая содержательная часть техники. «Написать бинпоиск» — лёгкая часть; «понять, что именно проверять» — основная работа. Несколько практических наводок.
Первый шаг: переформулировать вопрос как «можно ли … при …». Исходный вопрос — «найти максимальное , чтобы получить кусков». Переформулирован — «при фиксированном , можно ли получить кусков?». Второй вопрос — это и есть can(L). Если переформулировка вызывает затруднение — техника, скорее всего, не подойдёт.
Второй шаг: проверить монотонность. Если ответ на «достижим ли » меняется при увеличении ровно один раз — техника работает. Если меняется несколько раз (есть «плохие» внутри хороших) — это не бинпоиск по ответу, а что-то другое (часто DP или прямая конструкция). Доказательство монотонности на ревью занимает 1–2 строки; если оно не пишется — повод задуматься, точно ли это та техника.
Третий шаг: выбрать форму (A) или (B). Если максимизируем — (A); если минимизируем — (B). Это банально, но в условиях вида «минимизируй максимум» легко сбиться: мы минимизируем максимум, поэтому форма (B), а не (A); кандидат — значение максимума, а проверка — «существует ли план, в котором максимум ».
Четвёртый шаг: выбрать границы и . Простой принцип: — гарантированно недостижимый снизу ответ (или граница, при которой can тривиально истинна); — гарантированно достижимый сверху. Чем плотнее границы — тем меньше итераций (хотя log всё равно даст лимит). Иногда полезно сразу установить или подобную «физическую» нижнюю границу, чтобы избежать вырожденных случаев в can.
Задача-иллюстрация 1: распил досок (~CF 1400)
Условие
Есть деревянных досок целочисленной длины . Нужно нарезать из них не менее кусков одинаковой целочисленной длины . Каждая доска нарезается независимо: из доски длины получится ровно кусков длины , остаток (если есть) выбрасывается. Найти максимальное значение , при котором суммарно можно получить кусков. Если суммарно длин всех досок меньше , ответом считается .
Ограничения. , , .
Формат ввода. В первой строке — и . Во второй строке — целых чисел .
Формат вывода. Одно целое число — максимальное .
Пример. , , .
При : — ровно . При : — недостижимо. Значит, ответ — 200.
Выбор инварианта
Переформулировка: «при фиксированном , верно ли ?». Это и есть can(L).
Монотонность: при для каждой доски (с увеличением длины куска число кусков из той же доски не растёт). Значит, сумма по тоже не растёт; точка перехода «истина → ложь» единственная. Форма — (A), ищем максимальное с can(L) = true.
Границы: (при кусков ровно — если этого мало, отвечаем отдельной проверкой). (при из каждой доски получится 0 кусков, проверка тривиально ложна — нет смысла перебирать).
Применение шаблона
lo, hi ← 1, max(a)
если sum(a) < k:
вернуть 0
пока lo < hi:
mid ← (lo + hi + 1) / 2
cnt ← sum(a[i] / mid для всех i)
если cnt ≥ k:
lo ← mid
иначе:
hi ← mid - 1
вернуть lo
Сложность: , в нашем примере операций.
Проверка на примере
, . Сумма = 2541 , не вырожденный случай.
| шаг | действие | ||||
|---|---|---|---|---|---|
| 1 | 1 | 802 | 402 | ||
| 2 | 1 | 401 | 201 | ||
| 3 | 1 | 200 | 101 | ||
| 4 | 101 | 200 | 151 | ||
| 5 | 151 | 200 | 176 | ||
| 6 | 176 | 200 | 188 | ||
| 7 | 188 | 200 | 194 | ||
| 8 | 194 | 200 | 197 | ||
| 9 | 197 | 200 | 199 | ||
| 10 | 199 | 200 | 200 |
Итог: . ✓
Код решения
Что поменялось по сравнению с базовым шаблоном
- Конкретный предикат. Шаблонное
can(mid)стало одной строкой — суммой . Линейный проход, никаких структур. - Ранний выход. Цикл подсчёта прерывается, как только сумма достигла — это не меняет асимптотику, но даёт постоянный фактор: «лёгкие» кандидаты проверяются быстрее.
- Вырожденный случай. Отдельная проверка — без неё шаблон возвращал бы , что неверно (нельзя нарезать кусков из недостаточной суммарной длины).
- Тип данных в C++. до не помещается в
int. Сумма — до . Везде нуженlong long. В Python типы автоматически произвольной точности. midв C++. Записьlo + (hi - lo + 1) / 2— защита от переполненияlo + hi, если границы близки кLLONG_MAX. В этой задаче не критично, но привычка полезная.
Задача-иллюстрация 2: минимаксная разрезка массива (~CF 1700)
Условие
Дан массив из положительных целых чисел и целое (). Нужно разделить массив на ровно непустых непрерывных подмассивов так, чтобы максимум сумм этих подмассивов был минимален. Вывести этот минимум.
Ограничения. , , .
Формат ввода. В первой строке — и . Во второй строке — .
Формат вывода. Одно целое число — минимальное возможное значение максимальной суммы среди подмассивов.
Пример. , , .
Варианты деления на 2 подмассива:
| разрез | подмассивы | суммы | максимум |
|---|---|---|---|
| после 1 | 7, 25 | 25 | |
| после 2 | 9, 23 | 23 | |
| после 3 | 14, 18 | 18 | |
| после 4 | 24, 8 | 24 |
Минимум — 18.
Выбор инварианта
Прямой перебор разбиений — экспонента (). DP даёт , что на не помещается. Бинпоиск по ответу даёт .
Переформулировка: «при фиксированном , можно ли разрезать массив на не более подмассивов так, чтобы каждая сумма ?». Это и есть can(x).
Заметим важную деталь: исходный вопрос — про ровно подмассивов, а проверка — про не более . Почему это эквивалентно? Если получилось разрезать на подмассивов с суммой каждого , то любой подмассив можно дополнительно разрезать (отщепить от конца один элемент в новый подмассив — это не увеличит ни одну сумму). Значит, при деление на ровно подмассивов с тем же ограничением тоже существует. Поэтому проверка «» эквивалентна « при возможности дополнительно дробить».
Монотонность: при увеличении нужно столько же или меньше подмассивов (порог стал свободнее, любой работающий план для меньшего работает и для большего). Точка перехода — единственная. Форма — (B), ищем минимальное с can(x) = true.
Границы: — снизу не имеет смысла идти, потому что один элемент уже занимает подмассив, и его сумма не может быть меньше его значения. — сверху достаточно одного подмассива.
Жадная проверка
can(x): жадно набираем подмассив, пока сумма ; как только следующий элемент переполняет — закрываем подмассив и начинаем новый. В конце сравниваем число подмассивов с .
groups ← 1
cur ← 0
для v из a:
если cur + v ≤ x:
cur ← cur + v
иначе:
groups ← groups + 1
cur ← v
вернуть (groups ≤ k)
Почему жадно — оптимально? Допустим, есть решение с подмассивами, в котором первый подмассив короче, чем жадный. Заберём один элемент из второго подмассива и приклеим к первому — сумма первого не превысит (мы могли это сделать жадно — значит, помещается); сумма второго стала меньше — тоже . После цепочки таких обменов получим жадное деление с тем же числом подмассивов. Значит, жадное — оптимально по числу подмассивов при фиксированном пороге .
Сложность одной проверки: .
Применение шаблона
lo, hi ← max(a), sum(a)
пока lo < hi:
mid ← (lo + hi) / 2
если can(mid):
hi ← mid
иначе:
lo ← mid + 1
вернуть lo
Сложность: . На , до — порядка операций.
Проверка на примере
, . , .
| шаг | жадное деление | групп | ? | действие | |||
|---|---|---|---|---|---|---|---|
| 1 | 10 | 32 | 21 | 3 | нет | ||
| 2 | 22 | 32 | 27 | 2 | да | ||
| 3 | 22 | 27 | 24 | 3 | нет | ||
| 4 | 25 | 27 | 26 | 3 | нет |
Подробнее шаг 4: , добавляем 7 → 7; +2 → 9; +5 → 14; +10 → 24; +8 → 32 > 26, закрываем, новый = 8. Итог: подмассивы и ? Нет, проверим внимательнее. После 14 () пробуем добавить 10: , добавляем, . Пробуем добавить 8: — закрываем подмассив, , . В конце — да. Значит, я выше посчитал неправильно; пересчитаем шаги.
Перепроверим жадное деление руками:
: ; ; ; ; → новая, ; , . Итог 2 группы? Нет, в момент превышения мы увеличиваем groups с 1 до 2. После прохода . — да. Значит, достижимо, . (Я ошибся в первом проходе таблицы — на 21 уже всё помещается.)
Пересоставим таблицу аккуратно:
| шаг | проход | финальный | ? | действие | |||
|---|---|---|---|---|---|---|---|
| 1 | 10 | 32 | 21 | 2 | да | ||
| 2 | 10 | 21 | 15 | 3 | нет | ||
| 3 | 16 | 21 | 18 | 2 | да | ||
| 4 | 16 | 18 | 17 | 3 | нет |
Итог: . ✓ Совпадает с прямым перебором.
Код решения
Что поменялось по сравнению с базовым шаблоном
- Предикат не очевиден. В первой задаче
canнапрашивалась сразу из условия. Здесь её пришлось вывести через переформулировку «не более » вместо «ровно » и доказать эквивалентность дроблением подмассивов. Это типичный шаг в задачах на минимакс. - Жадная проверка требует обоснования. Простой жадный перебор «работает, потому что очевидно» — частая ошибка. На этой задаче — короткое доказательство exchange argument, всего пара строк, но без него не написать
canуверенно. - Нижняя граница не нулевая. — следствие физики задачи: ни один план не даст максимум меньше единственного элемента. Без этой нижней границы шаблон работал бы корректно, но лишние итерации съели бы лимит на больших данных.
- Ранний выход в
can. Как только , возвращаемfalse— это даёт постоянный фактор, особенно для маленьких . long longобязательно. Сумма до , — комбинация и . В C++ —long longвезде. В Python — типы автоматические.
Типичные ошибки
Не «ошибки в конкретной задаче», а именно ошибки в применении техники — те, что повторяются от задачи к задаче.
-
Зацикливание из-за неправильного округления
mid. Записьmid = (lo + hi) / 2с округлением вниз в форме (A) — классическая ловушка: при иcan(lo) = trueграница не сдвигается. Лекарство — не пытаться запоминать формулы, а проверять цикл на соседних границах для каждой задачи отдельно. -
Перепутанная форма (A) ↔ (B). В условиях вида «минимизируй максимум» легко выбрать форму (A) — потому что слово «максимум» в условии тянет. На самом деле мы минимизируем значение, форма (B). Различение: какое значение оптимизируем — таков и тип «оптимума».
-
Нет доказательства монотонности. Бинпоиск работает только если предикат меняется ровно один раз. Если про монотонность «и так понятно» — почти наверняка пропущен крайний случай. Полезное упражнение: явно доказать монотонность на 2 строки в комментарии к функции
can. -
Неправильные границы и . Слишком узкие — ответ может выпасть за пределы, шаблон вернёт неправильный результат. Слишком широкие — лишние итерации, в худшем случае переполнение
mid. Правильный приём: — гарантированно достижимый (или явно недостижимый, если форма позволяет), — гарантированно достижимый (или явно недостижимый). -
Переполнение
mid = (lo + hi) / 2. При границах около суммаlo + hiпереполняетlong long. Записьlo + (hi - lo) / 2илиlo + (hi - lo + 1) / 2(для формы A) — защищённая. -
can(x)имеет асимптотику хуже, чем кажется. Например, проверка через сортировку внутриcanдаёт на каждую итерацию — итого . Часто это нормально, но в задачах с — лимит уже жмёт. Если сортировку можно вынести за бинпоиск (отсортировать массив один раз) — это даёт ускорение на порядок. -
Пропущенный вырожденный случай. «Что, если ?» «Что, если массив пустой?» «Что, если сумма меньше ?» Бинпоиск как техника не защищает от вырожденных входов; их нужно отдельно проверять до запуска цикла или зашить в
can. -
Целочисленный бинпоиск с условием выхода через
lo + 1 < hi. Иногда пишут такой шаблон, чтобы избежать вопроса об округленииmid. Это работает, но требует дополнительной финальной проверки —can(lo)иcan(hi), выбрать правильный. Лишний шаг и лишний источник ошибок; форма «lo < hi» с правильным округлением — компактнее и реже ломается.
Когда бинпоиск по ответу НЕ подходит
Несколько ситуаций, когда техника выглядит подходящей, а на самом деле — нет.
-
Предикат не монотонный. Если внутри диапазона ответа есть «плохие» точки между «хорошими», бинпоиск даст случайный результат — он лишь найдёт какую-то точку перехода, не обязательно правильную. Полезный тест: если на маленьких данных перебрать все и нарисовать «достижимость», нет ли несвязного множества
true? Если есть — другая техника (DP, прямая конструкция, поток). -
Ответ — не число, а структура. «Найти раскраску графа с минимальным числом цветов» — ответ-число, но проверка «есть ли раскраска в цветов» — NP-полная задача. Бинпоиск переносит сложность в
can; еслиcanсама по себе сложна — техника не помогает. -
Проверка слишком дорогая. Если
canработает за , а , шаблон вылетит за TL. Бинпоиск спасает только когдаcanсильно дешевле, чем «прямое решение». -
Узкий диапазон с дешёвым прямым решением. Если ответ — это , или диапазон содержит 10 кандидатов — проще перебрать все, без шаблона.
-
Множественные ответы. Если задача требует не одно число, а пару (две границы интервала, несколько разрезов и т.п.) — бинпоиск даёт только одно. Иногда выручает двойной бинпоиск (бинпоиск по одному параметру при фиксированном другом), но это уже отдельная техника.
Связанные техники
Бинпоиск по ответу — каркас, на который ложатся несколько соседних подходов.
-
Жадный алгоритм — почти всегда внутри
can. Доказательство «жадная стратегия оптимальна по числу подмассивов / шагов / ресурсу» — отдельный навык; статья «Жадные алгоритмы и exchange argument» из этой же серии раскрывает его подробно. -
Two pointers / скользящее окно — частая структура
canв задачах про последовательности. Например, проверка «существует ли подсегмент с суммой длины » — естественно делается скользящим окном за . -
DP в
can. Иногда проверка «достижим ли » сама требует динамики. Например, «можно ли набрать сумму , выбрав не более непересекающихся подмассивов» —canрешается DP за . Тогда суммарная сложность — годится для до . -
Тернарный поиск. Когда функция «значение → ответ» не монотонна, а унимодальна (одна точка минимума или максимума) — бинпоиск не работает, но работает тернарный поиск (на каждом шаге сравнивает значения в двух средних точках). Шаблон похожий, идея другая.
-
Параметрический поиск (поиск по двойному инварианту). Когда ответ — отношение двух сумм по выборке элементов (например, «найти подмножество с максимальным средним»), используется бинпоиск по значению дроби с проверкой через сведение к линейной оптимизации. Это уже полоса 1900–2200.
<PostSeries> ниже выведет соседние статьи серии «Алгоритмические основы» автоматически.
Итого
- Техника: угадай ответ половинным делением, проверь предикат
can(x). - Два сигнала из условия: «найти максимальный/минимальный при котором …» и «минимизировать максимум / максимизировать минимум».
- Две формы шаблона: (A) max-true с ; (B) min-true с . Округление зависит от формы — иначе зацикливание.
- Сложность: . Обычно или .
- Главный навык: не написать цикл, а выбрать инвариант и доказать монотонность. Цикл — четыре строки.
- Типичные ловушки: зацикливание из-за округления
mid, перепутанная форма (A)↔(B), нечёткие границы , дорогаяcan, переполнениеmid. - Связанные техники: жадные (внутри
can), DP (внутриcanдля сложных проверок), скользящее окно, тернарный поиск (для унимодальных функций).