# теория · шаг 3 из 6
Что ещё делают этим приёмом
Коротко
Схема «раздели, реши, разберись с границей» работает далеко за пределами ближайшей пары. Ей же
считают число инверсий, выпуклую оболочку (наименьший выпуклый многоугольник вокруг точек; деление пополам с
объединением за линию, подробно - в разделе о геометрииРазбирается в разделе 23, глава «Выпуклая оболочка»: Выпуклая оболочка и заворачивание подарка) и
силуэт города. В многомерии доказательство про полосу не переносится - связать удаётся не все
оси, - и вместо деления берут сетку или kd-дерево (дерево, которое режет точки медианой по
координатам по очереди, - подробноРазбирается в разделе 23, глава «Локализация точки, kd-дерево, диаграмма Вороного»: kd-дерево: ближайший сосед без перебора всех точек); а для ближайшей пары существует
рандомизированный алгоритм за ожидаемые O(n). Общий признак задач, которые поддаются этому
приёму: ответ либо целиком в одной половине, либо задевает границу, и «пограничные» случаи можно
описать узкой зоной.
Если ты знаешь ещё три-четыре задачи, решаемые делением с разбором границы, и понимаешь, почему в многомерии схема ломается, листай до «Теперь сам».
Как думаешь, что общего у этих задач
Посмотри на четыре задачи: найти ближайшую пару точек; посчитать, сколько пар элементов массива стоят в неправильном порядке; построить выпуклую оболочку множества точек; нарисовать силуэт города из прямоугольных домов.
Геометрия, комбинаторика, графика - разные области. Но решаются все четыре одинаково, и не потому, что кто-то так захотел: у них одинаковое устройство ответа.
На что это похоже
Разбор архива по годам. Хочешь посчитать, сколько документов ссылаются друг на друга: раздели архив на две половины, посчитай внутри каждой, а потом отдельно разберись со ссылками «из первой половины во вторую». Если эти межполовинные ссылки удаётся посчитать не перебором, а одним проходом, - схема работает, какой бы ни была предметная область.
Механизм: четыре задачи одним приёмом
ЧИСЛО ИНВЕРСИЙ (пар i < j, где a[i] > a[j])
делим пополам, считаем внутри каждой половины
граница: при слиянии, забирая элемент из правой половины,
прибавляем число оставшихся слева - это инверсии через разрез
T(n) = 2·T(n/2) + n → Θ(n·log n)
ВЫПУКЛАЯ ОБОЛОЧКА
делим точки по x, строим оболочку каждой половины
граница: найти две «мостовые» касательные между оболочками за O(n)
T(n) = 2·T(n/2) + n → Θ(n·log n)
СИЛУЭТ ГОРОДА (skyline)
делим список домов пополам, строим силуэт каждой половины
граница: слить два силуэта, идя слева направо и беря максимум высоты
T(n) = 2·T(n/2) + n → Θ(n·log n)
БЛИЖАЙШАЯ ПАРА
граница: полоса шириной 2d, семь соседей по y
Общий признак: ответ либо целиком внутри половины, либо задевает границу, и пограничные случаи описываются узкой зоной или одним линейным проходом.
КОГДА СХЕМА ЛОМАЕТСЯ
многомерие в k измерениях ограничить удаётся не все оси, и константа «семь»
не получается вовсе: сетка вместо деления, 3^k клеток на точку
перекрытие подзадачи запрашивают одно и то же - это уже не деление пополам,
а динамическое программирование (раздел 15): ответы подзадач
запоминают, чтобы не считать дважды
дорогая граница объединение стоит n² - выигрыш исчезает целиком
Правило
Приём применим, когда ответ распадается на «внутри левой», «внутри правой» и «через границу», а
пограничный случай считается за линию или почти за линию. Так решаются ближайшая пара, число
инверсий, выпуклая оболочка, силуэт города, подсчёт пересечений. В пространстве большой размерности
схема вырождается: аналог полосы содержит слишком много точек, и вместо деления берут сетку,
kd-дерево или хеширование, чувствительное к близости. Для ближайшей пары существует и
рандомизированный алгоритм с ожидаемым временем O(n) - он строит сетку с шагом текущего минимума
и перестраивает её по мере уточнения.
Разбор: число инверсий - та же схема без геометрии
Массив [5, 2, 8, 1]. Инверсия - пара i < j, где a[i] > a[j].
делим: [5, 2] и [8, 1]
внутри левой: (5,2) - инверсия 1
внутри правой: (8,1) - инверсия 1
через границу: сливаем [2,5] и [1,8]
берём 1 из правой половины → слева ещё остались 2 и 5 → +2 инверсии
берём 2 из левой → ничего не прибавляем
берём 5 из левой → ничего
берём 8 из правой → слева не осталось ничего → +0
итого через границу 2
всего: 1 + 1 + 2 = 4
Проверим перебором: пары (5,2), (5,1), (2,1), (8,1) - четыре. Сходится.
Смотри, что здесь играет роль «полосы»: ничего геометрического, но структура та же. Пары внутри половин посчитаны рекурсией, пары через разрез - одним линейным проходом, попутно с обычным слиянием. Именно поэтому эта задача уже встречалась в разделе про рекуррентности: она и есть сортировка слиянием с одним дополнительным счётчиком.
Разбор: почему в многомерии всё разваливается
Попробуем повторить подсчёт из прошлого урока в трёх измерениях - и увидим, что он НЕ повторяется.
На плоскости опасная зона была ограничена с двух сторон, и обе границы взялись из разных мест: по
x зону сужала полоса (2d - по d в каждую сторону от разреза), по y - порядок сканирования
(d вперёд по отсортированному списку). Осей всего две, и обе оказались связаны - отсюда
прямоугольник 2d × d и восемь квадратов.
плоскость: ось x - ограничена полосой → 2d
ось y - ограничена сканированием → d
итого замкнутый прямоугольник 2d × d, 8 квадратов со стороной d/2
три оси: ось x - ограничена полосой → 2d
ось z - ограничена сканированием → d
ось y - НЕ ОГРАНИЧЕНА НИЧЕМ → зона бесконечна
Вот и вся беда: в трёх измерениях связать удаётся по-прежнему только две оси, а третья остаётся свободной, и опасных точек может быть сколько угодно. Никакого «шестнадцать кубиков» из этого не выводится - доказательство обрывается на первом же шаге.
Поэтому в многомерии деление пополам заменяют СЕТКОЙ с ребром d: точка может оказаться ближе d
только к точкам своей клетки и соседних, а соседних клеток 3^k − 1.
клеток на одну точку: k = 2 → 3² = 9 k = 3 → 3³ = 27
k = 10 → 59 049 k = 20 → 3²⁰ ≈ 3.5·10⁹
Пока k мало, это отличная замена: девять клеток на плоскости - та же константа, что «семь
соседей». Но константа растёт как 3^k, и «линейное» объединение дешевле перебора пар только пока
n больше этой константы. При k = 20 пришлось бы иметь больше семи миллиардов точек, чтобы
сетка окупилась, - на любых реальных данных честный перебор пар быстрее.
Это частный случай общего явления, которое называют проклятием размерности: в пространстве
большой размерности «близко» перестаёт быть редкостью, объём сосредотачивается у границ, и
геометрические приёмы, очевидные на плоскости, ломаются. Практический ответ на это - не улучшать
деление, а менять структуру: kd-деревья работают до десятка измерений, дальше применяют
приближённые методы вроде хеширования, чувствительного к близости.
Что ломается без этого
Первое: пытаться применить деление пополам к задаче с перекрывающимися подзадачами - получится
экспонента вместо линии. Второе: думать, что «через границу» всегда считается за линию: у выпуклой
оболочки поиск касательных линеен только при аккуратной реализации, наивный поиск даёт n² и
убивает схему. Третье: переносить константу 7 в трёхмерие - там она другая, и с ростом размерности
растёт экспоненциально. Четвёртое: забывать, что рандомизированный O(n) для ближайшей пары -
ОЖИДАЕМОЕ время, а не гарантия; там, где нужна гарантия, остаётся n·log n. Пятое: выбирать
kd-дерево для данных большой размерности: на сотне измерений оно вырождается в полный перебор,
только с накладными расходами.
Зачем это в работе
Список задач выше - это половина типовых вопросов на собеседованиях по алгоритмам, и узнавание
схемы экономит время: увидел «посчитать пары», «построить силуэт», «найти пересечения» - сразу
проверь, распадается ли ответ на «внутри половин» плюс «через границу». В продуктовой разработке
прямее всего пригождается родственная идея - пространственная индексация: сетка для поиска
ближайших объектов на карте, kd-дерево для рекомендаций по признакам, приближённый поиск
соседей для векторных баз. И полезно помнить границу применимости: всё это отлично работает на
двух-трёх измерениях и плохо - на сотнях, а векторные представления текста как раз сотнемерные,
поэтому там и живут приближённые методы.
Теперь сам
Дан массив [3, 1, 4, 2]. Посчитай число инверсий делением пополам: внутри половин и через
границу.
Ответ. Делим на [3, 1] и [4, 2].
внутри левой: пара (3,1) - инверсия 1
внутри правой: пара (4,2) - инверсия 1
через границу: сливаем отсортированные [1,3] и [2,4]
берём 1 из левой → +0
берём 2 из правой → слева осталось ещё 3 → +1
берём 3 из левой → +0
берём 4 из правой → слева не осталось → +0
итого через границу 1
всего: 1 + 1 + 1 = 3
Проверка перебором: пары (3,1), (3,2), (4,2) - три. Сходится.
Обрати внимание на ключевой момент слияния: когда мы берём элемент из ПРАВОЙ половины, все оставшиеся в левой больше него, и все они образуют с ним инверсии - значит их можно прибавить разом, не перебирая. Это и есть «линейное объединение», которое делает схему быстрой; без него пришлось бы сравнивать каждый с каждым.
Главное
Приём «раздели, реши, разберись с границей» применим, когда ответ распадается на «внутри левой»,
«внутри правой» и «через границу», а пограничный случай считается за линию. Так решаются ближайшая
пара, число инверсий (слияние с подсчётом), выпуклая оболочка (поиск мостовых касательных), силуэт
города (слияние силуэтов). В многомерии доказательство не переносится: ограничить удаётся только две оси из k, остальные
свободны. Там берут сетку с ребром d - и просматривают 3^k клеток на точку, то есть константа
растёт экспоненциально с размерностью. Это проклятие размерности, из-за которого на десятках
измерений переходят к kd-деревьям и приближённому поиску соседей. Для ближайшей
пары есть и рандомизированный алгоритм с ОЖИДАЕМЫМ O(n), но гарантию по-прежнему даёт только
n·log n.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий