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

# теория · шаг 1 из 6

Ближайшая пара точек: деление, которое упирается в границу

Коротко

Задача: среди n точек на плоскости найти две самые близкие. Перебор пар - n². Деление пополам по координате x выглядит очевидным: реши слева, реши справа, возьми меньшее. Но есть третий случай - пара, разрезанная границей, и именно он делает задачу интересной. Спасает наблюдение: если слева и справа минимум равен d, то опасны только точки в ПОЛОСЕ шириной 2d вокруг разреза. Точек в полосе может быть сколько угодно, но у каждой мало соседей по вертикали - и объединение получается линейным.

Если ты знаешь схему «раздели по x, возьми меньшее из двух, проверь полосу» и понимаешь, зачем полоса нужна, листай до «Теперь сам».

Как думаешь, достаточно ли двух половин

Разрежем точки вертикальной линией пополам: слева n/2, справа n/2. Рекурсивно найдём ближайшую пару в каждой половине; пусть слева минимум 7, справа 5. Ответ - 5?

Нет. Две точки могут лежать по разные стороны от линии, почти вплотную к ней - и расстояние между ними окажется 2. Ни левая рекурсия, ни правая эту пару не увидят: каждая из них смотрит только на свою половину. Значит объединение здесь - не «взять минимум», а отдельная работа.

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

Поиск двух самых похожих людей в очереди, разбитой пополам турникетом. Внутри каждой части сравнить можно, а вот пары «последний до турникета и первый после» видны только если специально посмотреть на стык. И чем лучше ответы внутри частей, тем УЖЕ зона у стыка, в которую вообще стоит смотреть.

Механизм: три случая и полоса

1. отсортировать точки по x (один раз, перед всей рекурсией)
2. если точек не больше трёх - перебрать все пары и вернуть минимум (база рекурсии)
3. разделить отсортированный массив ПО СЕРЕДИНЕ: первые n/2 точек - левая половина,
   остальные - правая; линия разреза проходит по x серединной точки.
   Делим именно по индексу, а не по значению x: при совпадающих x деление
   «по координате» оставило бы половину пустой и рекурсия не закончилась бы
4. d_left  = closest(левая)
   d_right = closest(правая)
   d = min(d_left, d_right)
5. ПОЛОСА: точки, у которых |x − x_разреза| < d
   отсортировать полосу по y
   для каждой точки полосы проверить следующие, пока разница по y < d
6. ответ = минимум из d и найденного в полосе

Три случая, которые покрывает эта схема:

обе точки слева   → нашла левая рекурсия
обе точки справа  → нашла правая рекурсия
по разные стороны → ОБЕ ближе d к разрезу, значит обе в полосе

Третья строчка - и есть обоснование полосы: если пара разрезана границей и расстояние между её точками меньше d, то каждая из них удалена от границы меньше чем на d.

цена:  T(n) = 2·T(n/2) + (сортировка полосы + линейный проход)
       если полосу сортировать заново:  T(n) = 2·T(n/2) + n·log n  →  Θ(n·log² n)
       если поддерживать порядок по y слиянием:  T(n) = 2·T(n/2) + n  →  Θ(n·log n)

Правило

Ближайшая пара делением пополам: отсортировать точки по x ОДИН раз, рекурсивно решить половины, взять d - меньшее из двух ответов, и отдельно проверить пары, разрезанные границей. Такие пары обязаны лежать в полосе шириной 2d вокруг разреза: если расстояние меньше d, то каждая точка ближе d к границе. Полосу сортируют по y и для каждой точки смотрят следующие, пока разница по y меньше d. Сортировка полосы на каждом уровне даёт Θ(n·log² n), поддержание порядка по y слиянием - честные Θ(n·log n). Расстояния сравнивают КВАДРАТАМИ: для сравнения корень не нужен, и на целых координатах арифметика остаётся точной.

Разбор: почему без полосы ответ неверен

Возьмём восемь точек и разрежем их по x = 50.

слева:  (10, 10)  (20, 80)  (30, 30)  (49, 55)
справа: (51, 57)  (70, 20)  (80, 90)  (95, 45)

ближайшая слева:   (10,10)-(30,30)  →  d² = 400 + 400 = 800
ближайшая справа:  (70,20)-(95,45)  →  d² = 625 + 625 = 1250
минимум из двух:   800

а теперь пара через разрез:
(49, 55)-(51, 57)  →  d² = 4 + 4 = 8

Восемь против восьмисот - ответ, который обе рекурсии в упор не видят. Проверим, что полоса его поймает. Здесь один раз возьмём корень для наглядности: d = √800 ≈ 28.3, полоса - точки с |x − 50| < 28.3, то есть x от 21.7 до 78.3. В коде корень не берут - там то же условие пишут как (x − 50)² < 800, и никакой потери точности на целых координатах не возникает. В неё попадают (30,30), (49,55), (51,57), (70,20) - четыре точки из восьми. Отсортировав их по y, получим (70,20), (30,30), (49,55), (51,57), и пара (49,55)-(51,57) стоит рядом.

