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

Алгоритм Грэхема: строим выпуклую оболочку точек

20 марта 2026Время чтения: 9 минут
#алгоритм Грэхема#выпуклая оболочка#вычислительная геометрия#поворот по часовой#Graham scan
Алгоритм Грэхема: строим выпуклую оболочку точек

Алгоритм Грэхема (Graham scan, опубликован Рональдом Грэхемом в 1972 году) - классический способ построить выпуклую оболочку конечного набора точек на плоскости за время O(nlogn)O(n \log n). Идея простая и красивая: выбрать заведомо граничную опорную точку, отсортировать остальные по полярному углу относительно неё и обойти их одним проходом, поддерживая стек кандидатов в вершины оболочки и каждый раз проверяя направление поворота. Это первая историческая реализация выпуклой оболочки с оптимальной по нижней оценке асимптотикой и до сих пор - учебный эталон при разборе вычислительной геометрии.

Постановка задачи

Дано множество S={p1,p2,,pn}S = \{p_1, p_2, \ldots, p_n\} из nn точек на плоскости. Выпуклая оболочка conv(S)\mathrm{conv}(S) - наименьшее выпуклое множество, содержащее все точки SS. Геометрически это многоугольник, чьи вершины - подмножество SS, такой что все остальные точки лежат внутри или на границе этого многоугольника. Задача - выдать вершины оболочки в порядке обхода (обычно против часовой стрелки), не пропустив ни одной граничной точки и не включив ни одной внутренней.

Нижняя оценка задачи - Ω(nlogn)\Omega(n \log n): построение выпуклой оболочки сводится к сортировке (если расположить nn чисел xix_i как точки (xi,xi2)(x_i, x_i^2) на параболе, их оболочка возвращает xix_i в отсортированном порядке). Значит, алгоритмы быстрее O(nlogn)O(n \log n) в общем случае невозможны без дополнительных предположений на вход.

Идея алгоритма Грэхема

Алгоритм Грэхема выбирает опорную точку p0p_0 - ту, у которой минимальная координата yy (а при равных yy - минимальная xx). Такая точка заведомо принадлежит выпуклой оболочке: ниже неё точек нет, значит, она лежит на нижней границе. Затем все остальные точки сортируются по полярному углу относительно p0p_0 - то есть по углу луча p0pip_0 p_i с положительным направлением оси xx. После сортировки получается список p0,p1,p2,,pn1p_0, p_1, p_2, \ldots, p_{n-1} - обход начинается из p0p_0 и идёт против часовой стрелки.

Дальше включается стек H\mathcal{H} кандидатов. В стек кладутся p0,p1,p2p_0, p_1, p_2. Для каждой следующей точки pkp_k проверяется ориентация тройки (предпоследняя вершина стека, верхушка стека, pkp_k). Если поворот левый (против часовой) - точка добавляется в стек. Если поворот правый или нулевой (по часовой или коллинеарный) - верхушка стека выкидывается, и проверка повторяется заново с новой верхушкой. После прохода всех точек стек содержит вершины выпуклой оболочки в порядке обхода.

Проверка поворота через cross product

Ключевая операция алгоритма - определение, куда повернула ломаная в очередной точке. Для трёх точек a=(ax,ay)a = (a_x, a_y), b=(bx,by)b = (b_x, b_y), c=(cx,cy)c = (c_x, c_y) вычисляется псевдоскалярное произведение векторов ab\vec{ab} и ac\vec{ac}:

cross(a,b,c)=(bxax)(cyay)(byay)(cxax).\text{cross}(a, b, c) = (b_x - a_x)(c_y - a_y) - (b_y - a_y)(c_x - a_x).

Знак определяет ориентацию:

  • cross>0\text{cross} > 0 - поворот против часовой (левый), точка cc лежит слева от прямой abab.
  • cross<0\text{cross} < 0 - поворот по часовой (правый), точка cc справа.
  • cross=0\text{cross} = 0 - три точки коллинеарны.

