Как понять, что задача решается бинарным поиском по ответу?
Часто на CF в разборе пишут «бинпоиск по ответу», но я не понимаю, как заранее распознать такую задачу. Например: «разделить массив на k подотрезков так, чтобы максимальная сумма подотрезка была минимальна». Почему тут бинпоиск, и по чему именно искать?
Как вообще формализовать признак, что бинпоиск по ответу применим?
2 ответа
Ключевой признак — монотонность предиката. Заведи булеву функцию check(x) = «можно ли уложиться в ограничение x». Бинпоиск по ответу работает тогда и только тогда, когда check монотонна: существует порог t такой, что check(x) ложно для всех x < t и истинно для всех x >= t (или наоборот). Тогда ты ищешь этот порог — минимальный x, при котором check(x) == true.
В твоей задаче «минимизировать максимальную сумму подотрезка при разбиении на k частей»:
check(x)= «можно ли разбить массив на не более k подотрезков, у каждого сумма <= x».- Это считается жадно за O(n): идём слева, копим текущую сумму, как только она превысит x — режем (новый кусок). Считаем число кусков.
- Монотонность очевидна: если при лимите x хватило k кусков, то при большем лимите тем более хватит. Предикат идёт
false...false true...true.
Ищем минимальный x. Границы: lo = max(a[i]) (один элемент уже должен влезть), hi = sum(a).
bool check(const vector<long long>& a, long long x, int k) {
int parts = 1; long long cur = 0;
for (long long v : a) {
if (cur + v > x) { parts++; cur = v; }
else cur += v;
}
return parts <= k;
}
long long solve(const vector<long long>& a, int k) {
long long lo = 0, hi = 0;
for (long long v : a) { lo = max(lo, v); hi += v; }
while (lo < hi) {
long long mid = lo + (hi - lo) / 2;
if (check(a, mid, k)) hi = mid;
else lo = mid + 1;
}
return lo;
}
Общая сложность O(n log(sum)). Память O(1). Чеклист распознавания: (1) спрашивают min/max некоторой величины; (2) если зафиксировать кандидата-ответ, проверка «годится ли он» сильно проще исходной задачи; (3) проверка монотонна по кандидату. Если все три есть — это бинпоиск по ответу.
Добавлю про инвариант поиска, на котором все спотыкаются. Я использую полуинтервал [lo, hi) и инвариант: check(hi) всегда истинно (или hi за границей искомого), check(lo-1) ложно. Тогда шаблон if (check(mid)) hi = mid; else lo = mid + 1; всегда сходится к минимальному истинному, и нет бесконечного цикла: при lo + 1 == hi mid = lo, и любая ветка либо сдвигает lo, либо ставит hi = lo.
Грабля: если ищешь максимальный x, при котором предикат истинен (предикат true...true false...false), нужен другой mid: mid = lo + (hi - lo + 1) / 2; if (check(mid)) lo = mid; else hi = mid - 1; — округление вверх обязательно, иначе зависнешь на lo + 1 == hi.