EduBrick

Семь задач на поиск по ответу

Разбор постановок, которые встречаются чаще всего: коровы в стойла, провода, встреча в точке, принтеры, шарики, выборы. Для каждой — предикат и границы.

7 мин

Схема поиска по ответу описана отдельно. Здесь — семь конкретных задач. Смысл не в самих условиях, а в том, чтобы натренировать шаг «придумать предикат»: именно на нём задачи и проигрываются.

Для каждой указано три вещи: что бинарим, как проверяем, где границы.

1. Коровы в стойла

На прямой стоят nn стойл с координатами xix_i. Нужно расставить kk коров так, чтобы минимальное расстояние между соседними было максимальным.

Формулировка «максимизировать минимум» — самая надёжная примета поиска по ответу.

Бинарим по расстоянию dd. Предикат: «можно расставить kk коров, чтобы все попарные расстояния были не меньше dd». Проверка жадная — ставим первую корову в самое левое стойло и дальше каждую следующую в первое подходящее:

bool fits(const vector<long long>& x, int k, long long d) {
    int count = 1;
    long long last = x[0];
    for (size_t i = 1; i < x.size(); i++)
        if (x[i] - last >= d) { count++; last = x[i]; }
    return count >= k;
}

Жадность корректна: если корову можно поставить сейчас, откладывать её невыгодно — правее места только меньше.

Границы: l=0l = 0 (всегда можно), r=xn1x0+1r = x_{n-1} - x_0 + 1 (заведомо нельзя, если коров хотя бы две). Ответ — в l.

2. Провода

Есть nn кусков провода длиной aia_i. Нужно нарезать хотя бы kk кусков одинаковой длины, как можно более длинных.

Бинарим по длине куска dd. Проверка в одну строку: из провода длины aia_i получается ai/d\lfloor a_i / d \rfloor кусков.

long long total = 0;
for (long long length : a) total += length / d;
return total >= k;

Границы: l=0l = 0, r=maxai+1r = \max a_i + 1. Ответ в l. Отдельно обработайте случай, когда ответ нулевой, — обычно требуется вывести 00.

Ловушка: если длины вещественные (метры с сантиметрами), домножьте на 100100 и работайте в целых. Вещественный поиск здесь заведомо хуже.

3. Встреча в одной точке

nn человек стоят в точках xix_i и двигаются со скоростями viv_i. За какое минимальное время все смогут собраться в одной точке?

Бинарим по времени tt. За время tt человек ii достигает любой точки отрезка [xivit,  xi+vit][x_i - v_i t,\; x_i + v_i t]. Все встретятся тогда и только тогда, когда у этих отрезков есть общая точка.

Пересечение отрезков — максимум левых концов против минимума правых:

double lo = -1e18, hi = 1e18;
for (int i = 0; i < n; i++) {
    lo = max(lo, x[i] - v[i] * t);
    hi = min(hi, x[i] + v[i] * t);
}
return lo <= hi;

Монотонность очевидна: с ростом tt отрезки только расширяются, и однажды пересёкшись, разойтись уже не могут.

Здесь ответ вещественный — значит, вещественный поиск с фиксированным числом итераций.

4. Два принтера

Первый принтер печатает лист за xx минут, второй за yy. Нужно напечатать nn листов. За какое минимальное время?

Задача решается формулой, но выводить её лень, а ошибиться легко. Бинарный поиск избавляет от этого целиком.

Бинарим по времени tt. За tt минут первый напечатает t/x\lfloor t/x \rfloor, второй t/y\lfloor t/y \rfloor:

return t / x + t / y >= n;

Границы: l=0l = 0, r=nmin(x,y)r = n \cdot \min(x, y) — этого хватит гарантированно. Ответ в r, потому что минимизируем.

Это общий приём: вместо вывода формулы напишите проверку. Проверка почти всегда очевидна, а формула — нет. Логарифм сверху обычно можно себе позволить.

5. Воздушные шарики

