← Вернуться в блог

Стратегия победы: бинпоиск по ответу — траектория 1200 → 1500 → 2000 СТРАТЕГИЯ

  • binpoisk-po-otvetu
  • binary-search
  • monotonicity
  • trayektoriya
  • predicate-check
  • greedy

«Бинпоиск по ответу» — техника, у которой каркас один (см. серию «Алгоритмические основы» и тему про инвариант). Растущая сложность задач — это не сложность шаблона, а сложность предиката 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

Условие

Дано nn позиций на прямой a1<a2<<ana_1 < a_2 < \ldots < a_n (все различны, целые, отсортированы). Нужно расставить kk объектов в эти позиции (knk \le n) так, чтобы минимум попарных расстояний между расставленными объектами был максимален. Найти это максимальное минимальное расстояние.

Ограничения. 2kn1052 \le k \le n \le 10^5, 0ai1090 \le a_i \le 10^9.

Пример. a=[1,2,4,8,9]a = [1, 2, 4, 8, 9], k=3k = 3. Возможные тройки и их минимумы попарных расстояний:

  • {1,4,8}\{1, 4, 8\}: min(3,4,7)=3\min(3, 4, 7) = 3.
  • {1,4,9}\{1, 4, 9\}: min(3,5,8)=3\min(3, 5, 8) = 3.
  • {2,4,9}\{2, 4, 9\}: min(2,5,7)=2\min(2, 5, 7) = 2.
  • {1,8,9}\{1, 8, 9\}: min(7,1,8)=1\min(7, 1, 8) = 1.
  • … и т.д.

Ответ — 33.

Выбор инварианта

Переформулировка: «при фиксированном dd, можно ли расставить kk объектов так, чтобы каждая пара соседних была на расстоянии d\ge d?». Это can(d).

Монотонность: при d1<d2d_1 < d_2 — если d2d_2 достижимо, то d1d_1 тем более достижимо (то же расположение). Точка перехода единственная. Форма (A) max-true, ищем максимальное dd с can(d) = true.

Границы: lo=1\textit{lo} = 1 (хотя d=0d = 0 всегда достижимо тривиально, нам важен «непрерывный» инвариант — отсечём вырожденный d=0d = 0 и стартуем с 11). hi=ana1\textit{hi} = a_n - a_1 (больше максимально возможного расстояния между крайними точками).

Проверка

Жадная: ставим первый объект в a1a_1, далее идём по позициям и ставим следующий объект в первую позицию, отстоящую от последнего поставленного на d\ge d. В конце сравниваем число поставленных с kk.

cnt ← 1
last ← a[0]
для x из a[1..]:
    если x - last ≥ d:
        cnt ← cnt + 1
        last ← x
вернуть cnt ≥ k

Почему жадно — оптимально? Допустим, существует расстановка из kk объектов с расстояниями d\ge d, где первый объект стоит не в a1a_1. Сдвинем его в a1a_1 — расстояния от первого до второго не уменьшатся (потому что мы сдвинули влево). Аналогично для второго: если он стоит правее «первой позиции с расстоянием d\ge d» — сдвинем влево; расстояние ко второму увеличится, расстояние к третьему не уменьшится. По индукции — жадный выбор всегда даёт ровно столько же или больше объектов, чем любая другая расстановка.

Код

Что характерно для ступени 1

  • Проверка — одна линейная пробежка. Никакой структуры данных, просто счётчик.
  • Жадность очевидна. Доказательство exchange argument умещается в одну фразу.
  • Сложность. O(nlogn)O(n \log n) на сортировку + O(nlogamax)O(n \log a_{\max}) на бинпоиск. Всё дешёво.

Ступень 2 (~CF 1500): минимизация времени с проверкой по сумме делений

Условие

В сервисе nn операторов с темпами t1,t2,,tnt_1, t_2, \ldots, t_n — оператор ii обрабатывает один запрос за tit_i секунд (последовательно). В очереди mm запросов. Найти минимальное целое время TT, за которое все mm запросов будут обработаны, при условии, что операторы работают параллельно и каждый берёт следующий запрос сразу, как освободится.

Ограничения. 1n1051 \le n \le 10^5, 1m1091 \le m \le 10^9, 1ti1091 \le t_i \le 10^9.

Пример. t=[1,2,3]t = [1, 2, 3], m=5m = 5. За время TT оператор ii обработает T/ti\lfloor T / t_i \rfloor запросов:

  • T=2T = 2: 2+1+0=32 + 1 + 0 = 3 < 5 — нет.
  • T=3T = 3: 3+1+1=53 + 1 + 1 = 5 ≥ 5 — да.

Ответ — 33.

Выбор инварианта