Операция целочисленная, если все входные координаты целые, - никаких тригонометрий, никаких накапливающихся ошибок плавающей точки. Это одна из причин, почему алгоритм Грэхема устойчив численно и удобен для соревновательного программирования.

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

Утверждение: после прохода стек содержит ровно вершины выпуклой оболочки в порядке против часовой стрелки. Доказательство идёт индукцией по числу обработанных точек.

База: точки p0,p1p_0, p_1 - два соседа по углу, p1p_1 - следующая граничная точка против часовой. Шаг: пусть после k1k - 1 шагов на стеке - корректная выпуклая оболочка точек {p0,,pk1}\{p_0, \ldots, p_{k-1}\} в порядке обхода. При добавлении pkp_k алгоритм откатывает с вершины все точки, образующие правый или нулевой поворот с pkp_k - то есть все точки, оказавшиеся внутри ломаной ,pk\ldots, p_k. Любая такая точка не может быть вершиной финальной оболочки. После откатов сверху лежит точка, от которой переход к pkp_k - левый поворот; новый кусок ломаной остаётся выпуклым.

Сортировка по полярному углу гарантирует, что точки добавляются в обход именно против часовой стрелки; опорная точка p0p_0 с минимальным yy гарантирует, что все полярные углы лежат в диапазоне [0,π][0, \pi] и сортировка корректно их упорядочивает.

Сложность O(nlogn)O(n\log n)

Затраты по шагам:

  1. Поиск опорной точки p0p_0 - O(n)O(n) одним проходом.
  2. Сортировка n1n - 1 точек по полярному углу - O(nlogn)O(n \log n) через любой стандартный log\log-сорт (сравнения через cross product, не через atan2).
  3. Обход и работа со стеком - амортизированно O(n)O(n): каждая точка попадает в стек ровно один раз и выкидывается из него не более одного раза. Каждая итерация внутреннего while уменьшает размер стека, общее число выкидываний не превышает nn.

Итого O(nlogn)O(n \log n), причём константа доминирует на сортировке. Это оптимально по нижней оценке.

Сравнение с другими алгоритмами

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

Andrew's monotone chain (1979) - модификация Грэхема: сортирует по xx (а не по углу), строит верхнюю и нижнюю полуоболочки и склеивает. Тоже O(nlogn)O(n \log n), но численно стабильнее и кодируется короче - в продакшене на C++ и Python чаще используют именно его.

QuickHull (1977) - рекурсивный аналог быстрой сортировки: находит крайние точки по xx, делит остальные относительно хорды и обрабатывает рекурсивно. Среднее O(nlogn)O(n \log n), худший случай O(n2)O(n^2). Эффективен на разбросанных данных, плох на точках, плотно лежащих на окружности.

Chan's algorithm (1996) - output-sensitive O(nlogh)O(n \log h). Объединяет Грэхема на подмножествах с шагом Jarvis по ним. Лучше всех при заранее неизвестном маленьком hh, но сложнее в реализации.

Подводные камни

Коллинеарные точки. Когда несколько точек лежат на одном луче из p0p_0, их полярный угол одинаков и сортировка даёт неоднозначный порядок. Починка - при равных углах сортировать по расстоянию от p0p_0. Если нужна строгая оболочка (только настоящие вершины), при cross=0\text{cross} = 0 откатывают предыдущую точку.

Совпадающие точки. Дубликаты убирают заранее, иначе сортировка выдаёт ноль для пары и обход уходит в цикл выкидываний.

Менее трёх точек. Алгоритм требует n3n \geq 3; при n=1n = 1 оболочка - сама точка, при n=2n = 2 - отрезок. Эти случаи обрабатывают отдельной веткой.

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