Заметь, что в полосу попали точки И СЛЕВА, И СПРАВА - иначе смысла бы не было: мы ищем именно разрезанные пары.

Разбор: почему полоса узкая, а не «все точки»

Может показаться, что полоса - это просто «ещё один перебор», только с лишним условием. Разница в том, что её ширина зависит от УЖЕ НАЙДЕННОГО ответа, и чем лучше ответ, тем меньше работы.

если точки распределены равномерно в квадрате n × n,
а минимальное расстояние d мало,
то в полосе шириной 2d оказывается примерно n · (2d / ширина квадрата) точек

Крайний случай, который стоит держать в голове: все точки лежат НА одной вертикальной прямой, близко друг к другу. Тогда в полосу попадут все n точек, и перебор внутри полосы был бы квадратичным. Спасает второе условие - обрыв по разнице y: как только по вертикали расстояние стало не меньше d, дальше смотреть незачем. Сколько точек может оказаться «в пределах d по вертикали» - вопрос следующего урока, и ответ там на удивление мал: не больше семи.

Без обрыва по y алгоритм остаётся верным, но теряет всю асимптотику: T(n) = 2·T(n/2) + n², то есть Θ(n²) - ровно случай «корень тяжелее» из первой главы раздела.

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

Первое: брать минимум из двух половин и забыть про разрезанные пары - ответ будет завышен, причём молча и не на всех входах. Второе: сортировать точки по x внутри рекурсии, а не один раз снаружи: добавится лишний log n на каждом уровне. Третье: путать «шире» и «уже»: полоса с d БОЛЬШИМ, чем нужно (например с минимумом только левой половины), оставляет ответ верным и лишь добавляет работы, а полоса уже d молча теряет разрезанные пары и делает ответ неверным. Четвёртое: сравнивать расстояния, а не их квадраты: корень стоит дорого и портит точность на целых координатах. Пятое: не обрывать внутренний цикл по разнице y - алгоритм останется верным и станет квадратичным.

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

Ближайшая пара - это не про геометрию ради геометрии. Тот же вопрос звучит как «найти два почти одинаковых документа», «обнаружить два объекта на курсе столкновения», «найти дубликаты записей по координатам», «выделить кластеры в облаке точек». В двумерии описанный алгоритм - классика; в пространстве большей размерности он вырождается (полоса перестаёт быть узкой), и там работают другие структуры: kd-деревья, сетки, хеширование, чувствительное к близости. Но приём «раздели, реши, и отдельно разберись с тем, что попало на границу» переносится прямо: так устроены пространственные индексы в базах данных и обнаружение столкновений в играх и симуляторах.

Теперь сам

Слева ближайшая пара даёт d² = 100, справа d² = 64, разрез идёт по x = 40. Какой ширины полосу надо проверить и какие из этих точек в неё попадут: (28, 5), (33, 40), (39, 12), (41, 60), (47, 12), (52, 33)?

Ответ. Сначала берём меньшее из двух: d² = 64, то есть d = 8. Ошибка здесь была бы брать d² = 100: полоса получилась бы шире необходимого - ответ остался бы верным, но работы больше.

Полоса - точки с |x − 40| < 8, то есть x от 32 до 48 (строго). Ширина полосы - 2d = 16. Попадают в неё (33, 40), (39, 12), (41, 60), (47, 12) - четыре точки; (28, 5) и (52, 33) до полосы не дотягивают (|28 − 40| = 12 и |52 − 40| = 12, оба больше 8).

Сравнивать внутри полосы надо по квадратам: точка попадает в полосу, если (x − 40)² < 64. И дальше для каждой точки полосы, идя по возрастанию y, проверяем следующие, пока (y_j − y_i)² < 64; как только разница по вертикали достигла 8, цикл обрывается.

Обрати внимание: и ширина полосы, и обрыв по y используют ОДНО И ТО ЖЕ d - текущий лучший ответ. Если внутри полосы найдётся пара ближе, d можно сразу уменьшить, и оставшаяся работа станет ещё меньше.

Главное

Ближайшая пара делением пополам: отсортировать по x один раз, решить половины рекурсивно, взять d - меньшее из двух ответов, и отдельно проверить пары, разрезанные границей. Такие пары лежат в полосе шириной 2d вокруг разреза: если расстояние меньше d, обе точки ближе d к границе. Полоса сортируется по y, и для каждой точки проверяются следующие, пока разница по y меньше d - без этого обрыва алгоритм остаётся верным, но становится квадратичным. Сортировка полосы на каждом уровне даёт Θ(n·log² n), поддержание порядка по y слиянием - Θ(n·log n). Все сравнения идут по КВАДРАТАМ расстояний: корень не нужен.

Комментарии

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

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

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