Вычислительная геометрия: Алгоритм Грэхема, Алгоритм Джарвиса, Проверка принадлежности точки многоугольнику
Алгоритм Грэхема
Выпуклая оболочка множества точек — наименьший выпуклый многоугольник, содержащий все эти точки. Алгоритм Грэхема строит её за $O(n\log n)$; основную стоимость составляет сортировка.
Опорная точка $p_0$ — самая нижняя из точек, а при равенстве $y$ — самая левая. Она заведомо лежит на оболочке. Остальные точки сортируем по полярному углу относительно $p_0$. Все углы лежат в одной полуплоскости, поэтому порядок задаёт знак векторного произведения: точка $a$ идёт раньше $b$, если $(a-p_0)\times(b-p_0)$ больше нуля. Точки на одном луче сортируем по расстоянию до $p_0$.
Затем проходим отсортированные точки и держим стек вершин. Перед добавлением новой точки $p$ снимаем с вершины стека точки, которые не дают строгого левого поворота: пока для двух последних вершин $a$ и $b$ выполнено $(b-a)\times(p-a)\le 0$, вершину $b$ удаляем. Так убираются вогнутости и точки на сторонах оболочки.
Все вычисления целочисленные: векторное произведение состоит только из сложений и умножений, без углов и корней. Повторы точек удаляются заранее.
Реализация на Python
# Алгоритм Грэхема, O(n log n)
from functools import cmp_to_key
def cross(o, a, b):
"""Векторное произведение (a - o) x (b - o): больше нуля, если поворот влево."""
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
def dist2(a, b):
return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2
def graham(points):
"""Выпуклая оболочка против часовой стрелки, начиная с нижней левой точки."""
# Сортируем по (y, x) и удаляем повторы: первая точка — опорная
pts = sorted(set(points), key=lambda p: (p[1], p[0]))
if len(pts) <= 2:
return pts
pivot, rest = pts[0], pts[1:]
def by_angle(a, b):
# a раньше b, если поворот pivot -> a -> b влево; при равном угле ближняя раньше
turn = cross(pivot, a, b)
if turn != 0:
return -turn
return dist2(pivot, a) - dist2(pivot, b)
rest.sort(key=cmp_to_key(by_angle))
hull = [pivot]
for p in rest:
# Удаляем вершины, которые не дают строгого левого поворота
while len(hull) >= 2 and cross(hull[-2], hull[-1], p) <= 0:
hull.pop()
hull.append(p)
return hull
if __name__ == "__main__":
print(graham([(0, 0), (4, 0), (4, 4), (0, 4), (2, 2), (2, 0), (1, 3), (0, 0)]))
Реализация на C++
// Алгоритм Грэхема, O(n log n)
#include <algorithm>
#include <cstdio>
#include <vector>
struct Point {
long long x;
long long y;
};
bool operator==(const Point& a, const Point& b) { return a.x == b.x && a.y == b.y; }
// Векторное произведение (a - o) x (b - o): больше нуля, если поворот влево
long long cross(const Point& o, const Point& a, const Point& b) {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
long long dist2(const Point& a, const Point& b) {
return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
}
// Выпуклая оболочка против часовой стрелки, начиная с нижней левой точки
std::vector<Point> graham(std::vector<Point> pts) {
// Сортируем по (y, x) и удаляем повторы: первая точка — опорная
std::sort(pts.begin(), pts.end(), [](const Point& a, const Point& b) {
return a.y != b.y ? a.y < b.y : a.x < b.x;
});
pts.erase(std::unique(pts.begin(), pts.end()), pts.end());
if (pts.size() <= 2) return pts;
// Остальные точки — по полярному углу вокруг опорной; при равном угле ближняя раньше
const Point pivot = pts[0];
std::sort(pts.begin() + 1, pts.end(), [&pivot](const Point& a, const Point& b) {
long long turn = cross(pivot, a, b);
return turn != 0 ? turn > 0 : dist2(pivot, a) < dist2(pivot, b);
});
std::vector<Point> hull;
for (const Point& p : pts) {
// Удаляем вершины, которые не дают строгого левого поворота
while (hull.size() >= 2 && cross(hull[hull.size() - 2], hull.back(), p) <= 0) {
hull.pop_back();
}
hull.push_back(p);
}
return hull;
}
int main() {
const std::vector<Point> pts = {{0, 0}, {4, 0}, {4, 4}, {0, 4}, {2, 2}, {2, 0}, {1, 3}, {0, 0}};
for (const Point& p : graham(pts)) {
std::printf("(%lld, %lld)\n", p.x, p.y);
}
return 0;
}
Реализация на Rust
// Алгоритм Грэхема, O(n log n)
type Point = (i64, i64);
// Векторное произведение (a - o) x (b - o): больше нуля, если поворот влево
fn cross(o: Point, a: Point, b: Point) -> i64 {
(a.0 - o.0) * (b.1 - o.1) - (a.1 - o.1) * (b.0 - o.0)
}
fn dist2(a: Point, b: Point) -> i64 {
(a.0 - b.0).pow(2) + (a.1 - b.1).pow(2)
}
// Выпуклая оболочка против часовой стрелки, начиная с нижней левой точки
pub fn graham(mut pts: Vec<Point>) -> Vec<Point> {
// Сортируем по (y, x) и удаляем повторы: первая точка — опорная
pts.sort_by_key(|p| (p.1, p.0));
pts.dedup();
if pts.len() <= 2 {
return pts;
}
// Остальные точки — по полярному углу вокруг опорной; при равном угле ближняя раньше
let pivot = pts[0];
pts[1..].sort_by(|&a, &b| {
cross(pivot, a, b).cmp(&0).reverse().then(dist2(pivot, a).cmp(&dist2(pivot, b)))
});
let mut hull: Vec<Point> = Vec::new();
for p in pts {
// Удаляем вершины, которые не дают строгого левого поворота
while hull.len() >= 2 && cross(hull[hull.len() - 2], hull[hull.len() - 1], p) <= 0 {
hull.pop();
}
hull.push(p);
}
hull
}
fn main() {
let pts = vec![(0, 0), (4, 0), (4, 4), (0, 4), (2, 2), (2, 0), (1, 3), (0, 0)];
for p in graham(pts) {
println!("({}, {})", p.0, p.1);
}
}
Алгоритм Джарвиса («заворачивание подарка»)
Алгоритм Джарвиса строит ту же оболочку иначе: как будто обворачиваем точки верёвкой. Начинаем с самой левой точки $p_0$, она лежит на оболочке, и на каждом шаге находим следующую вершину.
Пусть $c$ — текущая вершина. Кандидат $n$ — такая точка, что все остальные точки лежат слева от направленного отрезка $c \to n$. Чтобы его найти, просматриваем все точки $p$: если $p$ лежит правее луча $c \to n$, она становится новым кандидатом. Если $p$ лежит на том же луче, берём более дальнюю точку, чтобы не класть в ответ точки, лежащие на стороне.
Каждый шаг занимает $O(n)$, а вершин в оболочке $h$, поэтому весь алгоритм работает за $O(n \cdot h)$. При малом $h$ он быстрее алгоритма Грэхема, а в худшем случае, когда все точки на оболочке, даёт $O(n^2)$.
Обход заканчивается, когда вернулись в $p_0$. Вычисления целочисленные, как и у алгоритма Грэхема: нужен только знак векторного произведения.
Реализация на Python
# Алгоритм Джарвиса («заворачивание подарка»), O(n·h)
def cross(o, a, b):
"""Векторное произведение (a - o) x (b - o): больше нуля, если поворот влево."""
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
def dist2(a, b):
return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2
def jarvis(points):
"""Выпуклая оболочка против часовой стрелки, начиная с самой левой точки."""
pts = sorted(set(points))
if len(pts) <= 2:
return pts
start = pts[0]
hull = []
current = start
while True:
hull.append(current)
candidate = next(p for p in pts if p != current)
for p in pts:
turn = cross(current, candidate, p)
# p правее луча current -> candidate, или на нём, но дальше: берём p
if turn < 0 or (turn == 0 and dist2(current, p) > dist2(current, candidate)):
candidate = p
current = candidate
if current == start:
return hull
if __name__ == "__main__":
print(jarvis([(0, 0), (4, 0), (4, 4), (0, 4), (2, 2), (2, 0), (1, 3), (0, 0)]))
Реализация на C++
// Алгоритм Джарвиса («заворачивание подарка»), O(n·h)
#include <algorithm>
#include <cstdio>
#include <vector>
struct Point {
long long x;
long long y;
};
bool operator==(const Point& a, const Point& b) { return a.x == b.x && a.y == b.y; }
bool operator<(const Point& a, const Point& b) { return a.x != b.x ? a.x < b.x : a.y < b.y; }
// Векторное произведение (a - o) x (b - o): больше нуля, если поворот влево
long long cross(const Point& o, const Point& a, const Point& b) {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
long long dist2(const Point& a, const Point& b) {
return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
}
// Выпуклая оболочка против часовой стрелки, начиная с самой левой точки
std::vector<Point> jarvis(std::vector<Point> pts) {
std::sort(pts.begin(), pts.end()); // по (x, y)
pts.erase(std::unique(pts.begin(), pts.end()), pts.end());
if (pts.size() <= 2) return pts;
const Point start = pts[0];
std::vector<Point> hull;
Point current = start;
do {
hull.push_back(current);
Point candidate = pts[0] == current ? pts[1] : pts[0];
for (const Point& p : pts) {
long long turn = cross(current, candidate, p);
// p правее луча current -> candidate или лежит на нём, но дальше: берём p
if (turn < 0 || (turn == 0 && dist2(current, p) > dist2(current, candidate))) {
candidate = p;
}
}
current = candidate;
} while (!(current == start));
return hull;
}
int main() {
const std::vector<Point> pts = {{0, 0}, {4, 0}, {4, 4}, {0, 4}, {2, 2}, {2, 0}, {1, 3}, {0, 0}};
for (const Point& p : jarvis(pts)) {
std::printf("(%lld, %lld)\n", p.x, p.y);
}
return 0;
}
Реализация на Rust
// Алгоритм Джарвиса («заворачивание подарка»), O(n·h)
type Point = (i64, i64);
// Векторное произведение (a - o) x (b - o): больше нуля, если поворот влево
fn cross(o: Point, a: Point, b: Point) -> i64 {
(a.0 - o.0) * (b.1 - o.1) - (a.1 - o.1) * (b.0 - o.0)
}
fn dist2(a: Point, b: Point) -> i64 {
(a.0 - b.0).pow(2) + (a.1 - b.1).pow(2)
}
// Выпуклая оболочка против часовой стрелки, начиная с самой левой точки
pub fn jarvis(mut pts: Vec<Point>) -> Vec<Point> {
pts.sort();
pts.dedup();
if pts.len() <= 2 {
return pts;
}
let start = pts[0];
let mut hull = Vec::new();
let mut current = start;
loop {
hull.push(current);
let mut candidate = pts.iter().copied().find(|&p| p != current).unwrap();
for &p in &pts {
let turn = cross(current, candidate, p);
// p правее луча current -> candidate или лежит на нём, но дальше: берём p
if turn < 0 || (turn == 0 && dist2(current, p) > dist2(current, candidate)) {
candidate = p;
}
}
current = candidate;
if current == start {
return hull;
}
}
}
fn main() {
let pts = vec![(0, 0), (4, 0), (4, 4), (0, 4), (2, 2), (2, 0), (1, 3), (0, 0)];
for p in jarvis(pts) {
println!("({}, {})", p.0, p.1);
}
}
Принадлежность точки многоугольнику (правило луча)
Пусть простой многоугольник задан списком вершин в порядке обхода. Проведём из точки $p$ луч вправо, вдоль горизонтали, и посчитаем, сколько раз он пересекает рёбра. Нечётное число пересечений означает, что точка внутри, чётное — что снаружи.
Вершины, лежащие ровно на высоте $p_y$, нельзя считать дважды, поэтому используем полуинтервал: ребро $(a, b)$ учитываем, только если его концы лежат по разные стороны от горизонтали, то есть $(a_y > p_y) \neq (b_y > p_y)$. Горизонтальные рёбра при этом пропускаются автоматически.
Лежит ли точка пересечения правее $p$, проверяем без деления: это так тогда и только тогда, когда $(b-a)\times(p-a)$ и $b_y-a_y$ имеют один знак. Поэтому весь алгоритм целочисленный и работает за $O(n)$: один проход по рёбрам.
Точки на самой границе многоугольника этот алгоритм отдельно не выделяет, и результат для них не гарантирован. В демонстрации таких точек нет.
Реализация на Python
# Принадлежность точки многоугольнику (правило луча), O(n)
def cross(a, b, p):
"""Векторное произведение (b - a) x (p - a)."""
return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])
def point_in_polygon(polygon, point):
"""Луч из точки вправо: нечётное число пересечений с рёбрами означает «внутри»."""
inside = False
for a, b in zip(polygon, polygon[1:] + polygon[:1]):
# Ребро пересекает луч, только если его концы по разные стороны от горизонтали
if (a[1] > point[1]) == (b[1] > point[1]):
continue
# Точка пересечения правее point, когда cross и dy одного знака
if (cross(a, b, point) > 0) == (b[1] > a[1]):
inside = not inside
return inside
if __name__ == "__main__":
poly = [(0, 0), (6, 0), (6, 6), (3, 2), (0, 6)]
for point in [(3, 1), (3, 4), (1, 4), (5, 5)]:
print(point, point_in_polygon(poly, point))
Реализация на C++
// Принадлежность точки многоугольнику (правило луча), O(n)
#include <cstdio>
#include <vector>
struct Point {
long long x;
long long y;
};
// Векторное произведение (b - a) x (p - a)
long long cross(const Point& a, const Point& b, const Point& p) {
return (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
}
// Луч из точки вправо: нечётное число пересечений с рёбрами означает «внутри»
bool point_in_polygon(const std::vector<Point>& polygon, const Point& point) {
bool inside = false;
const size_t n = polygon.size();
for (size_t i = 0; i < n; ++i) {
const Point& a = polygon[i];
const Point& b = polygon[(i + 1) % n];
// Ребро пересекает луч, только если его концы по разные стороны от горизонтали
if ((a.y > point.y) == (b.y > point.y)) continue;
// Точка пересечения правее point, когда cross и dy одного знака
if ((cross(a, b, point) > 0) == (b.y > a.y)) inside = !inside;
}
return inside;
}
int main() {
const std::vector<Point> poly = {{0, 0}, {6, 0}, {6, 6}, {3, 2}, {0, 6}};
const std::vector<Point> tests = {{3, 1}, {3, 4}, {1, 4}, {5, 5}};
for (const Point& p : tests) {
std::printf("(%lld, %lld) %d\n", p.x, p.y, point_in_polygon(poly, p) ? 1 : 0);
}
return 0;
}
Реализация на Rust
// Принадлежность точки многоугольнику (правило луча), O(n)
type Point = (i64, i64);
// Векторное произведение (b - a) x (p - a)
fn cross(a: Point, b: Point, p: Point) -> i64 {
(b.0 - a.0) * (p.1 - a.1) - (b.1 - a.1) * (p.0 - a.0)
}
// Луч из точки вправо: нечётное число пересечений с рёбрами означает «внутри»
pub fn point_in_polygon(polygon: &[Point], point: Point) -> bool {
let mut inside = false;
for (&a, &b) in polygon.iter().zip(polygon.iter().cycle().skip(1)) {
// Ребро пересекает луч, только если его концы по разные стороны от горизонтали
if (a.1 > point.1) == (b.1 > point.1) {
continue;
}
// Точка пересечения правее point, когда cross и dy одного знака
if (cross(a, b, point) > 0) == (b.1 > a.1) {
inside = !inside;
}
}
inside
}
fn main() {
let poly = [(0, 0), (6, 0), (6, 6), (3, 2), (0, 6)];
for point in [(3, 1), (3, 4), (1, 4), (5, 5)] {
println!("{:?} {}", point, point_in_polygon(&poly, point));
}
}