nn помощников надувают шарики. Помощник ii работает tit_i минут, надувая ziz_i шариков, потом отдыхает yiy_i минут, и так по кругу. Нужно mm шариков. За какое минимальное время?

Бинарим по времени TT. Для каждого помощника считаем, сколько он успеет: полных циклов T/(ti+yi)\lfloor T / (t_i + y_i) \rfloor, плюс остаток, который надо аккуратно поделить между работой и отдыхом:

long long cycle = t[i] + y[i];
long long full = T / cycle;
long long rest = min(T % cycle, t[i]);   // в остатке он работает не дольше t[i]
total += full * z[i] + rest * z[i] / t[i];

Два подвоха. Первый — min в остатке: если остаток попал на отдых, лишнего не начислится. Второй — задача обычно просит вывести точное распределение, кто сколько надул, а за время TT помощники успевают больше нужного. Лишнее приходится отрезать отдельным жадным проходом. На этом и падают решения: время найдено верно, а восстановление ответа неверное.

6. Выборы

За кандидата ii голосует aia_i человек, и подкуп одного избирателя стоит одну монету. Для каждого кандидата надо сказать, сколько монет нужно, чтобы он победил (у него строго больше голосов, чем у любого другого).

Здесь поиск по ответу вложен во внешний цикл, и наивно получается медленно.

Фиксируем кандидата ii и бинарим по XX — числу голосов, с которым он победит; заведомо X>aiX > a_i. Проверка: у всех остальных должно стать меньше XX, значит у кандидата jj с ajXa_j \ge X нужно отобрать ajX+1a_j - X + 1 голосов. Сумма таких изъятий — обязательные траты SS. Если SXaiS \le X - a_i, бюджета хватает, и недостающие голоса докупаются у кого угодно.

Прямая проверка — O(n)O(n), итого O(n2logn)O(n^2 \log n) на всех кандидатов. Слишком медленно при n=105n = 10^5.

Ускорение: отсортируем aa и посчитаем суффиксные суммы. Тогда те, у кого ajXa_j \ge X, — это суффикс отсортированного массива, его начало ищется lower_bound за логарифм, а сумма изъятий берётся готовой:

S=suffixSum(g)(X1)(ng)S = \text{suffixSum}(g) - (X - 1) \cdot (n - g)

где gg — первый индекс с agXa_g \ge X. Проверка стала O(logn)O(\log n), всё решение — O(nlog2n)O(n \log^2 n).

Самого кандидата ii из суффикса исключать не нужно: раз X>aiX > a_i, он в него не попадает. Это тот случай, когда проверка формулы перебором на маленьких данных экономит час отладки.

Это типичный второй этап таких задач: сначала придумать проверку, потом ускорить её предподсчётом. Придумывать сразу быструю проверку почти никогда не нужно.

7. Дипломы

Нужно напечатать nn дипломов прямоугольниками w×hw \times h на квадратном листе стороны ss. Найдите минимальное ss.

Бинарим по стороне. На лист стороны ss помещается s/ws/h\lfloor s/w \rfloor \cdot \lfloor s/h \rfloor дипломов:

return (s / w) * (s / h) >= n;

Единственная сложность — переполнение. При ss до 101810^{18} произведение двух частных не помещается никуда. Спасает ранний выход: если первый множитель уже не меньше nn, ответ положителен без умножения.

long long first = s / w, second = s / h;
if (first == 0 || second == 0) return false;
if (first >= n || second >= n) return true;
return first * second >= n;   // оба меньше n — произведение не переполнится

Что общего

Во всех семи задачах порядок действий один:

  1. Назвать величину, которую бинарим, — это всегда ответ или его часть, а не индекс.
  2. Записать предикат словами и убедиться, что он монотонен.
  3. Найти границы, где предикат заведомо ложен и заведомо истинен, — с запасом.
  4. Написать проверку максимально прямолинейно.
  5. Только если не проходит по времени — ускорять проверку.

Шаг 2 пропускают чаще всего, и именно он ломает решение на закрытых тестах.