Подсчёты и запросы бинарным поиском
Сколько раз элемент встречается, какой ближайший, есть ли он на отрезке, где медиана объединения двух массивов. Всё — парами границ.
6 мин
Бинарный поиск чаще используют не для «найти элемент», а для «посчитать, сколько». Приём один и тот же: находим две границы и вычитаем.
Сколько раз встречается
int count = upper_bound(a.begin(), a.end(), x) - lower_bound(a.begin(), a.end(), x);
lower_bound даёт первое вхождение, upper_bound — позицию сразу за последним. Разность — количество. Если элемента нет, оба итератора совпадут и разность будет нулём — отдельная проверка не нужна.
В Python то же самое:
from bisect import bisect_left, bisect_right
count = bisect_right(a, x) - bisect_left(a, x)
Сколько элементов в диапазоне
Тот же приём для полуинтервала :
int count = upper_bound(a.begin(), a.end(), r) - lower_bound(a.begin(), a.end(), l);
Это основа целого класса решений: отсортировали массив один раз, и дальше каждый запрос «сколько чисел в промежутке» стоит логарифм вместо прохода.
Ближайшее число
Задача: найти в отсортированном массиве элемент, ближайший к .
Кандидатов ровно два: первый элемент не меньше и предыдущий. Всё, что дальше, заведомо хуже.
int nearest(const vector<int>& a, int x) {
auto it = lower_bound(a.begin(), a.end(), x);
int best = INT_MAX, answer = 0;
if (it != a.end()) { best = abs(*it - x); answer = *it; }
if (it != a.begin()) {
int prev = *(it - 1);
if (abs(prev - x) < best) answer = prev;
}
return answer;
}
Обе проверки обязательны. it == a.end() бывает, когда больше всех; it == a.begin() — когда меньше всех. Разыменование в этих случаях — обращение за границу массива, и «работает же на сэмплах» здесь ничего не значит.
Отдельно отметим: делать два независимых бинарных поиска — за первым большим и за последним меньшим — не нужно. Один lower_bound плюс шаг назад даёт то же самое вдвое дешевле.
Встречается ли x на отрезке индексов
Дан массив (не отсортированный) и запросы: встречается ли значение среди ?
Сортировать нельзя — индексы важны. Но можно построить обратный указатель: для каждого значения — отсортированный список позиций, где оно стоит.
unordered_map<int, vector<int>> positions;
for (int i = 0; i < n; i++) positions[a[i]].push_back(i);
// внутри каждого вектора индексы уже возрастают
bool occurs(int x, int l, int r) {
auto found = positions.find(x);
if (found == positions.end()) return false;
const vector<int>& p = found->second;
auto it = lower_bound(p.begin(), p.end(), l);
return it != p.end() && *it <= r;
}
Первое вхождение начиная с — и если оно не дальше , ответ положительный. Построение — , запрос — .
Тем же способом считается количество вхождений на отрезке: upper_bound(p, r) - lower_bound(p, l).
Медиана объединения двух массивов
Даны два отсортированных массива. Найти медиану их объединения, не сливая их, — за логарифм.
Идея: провести разрез. Пусть из первого массива в левую половину уходит элементов, тогда из второго — ровно . Разрез корректен, когда всё слева не больше всего справа:
Бинарим по .
long long median(const vector<long long>& a, const vector<long long>& b) {
if (a.size() > b.size()) return median(b, a); // бинарим по короткому
int n = a.size(), m = b.size(), half = (n + m + 1) / 2;
int lo = 0, hi = n;
while (lo <= hi) {
int i = (lo + hi) / 2, j = half - i;
long long aLeft = i > 0 ? a[i - 1] : LLONG_MIN;
long long aRight = i < n ? a[i] : LLONG_MAX;
long long bLeft = j > 0 ? b[j - 1] : LLONG_MIN;
long long bRight = j < m ? b[j] : LLONG_MAX;
if (aLeft <= bRight && bLeft <= aRight) return max(aLeft, bLeft);
if (aLeft > bRight) hi = i - 1; else lo = i + 1;
}
return 0;
}
Три детали, без которых не работает:
- бинарим по короткому массиву, иначе вылетит за границы;
- пустые части закрываются бесконечностями
LLONG_MINиLLONG_MAX, что убирает все проверки на края; halfсчитается с округлением вверх — тогда при нечётной сумме длин медиана оказывается слева, и брать нужноmaxлевых.
Есть и более простой путь к тому же результату — бинарный поиск по значению медианы с проверкой «сколько элементов не превосходит ». Он даёт вместо и пишется втрое короче; на практике почти всегда выбирают его.
MEX после k операций
Дано множество натуральных чисел. Операция: добавить к нему его собственный MEX — наименьшее натуральное, которого в множестве нет. Что окажется добавлено на -м шаге?
Каждая операция затыкает ровно одну «дыру» в натуральном ряду, слева направо. Значит, ответ — -я по счёту дыра.
Бинарим по границе : сколько дыр среди чисел от 1 до ? Это минус количество элементов множества, не превосходящих , — а второе как раз считается upper_bound по отсортированному массиву.
int holes = M - (upper_bound(a.begin(), a.end(), M) - a.begin());
Ищем минимальное , где число дыр достигает . Функция дыр неубывающая, значит бинарный поиск применим.
При ограничениях до можно и просто пройтись по ряду. Но при значениях до прохода нет, а бинарный поиск работает без изменений — это его типичное преимущество.
Общий шаблон
Все задачи выше сводятся к одной формуле:
сколько объектов удовлетворяет условию = (граница справа) − (граница слева)
И к одному правилу: если считаете «сколько», ищите две границы, а не элемент. Поиск конкретного элемента почти всегда лишний шаг.