Все темы
32

Угадать ответ и бинарно искать саму догадку

Tier 1Бесплатно

Где применяется

1. Задача

У тебя очередь задач с длительностями [7, 2, 5, 10, 8] часов и 2 работника. Очередь нужно разрезать на 2 куска, не меняя порядок задач, и отдать по куску каждому работнику. Ты хочешь, чтобы рабочий день самого загруженного работника был как можно короче.

Отсортированного массива, в котором можно искать, здесь нет. Вопрос звучит так: какую самую короткую «самую длинную смену» можно получить? Перебор всех способов разрезать очередь быстро становится огромным, когда задач много.

Зато на другой вопрос ответить легко: «можно ли разрезать так, чтобы ни одна смена не была длиннее 18 часов?». Это проверка «да/нет», и она делается одним проходом по задачам. Пределы идут по порядку: если подходит 18, то подходят и 19, и 20, и любое большее число. Этого порядка хватает, чтобы искать сам ответ бинарным поиском.

2. Простыми словами

Попробуй на задачах [7, 2, 5, 10, 8] предел 15 часов на одного работника. Первый берёт 7, 2 и 5, это 14 часов. Задача на 10 часов к нему уже не влезет, её берёт второй. Задача на 8 часов не влезет и ко второму: 10 + 8 = 18. Нужен третий работник, а их всего два. Значит, 15 не подходит.

Теперь предел 21. Первый берёт 7, 2 и 5 (14 часов), второй берёт 10 и 8 (18 часов). Двух работников хватило, 21 подходит.

Значит, ответ больше 15 и не больше 21. Дальше ты делишь этот промежуток пополам, как в обычном бинарном поиске. Предел не подошёл, бери больше. Подошёл, пробуй меньше. Через несколько проверок останется самый маленький предел, который ещё подходит. Здесь это 18.

3. Идея

Напиши проверку «влезает ли». Выбери cap, наибольшую сумму, которую разрешено набрать одному куску. Спроси, можно ли разрезать очередь на k кусков так, чтобы каждый уложился в этот предел. Проверка жадная. Идёшь по задачам и считаешь сумму текущего куска. Если следующая задача выведет сумму за cap, начинаешь новый кусок. В конце считаешь куски. Если их k или меньше, этот cap подходит. Назови эту проверку feasible(cap).

Проверка монотонная: при большем cap кусков никогда не нужно больше. Поэтому, если какой-то cap подошёл, подходит и любой больший. Получается та же картина false, false, ..., true, true, что в поиске границы. Разница в том, что теперь ты ищешь среди возможных значений ответа, а не среди позиций массива.

Задай границы поиска. Ответ не может быть меньше самой длинной задачи: одна задача целиком должна влезть в свой кусок. Поэтому lo = max(nums). И ответ не может быть больше суммы всех задач, когда всё лежит в одном куске. Поэтому hi = sum(nums). Дальше запусти поиск границы из урока 30 на промежутке от lo до hi. Проверь средний cap. Если он подходит, задай hi = mid. Если нет, задай lo = mid + 1. В конце верни lo.

значения cap → 15 17 18 21 32 нужно 3 части (нет) хватает 2 частей (да) наименьший допустимый cap = 18

Проследим на [7, 2, 5, 10, 8], k = 2. Границы: lo = 10 (самая длинная задача), hi = 32 (сумма всех).

  • Середина 21. Куски 7 + 2 + 5 = 14 и 10 + 8 = 18, всего 2. Подходит, поэтому hi = 21.
  • Середина (10 + 21) >> 1 = 15. Куски 14, 10 и 8, всего 3. Не подходит, поэтому lo = 16.
  • Середина (16 + 21) >> 1 = 18. Куски 14 и 18, всего 2. Подходит, поэтому hi = 18.
  • Середина (16 + 18) >> 1 = 17. Куски 14, 10 и 8, всего 3. Не подходит, поэтому lo = 18.
  • lo и hi сошлись на 18. Ответ 18.

4. Во что это обходится

Время: O(n · log(range)). Бинарный поиск делает около log(range) шагов, и на каждом шаге один проход проверки за O(n). Здесь log берётся от размаха возможных ответов (sum - max), а не от длины массива. Для примера урока размах 32 - 10 = 22, и шагов понадобилось 4. Память: O(1) сверх входа.

Найти разбиение можно и динамическим программированием (решение строится из ответов на меньшие подзадачи). Но написать его без ошибок обычно труднее. Монотонная проверка «да/нет» и поиск по ответу проще, и в них сложнее ошибиться.

5. Где это встречается

Балансировка нагрузки и выбор размера шардов (частей, на которые делят данные между серверами): работу режут так, чтобы самый загруженный сервер не вышел за предел. Планирование вида «отправить все посылки за D дней». Настройка ограничений скорости. Задача вида «Коко ест бананы» с собеседований: найти наименьшую скорость, при которой работа успевает к сроку. Выбор самой маленькой машины, которая ещё выполняет SLA (обещанный клиенту уровень сервиса). У git bisect та же форма: ответ это первый плохой коммит, а isBad(commit) даёт монотонную проверку на промежутке коммитов вместо значений нагрузки.