Где применяется

  • Компьютерная графика. Bounding shape для коллизий, упрощённая граница спрайтов и моделей - компактное «грубое» приближение формы, по которому быстро проверяются пересечения.
  • GIS и картография. Минимальная выпуклая область наблюдений: ареал вида, зона покрытия датчиков, граница облака GPS-отметок.
  • Кластеризация в ML. Граница кластера через convex hull точек кластера - для визуализации и для проверки «попадает ли новая точка внутрь».
  • SVM и оптимизация. Выпуклые оболочки классов - стандартный способ анализа линейной разделимости: если оболочки не пересекаются, классы разделимы.
  • Физические движки. Convex hull triangle mesh как broad-phase bounding до точного теста коллизий.
  • Олимпиадные задачи. Диаметр оболочки через rotating calipers, минимальный охватывающий прямоугольник, ближайшая пара точек.

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

  • Считают полярный угол через atan2 и сравнивают float. Числовая ошибка превращает соседние углы в произвольный порядок. Правильно - сравнивать через cross product двух векторов: cross(p0,pi,pj)>0\text{cross}(p_0, p_i, p_j) > 0 означает «pjp_j левее, угол меньше».
  • Не обрабатывают коллинеарные точки. Алгоритм без явной ветки на cross=0\text{cross} = 0 может оставить точку в оболочке или выкинуть, как повезёт. Решите заранее: нужна строгая оболочка или допускается коллинеарность на стороне.
  • Берут случайную опорную точку. Опорной должна быть гарантированно граничная точка (самая нижняя/левая), иначе сортировка по полярному углу не покрывает всю окружность 2π2\pi.
  • Сравнивают cross с нулём как float. Если координаты целые - считайте в int64, иначе при больших значениях получите переполнение и неправильный знак.
  • Забывают сжать дубликаты. Несколько одинаковых точек ломают сортировку и порождают зацикливание на выкидываниях.

FAQ

Чем алгоритм Грэхема отличается от Andrew's monotone chain? Идеи одинаковые - отсортировать точки и собрать оболочку линейным проходом со стеком. Различие в порядке сортировки. Грэхем - по полярному углу относительно опорной точки. Andrew - по координате xx, строит верхнюю и нижнюю полуоболочки и склеивает. У Andrew проще обработка коллинеарных случаев, у Грэхема приятнее интуиция.

Когда выгоднее использовать Jarvis march вместо Грэхема? Когда заранее известно, что число вершин оболочки hh мало, скажем h=O(logn)h = O(\log n). Jarvis работает за O(nh)O(nh), и при таком hh это O(nlogn)O(n \log n) - как Грэхем, но с меньшей константой и без сортировки (что удобно при потоковых данных). Если же оболочка может содержать почти все точки - Jarvis деградирует до O(n2)O(n^2), и Грэхем выигрывает.

Можно ли построить трёхмерную выпуклую оболочку алгоритмом Грэхема? Прямо - нет. Полярный угол на сфере и стек кандидатов уже не задают однозначного обхода. Для 3D используют QuickHull, incremental hull или divide and conquer - асимптотика лучших алгоритмов O(nlogn)O(n \log n) для случайных распределений и O(n2)O(n^2) в худшем случае на специально подобранных входах.

Коротко

Алгоритм Грэхема (1972) строит выпуклую оболочку nn точек на плоскости за O(nlogn)O(n \log n). Шаги: выбор опорной точки с минимальным yy, сортировка остальных по полярному углу, проход стеком с проверкой поворота через cross product. При левом повороте точка добавляется, при правом - верхушка стека выкидывается. Сложность доминирует сортировкой; обход со стеком - амортизированно O(n)O(n). Сравнимые альтернативы - Jarvis march (O(nh)O(nh)), Andrew's monotone chain (O(nlogn)O(n \log n), проще), QuickHull (среднее O(nlogn)O(n \log n)). Применяется в компьютерной графике, GIS, кластеризации, физических движках и олимпиадных задачах.

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

Открыть EssayAI

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

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