← Все вопросы

Как понять, что задача решается бинарным поиском по ответу?

Задан 10 месяцев назад474 просмотров2 ответа
11

Часто на CF в разборе пишут «бинпоиск по ответу», но я не понимаю, как заранее распознать такую задачу. Например: «разделить массив на k подотрезков так, чтобы максимальная сумма подотрезка была минимальна». Почему тут бинпоиск, и по чему именно искать?

Как вообще формализовать признак, что бинпоиск по ответу применим?

2 ответа

17
✓ Принятый ответ — помог автору

Ключевой признак — монотонность предиката. Заведи булеву функцию 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) проверка монотонна по кандидату. Если все три есть — это бинпоиск по ответу.

6

Добавлю про инвариант поиска, на котором все спотыкаются. Я использую полуинтервал [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.

Ваш ответ

Войдите, чтобы ответить на вопрос.