Переформулировка: «при фиксированном TT, можно ли обработать mm запросов?». Это can(T) = (sum(T // t_i) >= m).

Монотонность: при T1<T2T_1 < T_2 — каждое слагаемое не уменьшается, сумма не уменьшается. Если can(T_1), то can(T_2). Форма (B) min-true.

Границы: lo=1\textit{lo} = 1, hi=min(ti)m\textit{hi} = \min(t_i) \cdot m (один самый быстрый оператор сам справится за это время).

Проверка

sum ← 0
для t_i из t:
    sum ← sum + T // t_i
    если sum ≥ m:
        вернуть истина
вернуть истина если sum ≥ m, иначе ложь

Простая, но требует внимания: TmT \cdot m может переполнять int при T,m1018T, m \sim 10^{18}. В этой задаче ограничения мягче, но привычка использовать long long — полезна.

Код

Что характерно для ступени 2

  • Проверка — сумма по элементам, но требует осторожности с переполнением. T/tiT / t_i может суммироваться в большое число, обязательно long long.
  • Граница hi\textit{hi} — продуктовая. Не «удвоить lo\textit{lo}, пока не достигнет», а сразу min(t) * m — упирается в самого быстрого оператора. Тонкое наблюдение, отличающее ступень 1 от ступени 2.
  • Сложность. O(nlog(min(t)m))O(n \log (\min(t) \cdot m)) — лог от 1018\sim 10^{18} это 60\sim 60, итого 6106\sim 6 \cdot 10^6 операций при n=105n = 10^5.

Ступень 3 (~CF 2000): максимизация медианы через k операций

Условие

Дан массив aa из nn положительных целых чисел, nn нечётное. Дано целое k0k \ge 0 — количество доступных операций. За одну операцию можно увеличить любой один элемент массива на 11. Найти максимальное возможное значение медианы массива (медиана — элемент на позиции n/2\lceil n/2 \rceil в отсортированном порядке) после применения k\le k операций.

Ограничения. 1n21051 \le n \le 2 \cdot 10^5, nn нечётное, 0k1090 \le k \le 10^9, 1ai1091 \le a_i \le 10^9.

Пример. a=[1,3,5]a = [1, 3, 5], k=2k = 2. Медиана — второй элемент в отсортированном порядке. Чтобы максимизировать, увеличиваем средний элемент: [1,5,5][1, 5, 5] \Rightarrow медиана =5= 5. (Можно перейти в [1,4,5][1, 4, 5] за 1 операцию, в [1,5,5][1, 5, 5] за 2 операции — медианы соответственно 4 и 5.) Ответ — 55.

Пример 2. a=[1,3,5,7,9]a = [1, 3, 5, 7, 9], k=6k = 6. Медиана — третий элемент в отсортированном порядке. Нужно поднять верхние три элемента: позиции в отсортированном — 5,7,95, 7, 9. Чтобы медиана была x\ge x, должно быть 3\ge 3 элементов x\ge x. При x=9x = 9: 595 \to 9 (4 операции), 797 \to 9 (2 операции), 999 \to 9 (0) — итого 66 операций, всё ровно. Ответ — 99.

Выбор инварианта

Переформулировка: «при фиксированном xx, можно ли за k\le k операций добиться, чтобы n/2\ge \lceil n/2 \rceil элементов было x\ge x?». Это can(x).

Монотонность: при x1<x2x_1 < x_2 — если для x2x_2 нужно столько-то операций, для x1x_1 нужно не больше (любое поднятие до x2x_2 покрывает поднятие до x1x_1). Если can(x_2), то can(x_1). Форма (A) max-true.

Границы: lo=mina\textit{lo} = \min a (любое уже достигнутое значение, не превышающее), hi=maxa+k\textit{hi} = \max a + k (даже потратив все kk операций на один элемент, не получим больше).

Проверка — содержательная часть

Сколько операций минимально нужно, чтобы n/2\ge \lceil n/2 \rceil элементов было x\ge x? Жадный ответ: отсортировать массив; верхнюю половину (включая медиану) — это позиции от nn/2n - \lceil n/2 \rceil до n1n - 1 — поднять до xx. Поднимать ниже-стоящие элементы бесполезно: они меньше, операций потратили бы больше.

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:
            вернуть ложь
вернуть истина

Почему именно верхняя половина — оптимально? Допустим, есть допустимый план, в котором мы подняли не верхнюю половину, а какие-то другие n/2\lceil n/2 \rceil элементов до x\ge x. Тогда среди них есть хотя бы один элемент с индексом <nn/2< n - \lceil n/2 \rceil — он в отсортированном меньше каждого элемента из верхней половины. Заменим его в плане на любой нижний из «верхней половины, не достигший xx»: стоимость замены не вырастет (новый элемент уже не меньше старого, до xx ему «ближе»). По цепочке таких замен — приходим к плану, поднимающему именно верхнюю половину. Доказано оптимальность жадного — exchange argument.

Сложность проверки: O(n/2)O(\lceil n/2 \rceil) после однократной сортировки.

Применение шаблона

Сортируем массив один раз, потом запускаем бинпоиск. Это важно: если сортировать на каждой проверке, O(nlognlogR)O(n \log n \cdot \log R) — потенциально не пройдёт 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

  • Проверка — не одна формула, а жадный план + обоснование. Это уже не «сумма по элементам», а «оптимальная стратегия использования kk операций».
  • Подготовка данных вне can. Сортировка один раз перед бинпоиском — важно для сложности. На ступени 1 сортировка тоже была, но тривиальная; здесь её роль — основа корректности жадного.
  • Доказательство оптимальности — exchange argument на 2–3 строки. Без него жадный — догадка.
  • Сложность. O(nlogn+nlog(amax+k))O(n \log n + n \log (a_{\max} + k)).

Сводная таблица

СтупеньCF-полосаЧто проверяем can(x)Сложность одной проверкиЧто добавилось по сравнению с предыдущей
1~1200Можно ли расставить kk объектов с попарным расстоянием d\ge dO(n)O(n)Базовый шаблон, жадная проверка-пробежка
2~1500T/tim\sum \lfloor T / t_i \rfloor \ge mO(n)O(n)Содержательный выбор границы hi\textit{hi}, аккуратность с переполнением
3~2000За k\le k операций сделать n/2\ge \lceil n/2 \rceil элементов x\ge xO(n)O(n) после сортировкиПодготовка данных вне can, обоснование жадного через exchange argument

Сама форма цикла — одинаковая на всех трёх ступенях. Меняется только can. И именно can — фокус внимания при росте сложности.


Типичные ловушки по ступеням

Ступень 1. Зацикливание из-за неправильного округления mid (см. тему про бинпоиск по ответу). На массиве позиций — забыть отсортировать перед жадной проверкой.

Ступень 2. Переполнение при умножении lom\textit{lo} \cdot m для границы hi\textit{hi}. Слишком тесный TL при n=105n = 10^5 — нужно sys.stdin.buffer.read в Python, иначе ввод съест половину времени.

Ступень 3. Сортировать массив внутри canO(nlogn)O(n \log n) на каждую проверку, итого O(nlognlogR)O(n \log n \cdot \log R)51075 \cdot 10^7, потенциальный TL в Python. Поднимать неправильную половину массива (нижнюю, центральную, всю) — типичный WA до того, как обосновал, какую именно половину поднимать. Забыть, что nn нечётное и медиана — это позиция n/2\lceil n/2 \rceil (0-индексация: n/2\lfloor n/2 \rfloor), а не середина в обычном смысле.


План тренировки

Если только знакомитесь с бинпоиском по ответу — порядок такой:

  1. Прочесть тему «Бинпоиск по ответу: когда применять и как выбирать инвариант» (серия «Алгоритмические основы»). Освоить базовый шаблон, обе формы цикла и принцип монотонности.
  2. Решить 2–3 задачи уровня ступени 1 — Aggressive Cows, распил досок (см. тему), минимизация TT с простой проверкой. Цель — научиться формулировать can за 5 минут от прочтения условия.
  3. Перейти к ступени 2 — задачи, где граница hi\textit{hi} требует продуктового рассуждения, или где проверка требует аккуратной арифметики (long long, ceil/floor).
  4. Ступень 3 — задачи, где can сама нетривиальна: содержит жадный, требует exchange argument, или вообще DP внутри. На этой полосе техника бинпоиск по ответу перестаёт быть отдельной — она становится каркасом, в который вкладывается отдельный алгоритм.

Признак, что ступень освоена: при виде нового условия в соответствующей полосе CF — за 5 минут формулируете can, доказываете монотонность, выбираете границы. Дальше — техника.


Итого

  • Каркас один: цикл бинпоиска по диапазону кандидатов с предикатом can(x).
  • Растёт сложность can. От «сумма по массиву» до «жадный план с обоснованием через exchange argument». От O(n)O(n) до O(n)O(n) после O(nlogn)O(n \log n)-подготовки.
  • Растёт сложность выбора границ. От «11 и ana1a_n - a_1» до «min(t)m\min(t) \cdot m как продуктовая оценка».
  • Содержательная часть — не цикл, а can. Освоить технику — значит освоить не четырёхстрочный цикл, а сотни возможных способов записать can для разных задач.

После этой стратегии полезно вернуться к теме «Бинпоиск по ответу: когда применять и как выбирать инвариант» и пройти её ещё раз — многие тонкости станут понятнее именно после трёх практик.

В серии: Стратегия победы

  1. 1Стратегия победы: DP — от очевидного шаблона к ёмкому состоянию
  2. 2Стратегия победы: графы — от BFS к многоходовым обходам
  3. 3Стратегия победы: бинпоиск по ответу — траектория 1200 → 1500 → 2000 — эта статья
  4. 4Стратегия победы: жадные — когда жадность ломается и как её спасти
  5. 5Стратегия победы: префиксные суммы — от 1D к 2D и подсчёту на подотрезках

Попробуй разобрать похожие задачи

В CodePal AI-партнёр подсказывает идею, а не ответ. Разбор в диалоге, код проверяется в браузере.