Бинпоиск по ответу — одна из тех техник, после освоения которой десятки «средних» задач Div.2 C–D внезапно становятся понятными. Идея простая: вместо того чтобы искать сам ответ конструктивно, мы перебираем его, но не линейно, а половинным делением — и на каждом шаге только проверяем, достижим ли этот кандидат.
Техника работает, когда выполнены два условия:
- Монотонность: если кандидат достижим, то все тоже достижимы (или наоборот — если все ).
- Быстрая проверка: для фиксированного функцию «достижим или нет?» можно посчитать за или .
Эта задача — эталонный пример для освоения техники. Монотонность прямолинейная, проверка жадная и за линейное время, а в условии явно присутствует случай «ответа не существует» — что позволяет заодно разобрать типичную ловушку «забыл про ».
Что дано
Есть кружек кофе. Кружка содержит единиц кофеина. Участник садится писать работу и планирует выпивать кружки днями. В один день можно выпить сколько угодно кружек (в том числе ноль).
Механика одного дня. В дне участник выбирает некоторое подмножество ещё не выпитых кружек и пьёт их в произвольном порядке. Если в этот день он выпил кружек с кофеином именно в таком порядке, то страниц в этот день он напишет:
То есть первая кружка дня даёт полный кофеин, вторая теряет 1, третья — 2, и так далее. Если у кружки кофеин меньше «штрафа», страниц она даёт 0 (не может дать отрицательно).
За все дни суммарно нужно написать не менее страниц. Требуется найти минимальное количество дней , за которое это возможно. Если невозможно даже при (каждая кружка в свой день) — вывести .
Ограничения
- — количество кружек.
- — требуемое количество страниц.
- — кофеин каждой кружки.
Формат ввода
n m
a_1 a_2 ... a_n
Формат вывода
Одно целое число — минимальное или , если решения нет.
Примеры
Пример 1.
Вход:
5 8
2 3 1 1 2
Выход:
4
Пример 2.
Вход:
7 10
1 3 4 2 1 4 2
Выход:
2
Пример 3.
Вход:
5 15
5 5 5 5 5
Выход:
1
Пример 4.
Вход:
5 16
5 5 5 5 5
Выход:
2
Пример 5.
Вход:
5 26
5 5 5 5 5
Выход:
-1
Разберём первый пример руками
, , .
Отсортируем кружки по убыванию кофеина: .
Попробуем (все за один день). Участник выпивает в порядке . Страниц:
— не хватает.
Попробуем . Нам нужно распределить 5 кружек на 2 дня. Оптимальная стратегия: в каждом дне первая по порядку кружка — самая «жирная» из оставшихся, вторая — следующая и так далее. Распределение:
| День 1 | День 2 |
|---|---|
| (штраф 0) | (штраф 0) |
| (штраф 1) | (штраф 1) |
| (штраф 2) | — |
Страниц: в 1-й день ; во 2-й . Итого .
Попробуем .
| День 1 | День 2 | День 3 |
|---|---|---|
| (штраф 1) | (штраф 1) | — |
Страниц: .
Попробуем .
| День 1 | День 2 | День 3 | День 4 |
|---|---|---|---|
| (штраф 1) | — | — | — |
Страниц: ✓.
Ответ — дня.
Интуиция, которую мы уже увидели. При раскладке по дням выгоднее сначала распределить по "первой позиции" в каждом дне кружки с наибольшим кофеином, потом — по "второй", и так далее. Это максимизирует страницы, потому что большие получают маленькие штрафы. И вторая вещь: функция «страниц за дней» монотонно не убывает по — добавили ещё один день, стало не хуже.
Осталось эти два наблюдения формализовать и использовать в бинпоиске.
Идея решения: бинпоиск по количеству дней
Алгоритм в одну фразу.
Выполняем бинпоиск по ответу : ищем минимальное , для которого проверка . Проверка за реализует жадную раскладку, описанную выше, и сравнивает сумму страниц с .
Почему работает монотонность
Утверждение: если за дней можно написать страниц, то за дней тоже можно.
Доказательство (конструктивное). Возьмём оптимальную раскладку на дней, дающую страниц. Добавим -й пустой день и не выпьем в нём ни одной кружки. Количество страниц не изменилось (новый день даёт 0), значит и при днях тоже достижимо. ∎
Следовательно, множество допустимых — это непрерывный суффикс вида . На таком множестве бинпоиск работает напрямую.
Границы бинпоиска: , . Если — ответ . Иначе бинпоиск находит минимальное с .
Почему работает жадная проверка
Для фиксированного утверждение: оптимальная раскладка — та, где кружки отсортированы по убыванию кофеина, и кружка номер (0-индексация после сортировки) идёт в день на позицию в этом дне.
Другими словами: сначала раскладываем кружек с самым большим кофеином — по одной в каждый день на «первую позицию» (штраф 0). Потом следующие кружек — на «вторую позицию» в каждом дне (штраф 1). И так далее.
Почему это оптимально (exchange argument). Предположим, существует раскладка лучше нашей. Значит, в ней какая-то кружка с бо́льшим стоит на более поздней позиции внутри дня (с бо́льшим штрафом), чем какая-то кружка с меньшим на более ранней позиции.
Поменяем их местами. Штрафы у позиций не меняются — меняются только кружки, стоящие на них. Было:
- кружка на позиции со штрафом : вклад .
- кружка на позиции со штрафом , причём : вклад .
Станет:
- кружка на позиции со штрафом : вклад .
- кружка на позиции со штрафом : вклад .
Разность «после − до»:
Это выражение для и (легко проверить перебором знаков слагаемых — свойство «монотонной парной рокировки»). То есть swap не ухудшает результат. Применив все такие свопы, приходим к нашей жадной раскладке. Значит, она не хуже любой другой. ∎
Как выражается штраф позиции
Кружка номер (0-индексация, в отсортированном по убыванию массиве) идёт в день , это её позиция -я в дне (тоже 0-индексация). Штраф равен самой позиции. То есть вклад кружки в сумму страниц:
И проверка за один проход по массиву:
Решение: псевдокод
прочитать n, m, массив a длины n
отсортировать a по убыванию
функция check(d):
total ← 0
для i от 0 до n-1:
penalty ← i // d
total ← total + max(0, a[i] - penalty)
вернуть total ≥ m
если не check(n):
вывести -1 и выйти
# иначе — бинпоиск по d в [1, n], ищем минимальное с check(d) = true
lo ← 1
hi ← n
пока lo < hi:
mid ← (lo + hi) // 2
если check(mid):
hi ← mid
иначе:
lo ← mid + 1
вывести lo
Сложность одной проверки — . Бинпоисков — . Итог: с учётом сортировки.
Код решения
Комментарии по реализации.
- Тип для сумм. достигает , что превышает
int32. В C++ обязателенlong longдляm,a[i]иtotal. В Pythonintавтоматически большой — переполнения нет. - Целочисленное деление. В Python —
//, не/: оператор/даётfloat. В бинпоиске(lo + hi) // 2обязательно. В C++ используемmid = lo + (hi - lo) / 2— каноничная безопасная форма без риска переполнения; для можно и(lo + hi) / 2, но привычка окупается на больших границах. - Сортировка по убыванию. Python —
a.sort(reverse=True). C++ —sort(a.begin(), a.end(), greater<long long>())напрямую, безrbegin/rend. - Быстрый ввод. Python —
sys.stdin.buffer.read().split()быстрееinput()в цикле на больших . - Ранний выход из
check(break, еслиvalue ≤ penalty) допустим, когда массив строго отсортирован по убыванию: дальше штрафы только растут, а значения только падают. Оставлено без раннего выхода для прозрачности — оба варианта в комментариях кода.
Проверим на всех примерах из условия
Ниже — отсортированные массивы и прогон check(d) для ключевых около ответа. Таблица для каждого примера показывает штраф и вклад кружки .
Пример 1: → отсортированно
: штрафы . Вклады . Сумма . : штрафы . Вклады . Сумма ✓.
Бинпоиск: . , false → . , true → . . Ответ: 4 ✓.
Пример 2: → отсортированно
: штрафы . Вклады . Сумма . : штрафы . Вклады . Сумма ✓.
Бинпоиск: . , надо проверить: штрафы , вклады , сумма , true → . , true → . , false → . Ответ: 2 ✓.
Пример 3:
: штрафы . Вклады . Сумма ✓.
Бинпоиск стартует с почти сразу: , true → . , true (сумма ). , true. Ответ: 1 ✓.
Пример 4:
: сумма . : штрафы . Вклады . Сумма ✓.
Бинпоиск даёт минимальное . Ответ: 2 ✓.
Пример 5:
: штрафы (все кружки — первые в своих днях). Вклады . Проверка не проходит → Ответ: ✓.
Все 5 примеров дают эталонные ответы. Код корректен.
Крайние случаи
1.
Бинпоиск в диапазоне . check(1): штраф , вклад ? Если да — ответ , иначе — . Код обрабатывает корректно.
2. Все одинаковые
Массив уже «отсортирован». Суммарный максимум страниц при равен . Если — . Иначе бинпоиск находит минимальное .
3.
Всегда достижимо за , если (а по ограничениям , значит всегда). Ответ без экзотики.
4. Максимальные значения
, , . Промежуточная сумма в check — до . В C++ — long long (иначе переполнение int). В Python — автоматически.
5. Единственный , где проверка падает
Теоретически бинпоиск может в какой-то момент «попасть» в , где check(d) ложная, но соседние истинные. Не случится — из-за монотонности множество true — это суффикс. Если check(d) false, то и все меньшие false.
Типичные ошибки
- Забыть про . Бинпоиск по всегда вернёт какое-то . Без предварительной проверки
check(n)код выведет неверный ответ на тестах, где задача неразрешима (пример 5). intвместоlong longв C++. Сумма страниц до —intпереполняется. После переполненияtotalстановится отрицательным, иcheckвозвращаетfalseтам, где должно бытьtrue.WAна больших тестах.- Сортировка по возрастанию вместо убывания. Жадная раскладка требует, чтобы большие шли на малые штрафы. С обратной сортировкой всё работает «в зеркале» и даёт неправильный ответ.
- Неверный размер штрафа. Штраф — (целочисленное деление), а не (вещественное) и не .
/в Python 3 — это float-деление; писать//. В C++ дляint/уже целочисленное, но важно не путать с остатком%. - Границы бинпоиска
[0, n]вместо[1, n]. Приcheckделит на ноль. Нижняя граница должна быть . - Ранний выход в
checkпри нестрогом убывании. Если дваa_iравны, строгое неравенствоa[i] <= penaltyв условииbreakможет сработать раньше времени. Если всё же использовать ранний выход — условиеa[i] < penalty(строгое) илиa[i] - penalty <= 0. - Интерпретация «раскладки по дням» как
день = i / d, позиция = i % d. Это тоже валидная раскладка, но для неё штраф позиции —i % d, а вклад —max(0, a[i] - (i % d)). Алгоритм остаётся корректным, но с другой формулой. Важно не перепутать/и%: раскладка, в которой день =i % dи позиция =i / d, даёт тот же ответ по сумме, но через другое «расписание».
Анализ сложности
- Сортировка: .
- Бинпоиск: итераций.
- Одна проверка: проход по массиву.
- Итог: по времени, по памяти (массив ).
При — это операций, укладывается в типичный лимит 1–2 секунды с большим запасом.
Что ещё полезно потренировать
Задачи на ту же идею (бинпоиск по ответу с жадной или симуляционной проверкой). Все — с возраст-нейтральных платформ.
- Codeforces 1118D2 Div.3 (та же самая задача в «hard version» контеста, формулировка чуть развёрнутее) — если хочется закрепить именно этот паттерн.
- Codeforces Div.2 C–D уровня 1400–1600 с тегом
binary searchиgreedy: типичные формулировки «найти минимальное , такое что...» с быстрой жадной проверкой за или . - acmp.ru, раздел «Бинарный поиск»: классические задачи уровня –, много чистых формулировок монотонной проверки (например, «разместить коров максимально далеко друг от друга по стойлам»).
- Яндекс Контест: архивы «Алгоритмы и структуры данных» содержат классические задачи на бинпоиск по ответу с различными типами проверок.
Для следующего шага — попробовать задачу, где проверка уже не чисто жадная, а требует небольшой симуляции или DP. Типичный пример — «минимальная длина подмассива с заданным свойством» через бинпоиск по длине + двухуказательная проверка.
Итого
- Идея: бинпоиск по минимальному количеству дней с проверкой за .
- Монотонность: если достижимо за , то и за (добавили пустой день).
- Жадная проверка: сортировка по убыванию, кружка номер идёт в день , позиция . Вклад — .
- Крайний случай : проверить
check(n)до бинпоиска. Без этой проверки алгоритм всегда выдаст валидное , даже если задача неразрешима. - Переполнение:
long longв C++ дляtotal(до ). - Сложность: по времени, по памяти.