Поиск по ответу
Как свести незнакомую задачу к массиву из нулей и единиц — и три приметы, по которым это узнаётся в условии.
4 мин
Бинарный поиск по массиву — частный случай. Общий выглядит так.
Есть функция check(x), которая для маленьких возвращает false, а начиная с какого-то момента — всегда true. Иначе говоря, воображаемый массив её значений выглядит как
0 0 0 0 0 1 1 1 1 1
Найти границу между нулями и единицами — это ровно тот же бинарный поиск, только вместо a[mid] вызывается check(mid).
long long l = -1, r = LIMIT; // check(l) ложно, check(r) истинно
while (r - l > 1) {
long long mid = l + (r - l) / 2;
if (check(mid)) r = mid;
else l = mid;
}
// r — наименьшее значение, при котором check истинно
Массива здесь нет вовсе. Есть только функция и обещание, что она монотонна.
Три приметы в условии
Задача решается поиском по ответу, когда сходятся три вещи:
- Спрашивается «наименьшее , при котором получится» или «наибольшее , при котором ещё хватит».
- Проверить конкретное легко, а найти его прямо — непонятно как.
- Ответ монотонен: если при получилось, то при всех больших (или всех меньших) тоже.
Третий пункт нужно проговаривать вслух, а не считать очевидным. Именно на нём ломаются задачи, где монотонности нет: скажем, «раздать поровну» монотонностью не обладает, и поиск по ответу там даст неверный результат, а не медленный.
Как выглядит check
Проверка почти всегда — жадный проход или формула.
Развезти ящики за дней машиной грузоподъёмности . Грузим подряд, пока влезает; не влезло — новый день. Считаем дни, сравниваем с .
bool check(long long capacity) {
int days = 1;
long long current = 0;
for (long long weight : boxes) {
if (weight > capacity) return false; // не увезём вообще
if (current + weight <= capacity) current += weight;
else { days++; current = weight; }
}
return days <= d;
}
Нарезать кусков длины из имеющихся верёвок. Сумма сравнивается с .
Успеют ли станки сделать деталей за минут. Сумма сравнивается с .
Общее у всех трёх: проверка линейна, а перебор ответа стоил бы линии на каждое значение.
Границы поиска
Нижнюю границу берут не «ноль на всякий случай», а такую, при которой проверка осмысленна. В задаче про грузоподъёмность машина легче самого тяжёлого ящика не увезёт ничего, и жадный проход на таком значении либо соврёт, либо зациклится. Значит, нижняя фиктивная граница — «самый тяжёлый ящик минус один».
Верхнюю берут заведомо достаточной, но не «на всякий случай огромной»: чем шире промежуток, тем больше шагов, а при вычислениях внутри check большие числа ещё и переполняются.
Когда монотонность спрятана
Интереснее всего задачи, где монотонность есть, но не та, про которую спрашивают.
Минимум максимума. Даны неубывающий массив и невозрастающий ; нужно , при котором минимален. Сам максимум по не монотонен — он сначала падает, потом растёт. Зато монотонна разность : она не убывает. Бинарным поиском находим первое , где , и проверяем два соседних варианта.
Общий приём: если функция «падает, потом растёт», ищите монотонную величину, которая меняет знак в точке минимума.
Отношение. Максимизировать по подмножеству из элементов. Набор с отношением не меньше существует тогда и только тогда, когда максимум суммы по элементам неотрицателен — а это просто «взять наибольших». Предикат монотонен по , и поиск работает.
Чётность. В отсортированном массиве все числа встречаются дважды, кроме одного. Само значение ничего не подсказывает, а вот позиции — да: до одинокого элемента пары стоят на местах , а после него сдвигаются. Предикат «пара с номером ещё не сдвинута» монотонен.