1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 2 из 5

Интервальное дерево: другое поле - другая операция

Коротко

Тот же приём дополнения, но поле другое - и операция другая. Интервальное дерево хранит ОТРЕЗКИ [низ, верх], упорядоченные по левому концу (как ключу BST), и дополняет каждый узел полем макс. верхний конец во всём его поддереве. С этим полем дерево за O(log n) отвечает на запрос «есть ли в наборе отрезок, ПЕРЕСЕКАЮЩИЙ данный?» - вместо перебора всех O(n). Секрет в поле max: стоя в узле, по нему можно решить, в какой ребёнок идти, а какой отбросить целиком, - если в левом поддереве максимальный правый конец меньше начала запроса, там пересечений быть не может. Это показывает суть дополнения: сменил локально-восстановимое поле (size → max конца) - получил новую операцию (ранг → пересечение), не меняя ни балансировку, ни старые операции.

Если ты понимаешь, что интервальное дерево - это BST по левому концу с полем «макс правый конец поддерева», и как это поле за O(log n) находит пересекающийся отрезок, отбрасывая поддерево, листай до «Теперь сам».

Как думаешь, как быстро найти пересекающийся отрезок

Задача: есть набор отрезков (брони переговорки по времени, диапазоны IP, интервалы генома), и спрашивают - пересекается ли данный отрезок [l, r] с каким-нибудь из набора? В лоб - перебрать все и проверить каждый: O(n) на запрос. Хочется O(log n). Отрезки можно сложить в дерево поиска по ЛЕВОМУ концу - но одного этого мало: два отрезка пересекаются по сложному условию (l ≤ чужой_верх И чужой_низ ≤ r), которое по левому концу напрямую не проверить. Нужна подсказка, позволяющая на каждом узле отбросить ЦЕЛОЕ поддерево, где пересечений точно нет. Такой подсказкой станет одно дополнительное число в узле - максимальный правый конец среди всех отрезков поддерева.

На что это похоже

Диспетчер, раскладывающий заявки на аренду зала по времени начала, и на каждой стопке заявок пометка: «самое позднее ОКОНЧАНИЕ в этой стопке - такое-то». Пришёл клиент с интервалом - ищем конфликт. Смотрим левую стопку: если там самое позднее окончание РАНЬШЕ, чем начало клиента, значит все заявки слева заканчиваются до его прихода - конфликта там быть не может, стопку целиком откладываем. Иначе лезем в неё. Пометка «самое позднее окончание» позволяет отбрасывать стопки не глядя внутрь - и находить конфликт за считанные шаги, а не перебором всех заявок.

Механизм: BST по левому концу + поле max

Узел хранит отрезок [низ, верх]; КЛЮЧ дерева - левый конец низ. Дополнительное поле:

max(узел) = наибольший ВЕРХНИЙ конец среди всех отрезков поддерева
          = max(узел.верх, max(узел.лево), max(узел.право))     ← восстановимо из детей за O(1)

Запрос «найти отрезок, пересекающий [l, r]» (два отрезка пересекаются, если низ ≤ r И l ≤ верх):

поиск_пересечения(узел, l, r):
    пока узел не пуст:
        если отрезок узла пересекает [l, r]:  вернуть его
        если узел.лево не пусто И max(узел.лево) >= l:
            узел = узел.лево          # в левом поддереве МОЖЕТ быть пересечение
        иначе:
            узел = узел.право         # слева все верхние концы < l → пересечений нет, идём вправо
    вернуть «нет»

Разбор ключевого шага: если max(узел.лево) < l, то ВСЕ отрезки левого поддерева заканчиваются раньше l, значит ни один не дотянется до [l, r] - левое поддерево отбрасываем целиком и идём вправо. Так на каждом узле отсекается одна сторона, спуск идёт по одному пути - O(log n). Поле max обновляют при вставке/удалении на пути и при вращениях (O(1) по формуле) - балансировка цела, как и с полем size.

