Дерево отрезков — структура данных для быстрых запросов на отрезках. Узнайте, как построить, обновлять и выполнять запросы на Python. Реализуйте свой первый сегментный код.
Представьте: нужно много раз вычислять сумму на отрезке массива, а потом ещё и обновлять элементы. Если считать в лоб — for i in range(l, r+1): total += arr[i] — то на 10⁵ запросах программа зависнет. Что делать? Дерево отрезков (сегментное дерево) превращает O(n·q) в O((n+q)·log n). Разберёмся, как это работает.
Допустим, у нас есть массив, и мы хотим знать сумму любого отрезка [l, r]. Если разбить отрезок на несколько заранее вычисленных кусков, то нужно сложить только их значения, а не каждый элемент. Дерево отрезков — это бинарное дерево, где каждый узел хранит агрегат (сумму, минимум, максимум и т.д.) для своего отрезка. Корень покрывает весь массив [0, n-1], его дети — левую и правую половины, и так до листьев, которые соответствуют отдельным элементам.
Два ключевых факта:
Итог: построение — O(n), запрос — O(log n), обновление — O(log n).
Вот итеративная версия (без рекурсии) — её удобно использовать на собеседованиях.
class SegTree:
def __init__(self, data):
"""
Построение дерева за O(n).
"""
self.n = len(data)
# size — ближайшая степень двойки >= n
self.size = 1
while self.size < self.n:
self.size <<= 1
# массив дерева: листья начинаются с индекса size
self.tree = [0] * (2 * self.size)
# копируем данные в листья
self.tree[self.size:self.size + self.n] = data
# строим внутренние узлы
for i in range(self.size - 1, 0, -1):
self.tree[i] = self.tree[i << 1] + self.tree[i << 1 | 1]
def _apply(self, pos, value):
"""
Устанавливает лист pos в value и обновляет предков.
"""
idx = pos + self.size
self.tree[idx] = value
idx >>= 1
while idx:
self.tree[idx] = self.tree[idx << 1] + self.tree[idx << 1 | 1]
idx >>= 1
def update(self, idx, value):
"""
Точечное обновление: arr[idx] = value (O(log n)).
"""
self._apply(idx, value)
def query(self, l, r):
"""
Сумма на отрезке [l, r] включительно (O(log n)).
"""
l += self.size
r += self.size + 1 # делаем r исключительным
res = 0
while l < r:
if l & 1:
res += self.tree[l]
l += 1
if r & 1:
r -= 1
res += self.tree[r]
l >>= 1
r >>= 1
return res
update меняет лист и поднимается вверх, пересчитывая предков.query поднимает два указателя l и r, добавляя целые узлы, когда указатель — правый ребёнок (значит, его отрезок полностью внутри запроса). Из-за подъёма на один уровень за итерацию мы касаемся O(log n) узлов.| Ошибка | Последствия | Решение |
|---|---|---|
| Забыть сделать r исключительным в query | Пропуск последнего элемента или выход за границы | Используйте r += size + 1 |
| Неправильный size (не степень двойки) | Некорректные индексы детей, неверные суммы | Вычислите size как наименьшую степень двойки ≥ n |
| Обновление листа без подъёма | Устаревшие значения в узлах | Всегда поднимайтесь до корня после изменения листа |
Дерево отрезков — стандартный инструмент для задач вида «запрос на отрезке + точечное обновление». Вот две классические задачи FAANG:
Имея шаблон дерева отрезков под рукой, вы чувствуете себя увереннее на собеседовании.
Прямо сейчас возьмите лист бумаги или IDE и реализуйте дерево отрезков для поиска минимума на отрезке (range minimum query) с точечными обновлениями. Протестируйте на случайном массиве, сравните скорость с наивным O(n) подходом. Когда увидите ускорение в log раз, вы поймёте, что прокачались.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →