EssayAI
Блог
Блог
Математика и алгоритмы

Алгоритм Чана: как строить выпуклую оболочку за O(n log h)

14 марта 2026Время чтения: 9 минут
#алгоритм Чана#выпуклая оболочка#вычислительная геометрия#output-sensitive#Chan algorithm
Алгоритм Чана: как строить выпуклую оболочку за O(n log h)

Алгоритм Чана (Chan's algorithm, опубликован Тимоти Чаном в 1996 году) строит выпуклую оболочку nn точек на плоскости за время O(nlogh)O(n \log h), где hh - число вершин самой оболочки. Это output-sensitive результат: чем меньше оболочка, тем быстрее работает алгоритм, причём асимптотика оптимальна по нижней оценке для output-sensitive класса. Идея красивая: взять Graham scan, который силён на O(nlogn)O(n \log n), и Jarvis march, который силён на O(nh)O(nh), и комбинировать их так, чтобы каждый компенсировал слабость другого. Параметр mm - гипотетический «угаданный» размер оболочки - подбирается через геометрическую прогрессию с early termination.

Мотивация: чем плохи Graham и Jarvis по отдельности

Классические алгоритмы выпуклой оболочки берут крайние точки множества SS из nn точек на плоскости. Graham scan (1972) сортирует точки по полярному углу относительно нижней опорной и обходит их стеком с проверкой поворота - итого O(nlogn)O(n \log n). Эта оценка оптимальна в худшем случае: задача сводится к сортировке, и Ω(nlogn)\Omega(n \log n) - нижняя оценка по модели сравнений.

Jarvis march (gift wrapping, 1973) идёт иначе: начиная с граничной точки, шаг за шагом ищет следующую вершину оболочки как «самую правую» из оставшихся. Каждый шаг - O(n)O(n), всего hh шагов, итого O(nh)O(nh). Если оболочка маленькая (h=O(logn)h = O(\log n)), Jarvis быстрее Грэхема. Если оболочка огромная (h=Θ(n)h = \Theta(n), например, точки на окружности), Jarvis деградирует до O(n2)O(n^2).

Возникает вопрос: можно ли получить алгоритм, который всегда не хуже обоих - то есть O(nlogh)O(n \log h)? Output-sensitive нижняя оценка Ω(nlogh)\Omega(n \log h) известна давно. Первый match - Kirkpatrick–Seidel «ultimate planar convex hull» (1986) - достигает O(nlogh)O(n \log h), но через сложный bridge-finding с медианой и prune-and-search. Чан в 1996 нашёл намного более простую конструкцию из двух стандартных кубиков.

Идея: Graham внутри, Jarvis снаружи

Пусть нам как-то заранее «угадан» размер оболочки mhm \approx h. Алгоритм Чана делает следующее.

Шаг 1. Разбиение и локальные оболочки. Разбить nn точек на n/m\lceil n/m \rceil групп по mm точек. В каждой группе построить локальную выпуклую оболочку обычным Graham scan за O(mlogm)O(m \log m). Суммарно по всем группам - O((n/m)mlogm)=O(nlogm)O((n/m) \cdot m \log m) = O(n \log m).

Шаг 2. Jarvis march по группам. Запустить Jarvis на «макроуровне»: на каждом шаге надо найти следующую вершину глобальной оболочки. Делается это так - для каждой из n/mn/m локальных оболочек найти касательную из текущей вершины к этой оболочке. Касательная к выпуклому многоугольнику размером mm ищется бинарным поиском за O(logm)O(\log m). Среди n/mn/m кандидатов выбрать «самого правого» - O(n/m)O(n/m) сравнений. Один шаг Jarvis - O((n/m)logm)O((n/m) \log m). Всего шагов - hh, итого O((nh/m)logm)O((nh/m) \log m).

Сложение. O(nlogm)+O((nh/m)logm)O(n \log m) + O((nh/m) \log m). При m=hm = h получаем O(nlogh)+O(nlogh)=O(nlogh)O(n \log h) + O(n \log h) = O(n \log h). То есть если бы мы знали hh, всё бы сошлось.

Угадывание hh через геометрическую прогрессию

Проблема - мы не знаем hh заранее: оно и есть то, что мы хотим вычислить. Решение Чана - пробовать mm по геометрической прогрессии mt=22tm_t = 2^{2^t} (или проще - mt=2,4,8,16,m_t = 2, 4, 8, 16, \ldots с двойным возведением) и запускать алгоритм с early termination: если после mm шагов Jarvis обход не замкнулся, значит, h>mh > m, прерываем и пробуем следующее mm.

Точнее, на итерации tt выставляем m=min(22t,n)m = \min(2^{2^t}, n), выполняем шаги 1 и 2, но ограничиваем Jarvis-цикл mm итерациями. Если оболочка построилась (вернулись в стартовую вершину за m\leq m шагов) - выводим её и стоп. Если нет - увеличиваем tt и повторяем.

Затраты по итерациям складываются в геометрический ряд:

T=t=0log2log2hO(nlogmt)=O ⁣(nt=0loglogh2t)=O(n2loglogh+1)=O(nlogh).T = \sum_{t=0}^{\lceil \log_2 \log_2 h \rceil} O(n \log m_t) = O\!\left(n \sum_{t=0}^{\log\log h} 2^t\right) = O(n \cdot 2^{\log\log h + 1}) = O(n \log h).

Последняя итерация даёт mthm_t \geq h, и алгоритм гарантированно завершается. Сумма доминируется последним членом - это и есть итоговое O(nlogh)O(n \log h).

Корректность

Локальные оболочки шага 1 содержат все граничные точки своих групп (Graham scan корректен). Любая вершина глобальной оболочки conv(S)\mathrm{conv}(S) - граничная точка своей группы (если бы была внутренней, нашлась бы группа-сосед с точкой левее/правее, противоречие). Значит, каждая вершина глобальной оболочки присутствует хотя бы в одной локальной.

Касательная из точки pp к выпуклому многоугольнику размером mm корректно находится бинарным поиском по углам. Шаг Jarvis выбирает «самую правую» точку среди всех касательных - это и есть следующая глобальная вершина. Обход замыкается за hh шагов, потому что глобальная оболочка имеет ровно hh вершин.

Early termination не теряет корректности: если оболочка не замкнулась за mm шагов, мы выкидываем промежуточные результаты и перезапускаем с большим mm. Не запускается ни одной лишней операции на корректном выходе.

Итоговая сложность O(nlogh)O(n\log h)

Соберём:

  • Шаг 1 на итерации tt - O(nlogmt)O(n \log m_t).
  • Шаг 2 на итерации tt - O((nmt/mt)logmt)=O(nlogmt)O((n m_t / m_t) \log m_t) = O(n \log m_t) (потому что мы ограничили Jarvis mtm_t шагами).
  • Итерации t=0,1,,log2log2ht = 0, 1, \ldots, \lceil \log_2 \log_2 h \rceil.

Каждая итерация - O(nlogmt)=O(n2t)O(n \log m_t) = O(n \cdot 2^t). Сумма геометрическая, доминируется последним членом, который O(nlogh)O(n \log h). Итог: O(nlogh)O(n \log h).

Память - O(n)O(n): хранятся точки, локальные оболочки, текущая глобальная вершина. Никаких дополнительных структур.

Сравнение с Kirkpatrick–Seidel

Оба алгоритма достигают O(nlogh)O(n \log h) и оба оптимальны output-sensitive. Различия:

  • Конструкция. Чан - два знакомых кубика (Graham + Jarvis) и геометрическая прогрессия. Kirkpatrick–Seidel - bridge-finding с prune-and-search на медиане, 22-fold marriage-before-conquest. Реализация Чана умещается в 80 строк C++, K-S - 200+.
  • Константа. У Чана константа выше в Jarvis-шаге (бинарный поиск касательной для каждой группы), но проще структура памяти. На практике в библиотеках (CGAL, Boost.Geometry) чаще берут Чана или Andrew's monotone chain - K-S редко.
  • 3D-обобщение. Чан обобщается до O(nlogh)O(n \log h) в 3D (Chan 1996 же), K-S - нет.

Поэтому Чан - фактический рабочий «output-sensitive ultimate» алгоритм.

Когда брать алгоритм Чана

  • Огромные облака точек с малой оболочкой. GPS-треки, LiDAR-сканы, выборки из равномерного распределения на квадрате (там h=O(logn)h = O(\log n) в среднем). Чан даёт O(nloglogn)O(n \log \log n), Graham - O(nlogn)O(n \log n).
  • Стримы сенсоров. Когда nn велико, а граница мала и заранее неизвестна - Чан адаптируется.
  • 3D convex hull в варианте Chan 1996 - O(nlogh)O(n \log h) в 3D, единственный простой output-sensitive.

Не брать, если hnh \approx n (точки на окружности) - там Graham/Andrew проще и константа меньше. Также не брать, если важна устойчивость к коллинеарным случаям без специальной обработки - Чан наследует все подводные камни и Graham, и Jarvis сразу.

Типовые задачи

  • Минимальный охватывающий многоугольник. Когда нужна только сама оболочка для дальнейших операций (диаметр, площадь, проверка вхождения).
  • Broad-phase коллизии в физике. Convex hull mesh как грубое приближение формы.
  • GIS-аналитика. Ареал наблюдений по тысячам/миллионам отметок.
  • Кластеризация в ML. Convex hull кластера для визуализации и проверки разделимости.
  • Олимпиадные задачи. При n106n \geq 10^6 и заранее малом hh Чан проходит, Graham - нет.

Частые ошибки

  • Берут mt=2tm_t = 2^t вместо mt=22tm_t = 2^{2^t}. Тогда число итераций - logh\log h, и сумма nlog2t=O(nlog2h)\sum n \log 2^t = O(n \log^2 h) - на log\log хуже оптимума.
  • Не ограничивают Jarvis mm шагами. Без early termination первая же итерация с m<hm < h просто зациклится или построит мусор; алгоритм перестаёт быть output-sensitive.
  • Используют линейный поиск касательной вместо бинарного. Касательная к выпуклому многоугольнику размером mm ищется за O(logm)O(\log m) - это критично для асимптотики.
  • Не сжимают дубликаты и не обрабатывают коллинеарные точки. Те же баги, что в Graham и Jarvis, унаследованы целиком.
  • Считают crosscross с плавающей точкой при больших координатах. Целочисленный int64\text{int64} обязателен, иначе знак поворота врёт.

FAQ

Чем Chan's algorithm лучше Kirkpatrick–Seidel при одинаковой O(nlogh)O(n \log h)? Реализация. Чан - это пять-семь страниц учебника с двумя стандартными кубиками. K-S - bridge-finding на медиане, prune-and-search, 22-fold conquer; в учебниках занимает раздел на 20 страниц. На практике Чан реализован в CGAL и широко используется; K-S остаётся в основном теоретическим результатом. Асимптотика одинаковая, константа у Чана чуть выше, но кодоёмкость в разы меньше.

Можно ли построить трёхмерную выпуклую оболочку Чаном? Да. В той же работе 1996 года Чан показал O(nlogh)O(n \log h) для 3D через тот же приём - разбить на группы, локально построить O(nlogn)O(n \log n) или O((n/m)mlogm)O((n/m) \cdot m \log m) оболочкой, обходить «снаружи» с касательными плоскостями. В 3D output-sensitive нижняя оценка тоже Ω(nlogh)\Omega(n \log h), и Чан её достигает.

Что выбрать на собеседовании, если просят convex hull? Сначала уточните: «Знаем ли мы что-то про hh?». Если нет - Andrew's monotone chain (O(nlogn)O(n \log n), проще Graham и стабильнее численно). Если ожидается малая оболочка на больших данных - Chan (O(nlogh)O(n \log h)). На полу-устной задаче можно обойтись Graham - все его знают наизусть. Чана упоминают, если интервьюер хочет услышать про output-sensitive.

Коротко

Алгоритм Чана (1996) - output-sensitive алгоритм выпуклой оболочки на плоскости со сложностью O(nlogh)O(n \log h), где hh - число вершин оболочки. Идея: разбить nn точек на группы по mm и в каждой построить локальную оболочку Graham scan за O(mlogm)O(m \log m); затем Jarvis march по группам, на каждом шаге ища касательную бинарным поиском за O(logm)O(\log m). При m=hm = h суммарно - O(nlogh)O(n \log h). Значение hh заранее неизвестно, поэтому mm перебирают по двойной геометрической прогрессии mt=22tm_t = 2^{2^t} с early termination. Альтернатива той же асимптотики - Kirkpatrick–Seidel, но он значительно сложнее в реализации. Чана используют на больших облаках точек с малой оболочкой, в 3D-обобщении и в библиотеках вычислительной геометрии.

Доверьте текст нейросети EssayAI

Открыть EssayAI

Бесплатно, на русском языке и без VPN

Читайте также