Наименьшая вместимость корабля, которая успевает в срок
smallestFeasible(4, 16, (cap) => canShip([3, 2, 2, 4, 1, 4], 3, cap))
кандидаты на вместимость
4
5
6
7
8
9
10
11
12
13
14
15
16

lo

hi
canDo(cap): груз на каждый день
пусто
lo = 4hi = 16days = 3

Нужно отправить грузы [3, 2, 2, 4, 1, 4] по порядку за 3 дня. Вместимость не меньше 4 (самый тяжёлый груз) и не больше 16 (всё сразу).

Гонка скорости
Два способа найти самый маленький корабль, который перевезёт 24 посылок за 3 дня

Столбики: веса посылок. Каждая проверка вместимости читает их все по порядку.

Одна операция: одно чтение веса посылки. За каждый кадр все дорожки делают одинаковое число операций, поэтому медленная отстаёт.

Перебор всех вместимостей O(n·S)
операций: 0
Бинарный поиск по ответу O(n log S)
операций: 0

 

А если чисел больше

Операции для n чисел и примерное время при 100 миллионах простых операций в секунду. Это оценка по тому же счёту, а не замер.

n = 1 000
Перебор всех вместимостей2,2 млн · 22 мс
Бинарный поиск по ответу12,7 тыс. · меньше 1 мс
n = 100 000
Перебор всех вместимостей21,7 млрд · 3,6 мин
Бинарный поиск по ответу1,9 млн · 19 мс
n = 10 000 000
Перебор всех вместимостей216,7 трлн · 25 дн.
Бинарный поиск по ответу259,5 млн · 2,6 с

Как узнать

Бинарный поиск по ответу (Binary search on the answer)

Эти слова в условии значат этот паттерн

  • минимизировать максимум / максимизировать минимум
  • наименьшее значение, удовлетворяющее условию
  • найти наименьшую вместимость/скорость/число дней, которое подходит
  • ответ лежит в числовом диапазоне, а не в позиции массива
  • проверка выполнимости монотонна (становится true после некоторой точки)

Похоже, но не то

Диапазон в 1e9 требует около 30 проверок. Итог O(n log range), если одна проверка стоит O(n).

На LeetCode

Задачи, где нужна наименьшая скорость, вместимость или граница, при которой всё получается, или наибольшая из допустимых. Напиши проверку «да или нет» для одной догадки и ищи бинарным поиском по диапазону ответов.

Твоя задача (Tier 1)

Напиши функцию minLargestSplit(nums, k).

Разрежь список на k частей. Каждая часть состоит из соседних чисел в исходном порядке, пустых частей нет. У каждой части есть сумма. Самая большая из этих сумм должна быть как можно меньше. Верни эту наименьшую возможную наибольшую сумму.

  • Вход: nums, список чисел, каждое 0 или больше, и k, на сколько частей резать. Пример: [7, 2, 5, 10, 8] и k = 2.
  • Возвращает: наименьшее значение, которое может принять самая большая сумма части.

Шаги:

  1. Напиши вспомогательную функцию feasible(cap). Иди по списку и добавляй числа в текущую часть. Если следующее число выведет сумму за cap, начни новую часть. Предел подходит, если частей вышло k или меньше.
  2. Ответ лежит между самым большим отдельным числом (часть не может быть меньше него) и суммой всех чисел (одна часть на всё).
  3. Ищи по этому промежутку бинарным поиском. Пробуй средний cap. Если feasible(cap) даёт true, пробуй меньше. Если нет, пробуй больше. Верни наименьший подходящий cap.

Готово, когда проходят все тесты:

  • [7, 2, 5, 10, 8], k = 2 -> 18
  • [1, 2, 3, 4, 5], k = 2 -> 9
  • [1, 4, 4], k = 3 -> 4 (по одному числу на часть, ответ равен самому большому)
  • [1, 2, 3], k = 1 -> 6 (одна часть, вся сумма)
  • [1, 2, 3], k = 3 -> 3 (по одному числу на часть)
  • [5], k = 1 -> 5 (одно число)
  • [2, 2, 2], k = 3 -> 2 (равные части)

Практика

minLargestSplit

minLargestSplit(nums, k) -> number

export function minLargestSplit(nums, k) {
  // Your code here. The task is described in the box above the editor.
}

Ты здесь как гость

Тесты запускаются прямо здесь, без аккаунта. Код сохраняется только в этом браузере. Бесплатный аккаунт сохранит его на любом устройстве, отметит шаг пройденным и покажет, где ты в курсе.

Лестница подсказок

Застрял? Открывай по одной, но сначала попробуй сам.

    Мы используем необходимые куки для работы сайта (вход и язык). Если ты согласишься, мы также загрузим Google Analytics, чтобы видеть, какие страницы читают, и Google reCAPTCHA для защиты форм обратной связи и сообщений об ошибке от спама. Политика конфиденциальности