1. Бинарный поиск и префиксные суммы
Бинарный поиск и префиксные суммы
Представь: у тебя есть отсортированный список из миллиона чисел, и нужно найти, есть ли там число 7 438 291. Перебор займёт миллионы операций. Бинарный поиск справится за 20 шагов. Почему? Потому что каждый шаг делит пространство поиска пополам — и это не просто трюк, а фундаментальный принцип, на котором строятся базы данных, файловые системы и поисковые движки.
Как работает бинарный поиск
Суть алгоритма: мы знаем, что массив отсортирован, поэтому можем сравнивать искомое значение с серединным элементом текущего диапазона. Если значение меньше — ищем в левой половине, если больше — в правой. Повторяем, пока диапазон не сузится до одного элемента.
Инвариант бинарного поиска — ключевое понятие для интервью. Это условие, которое остаётся истинным на каждом шаге: «ответ всегда находится в диапазоне [left, right]». Если ты нарушишь инвариант — получишь бесконечный цикл или пропущенный ответ.
Обрати внимание на детали: left + (right - left) // 2 вместо (left + right) // 2 — это защита от целочисленного переполнения в языках с фиксированным размером типа. В Python это не критично, но интервьюеры замечают такие вещи.
Три варианта бинарного поиска
На практике бинарный поиск редко используется в «чистом» виде. Чаще встречаются три модификации:
| Вариант | Когда использовать | Что возвращает | |---------|-------------------|----------------| | Поиск точного значения | Найти элемент в отсортированном массиве | Индекс или -1 | | Поиск левой границы | Найти первый элемент target | Индекс вставки | | Поиск правой границы | Найти последний элемент target | Индекс последнего вхождения |
Поиск левой границы — самый частый гость на собеседованиях. Он отвечает на вопрос: «где бы стоял элемент target, если бы он был в отсортированном массиве?»
Здесь right инициализируется как len(arr), а не len(arr) - 1, потому что ответ может указывать за пределы массива. Цикл работает при left < right, а не left <= right — это классический паттерн, который стоит запомнить.
Бинарный поиск по ответу
Самый мощный приём — бинарный поиск не по элементам массива, а по пространству ответов. Идея: если мы можем проверить, является ли кандидат «достаточно хорошим» за линейное время, то можем сузить пространство кандидатов бинарным поиском.
Классический пример: разместить объектов на прямой так, чтобы минимальное расстояние между любыми двумя было максимально. Решение — бинарный поиск по ответу: перебираем возможное минимальное расстояние и проверяем жадным алгоритмом, можно ли расставить объекты.
Префиксные суммы
Теперь представь другую задачу: дан массив чисел, и нужно многократно отвечать на запросы «чему равна сумма элементов с индекса по включительно?». Линейный перебор на каждый запрос — слишком медленно. Префиксные суммы решают это за на запрос после предобработки.
Идея: строим вспомогательный массив prefix, где prefix[k] — сумма первых элементов исходного массива. Тогда сумма на отрезке равна prefix[j+1] - prefix[i].
Элегантность в том, что мы торгуем память на скорость: один проход для построения — и любое количество запросов за константное время.
Префиксные суммы на двумерных массивах
Для матриц применяется тот же принцип, но с двумерным массивом префиксных сумм. Сумма на подматрице от до вычисляется по формуле включений-исключений:
где — двумерный массив префиксных сумм. Четвёртое слагаемое добавляется обратно, потому что мы вычли его дважды.
Когда что использовать
Бинарный поиск эффективен, когда данные отсортированы или пространство ответов монотонно. Префиксные суммы — когда нужно быстро отвечать на запросы суммирования на подотрезках. Часто эти техники комбинируются: например, префиксные суммы помогают проверить кандидата в бинарном поиске по ответу.
Ловушка интервью: бинарный поиск кажется простым, но 80% ошибок — в определении инварианта и выборе между left <= right и left < right. Перед написанием кода всегда проговаривай: что инвариант, что возвращаем, что делаем при arr[mid] == target.