[15, 20]max=30 [8, 12]max=18 [22, 30]max=30 [5, 18]max=18 запрос [21, 25]: у левого поддерева max=18 < 21 → отбрасываем, идём вправо → [22, 30] пересекает max = наибольший правый конец поддерева, восстановим из детей за O(1)
Отрезки в дереве по левому концу, поле max - наибольший правый конец поддерева. Если max левого поддерева меньше начала запроса, там пересечений нет - отбрасываем и идём вправо. O(log n).

Правило

Интервальное дерево - тот же приём дополнения с ДРУГИМ полем: BST по левому концу отрезков плюс поле max (наибольший правый конец поддерева, восстановимо из детей за O(1)). Оно за O(log n) находит отрезок, ПЕРЕСЕКАЮЩИЙ данный: если max(левого поддерева) < начала запроса, все левые отрезки кончаются раньше - отбрасываем их и идём вправо, иначе влево. Поле обновляют при вставках/удалениях и вращениях (O(1)), балансировка не меняется. Сменил поле (size → max конца) - получил новую операцию (ранг → пересечение), не трогая старую структуру.

Разбор: почему поле max позволяет отбросить целое поддерево

Вся скорость держится на одном наблюдении. Два отрезка пересекаются, только если один начинается не позже, чем другой кончается. Стоя в узле, мы хотим понять: может ли в ЛЕВОМ поддереве быть отрезок, пересекающий [l, r]? Для пересечения такому отрезку нужно кончаться не раньше l (его верхний конец ≥ l). Поле max(левого) - это САМЫЙ ПОЗДНИЙ верхний конец во всём левом поддереве. Если даже он < l, то и все остальные там кончаются ещё раньше - НИ ОДИН не дотянется до l, пересечения в левом поддереве невозможны, и мы отбрасываем его целиком, не заглядывая. Одно число заменило проверку всех отрезков поддерева. А если max(левого) ≥ l, пересечение ТАМ возможно (не гарантировано, но возможно) - идём влево проверять. А почему тогда безопасно отбрасывать ПРАВОЕ поддерево, уходя влево? Потому что если в левом поддереве есть отрезок с концом ≥ l, но пересечения там всё же нет, то этот отрезок начинается ПОЗЖЕ r; а все отрезки правого поддерева начинаются не раньше него (дерево упорядочено по левому концу) - значит и они начинаются позже r, и пересечения справа тоже нет. В любом случае на каждом узле отсекается одна ветвь, и спуск - O(log n). Это и есть сила дополнения: удачно выбранное поле превращает «проверить всех» в «дойти».

Разбор: почему это тот же приём, что и порядковая статистика

Стоит увидеть, что интервальное дерево и дерево порядковой статистики - НЕ два разных изобретения, а один шаблон с разной начинкой. Шаблон: (1) берём сбалансированное BST; (2) добавляем в узел поле, восстановимое из детей за O(1); (3) поле обновляем при вставке/удалении и вращениях (O(1)); (4) новая операция спускается по дереву, используя поле, чтобы на каждом узле отбросить поддерево. Меняется лишь поле и правило спуска: size (число узлов) → операции про ПОЗИЦИЮ (select, rank); max правого конца → операция про ПЕРЕСЕЧЕНИЕ; сумма поддерева → диапазонные суммы; min/max ключа → быстрые экстремумы поддерева. Старые операции (поиск, вставка, удаление, баланс) при этом не меняются вовсе - дополнение их не касается. Поэтому дополнение - это не набор трюков, а один метод: «какое локальное поле ответит на мой запрос спуском?». Освоив шаблон, ты конструируешь нужную структуру под задачу, а не ищешь готовую.

Что ломается без этого

Первая ошибка - выбрать поле, не отвечающее на нужный вопрос: для пересечения интервалов нужен именно МАКСИМУМ правого конца (не левого, не сумма) - только он позволяет отбросить поддерево «все кончаются раньше l». Поле подбирают ПОД правило отсечения. Вторая - забыть обновлять max при вращениях: после балансировки поле у переставленных узлов станет неверным, и запрос начнёт пропускать пересечения. Третья - неверное условие пересечения: отрезки [a,b] и [l,r] пересекаются, если a ≤ r И l ≤ b; перепутаешь знаки - получишь ложные ответы. Четвёртая - ждать, что интервальное дерево вернёт ВСЕ пересечения за O(log n): за O(log n) оно находит ОДНО (или отвечает «есть/нет»); чтобы перечислить все k пересечений, нужно O(k·log n) или O(k + log n) в зависимости от реализации.

Зачем это в работе

Интервальные деревья решают запросы про диапазоны там, где перебор O(n) недопустим: расписания и брони (конфликтует ли новая встреча с существующими), сети (какие правила файрвола накрывают данный порт/IP-диапазон), вычислительная геометрия (пересечения отрезков, оконные запросы), биоинформатика (перекрытия участков генома), базы данных с типами-диапазонами. Но главная ценность урока - не конкретная структура, а ЗАКРЕПЛЕНИЕ приёма дополнения: ты увидел его дважды (size для рангов, max для пересечений) и теперь понимаешь шаблон, а не два частных случая. Это меняет способ мышления: встретив запрос, которого «стандартная структура не умеет», ты спрашиваешь «каким полем дополнить дерево, чтобы ответить спуском?» - и часто конструируешь решение сам. Тот же метод даёт диапазонные суммы (следующая глава - дерево отрезков и Фенвика довели идею до предела). Дополнение - один из самых практичных инструментов в арсенале, и он именно про КОНСТРУИРОВАНИЕ структур под задачу.

Теперь сам

В интервальном дереве (см. схему) запрос [13, 14] - есть ли пересекающийся отрезок? Пройди поиск и объясни, где поле max позволяет (или не позволяет) отбросить поддерево.

Ответ. Начинаем с корня [15, 20]. Пересекается ли он с [13, 14]? Условие: 15 ≤ 14 (низ узла ≤ верх запроса)? Нет, 15 > 14 - НЕ пересекается. Решаем, куда идти. Левое поддерево есть, и max(левого) = 18. Сравниваем с началом запроса l = 13: 18 ≥ 13 - значит в левом поддереве МОЖЕТ быть пересечение (там есть отрезки, кончающиеся не раньше 13), идём ВЛЕВО. Узел [8, 12]: пересекается с [13, 14]? 8 ≤ 14 да, но 13 ≤ 12? нет - НЕ пересекается (он кончается в 12, запрос начинается в 13). Идём дальше: левое поддерево [8,12] - это [5, 18], max(левого) = 18 ≥ 13 - снова возможно, влево. Узел [5, 18]: 5 ≤ 14 да И 13 ≤ 18 да - ПЕРЕСЕКАЕТСЯ! Нашли [5, 18] (он тянется с 5 до 18 и накрывает [13,14]). Заметь работу поля: если бы max(левого) где-то оказался < 13, мы бы отбросили целое левое поддерево не глядя и ушли вправо. Здесь max был ≥ 13, поэтому мы каждый раз шли ВЛЕВО, отбрасывая правые поддеревья. Так или иначе на каждом узле отсекалась одна сторона, и мы прошли по одному пути - O(log n), а не перебрали все отрезки.

Главное

Интервальное дерево - приём дополнения с полем max (наибольший правый конец поддерева, восстановимо из детей за O(1)): BST по левому концу отрезков плюс это поле находит ПЕРЕСЕКАЮЩИЙ отрезок за O(log n). Правило спуска: если max(левого) < начала запроса, все левые отрезки кончаются раньше - отбрасываем их, идём вправо; иначе влево. Одно число заменяет проверку всех отрезков поддерева. Поле обновляют при вставках/удалениях и вращениях (O(1)) - старые операции и баланс не меняются. Это ТОТ ЖЕ шаблон, что порядковая статистика (только поле size → max конца, операция ранг → пересечение): «каким локально-восстановимым полем дополнить дерево, чтобы ответить на запрос спуском». Дополнение - метод конструирования структур под задачу, а не набор трюков.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.