Перейти до основного вмісту

Перевірка належності точки опуклому многокутнику за O(log⁡N)O(\log N)

Розглянемо таку задачу: задано опуклий многокутник із цілочисловими вершинами та багато запитів. Кожен запит — це точка, для якої треба визначити, лежить вона всередині многокутника, на його межі чи ні. Припустимо, що вершини многокутника впорядковані проти годинникової стрілки. Ми відповідатимемо на кожен запит за O(log⁡n)O(\log n) онлайн.

Коли підходить цей алгоритм?
  • Многокутник, у якому ви перевіряєте належність, є опуклим? (якщо він довільний/неопуклий → потрібен загальний тест belonging, напр. через перетин променя; цей O(log⁡n)O(\log n)-метод незастосовний)
  • Запитів багато, тож варто один раз підготувати дані й відповідати на кожен запит за O(log⁡n)O(\log n), а не лінійним обходом?
  • Вершини задано в порядку обходу проти годинникової стрілки (або їх можна так упорядкувати) для побудови бінарного пошуку за полярним кутом?

Алгоритм​

Виберемо точку з найменшою x-координатою. Якщо таких декілька, виберемо ту, що має найменшу y-координату. Позначимо її як p0p_0. Тоді всі інші точки p1,…,pnp_1,\dots,p_n многокутника впорядковані за полярним кутом відносно вибраної точки (бо вершини многокутника впорядковані проти годинникової стрілки).

Якщо точка належить многокутнику, то вона належить деякому трикутнику p0,pi,pi+1p_0, p_i, p_{i + 1} (можливо, більш ніж одному, якщо вона лежить на межі трикутників). Розглянемо трикутник p0,pi,pi+1p_0, p_i, p_{i + 1} такий, що pp належить цьому трикутнику і ii є максимальним серед усіх таких трикутників.

Є один особливий випадок. pp лежить на відрізку (p0,pn)(p_0, p_n). Цей випадок ми перевіримо окремо. Інакше всі точки pjp_j з j≤ij \le i лежать проти годинникової стрілки від pp відносно p0p_0, а всі інші точки — ні. Це означає, що ми можемо застосувати бінарний пошук для точки pip_i такої, що pip_i не лежить проти годинникової стрілки від pp відносно p0p_0, а ii є максимальним серед усіх таких точок. А потім ми перевіряємо, чи точка справді лежить у визначеному трикутнику.

Знак виразу (a−c)×(b−c)(a - c) \times (b - c) підкаже нам, чи лежить точка aa за годинниковою стрілкою, чи проти годинникової стрілки від точки bb відносно точки cc. Якщо (a−c)×(b−c)>0(a - c) \times (b - c) > 0, то точка aa лежить праворуч від вектора, що йде з cc до bb, тобто за годинниковою стрілкою від bb відносно cc. А якщо (a−c)×(b−c)<0(a - c) \times (b - c) < 0, то точка лежить ліворуч, тобто проти годинникової стрілки. А якщо рівне нулю — то вона лежить точно на прямій між точками bb і cc.

Повернімося до алгоритму. Розглянемо точку запиту pp. Спочатку ми маємо перевірити, чи лежить точка між p1p_1 і pnp_n. Інакше ми вже знаємо, що вона не може належати многокутнику. Це можна зробити, перевіривши, чи векторний добуток (p1−p0)×(p−p0)(p_1 - p_0)\times(p - p_0) дорівнює нулю або має той самий знак, що й (p1−p0)×(pn−p0)(p_1 - p_0)\times(p_n - p_0), і чи (pn−p0)×(p−p0)(p_n - p_0)\times(p - p_0) дорівнює нулю або має той самий знак, що й (pn−p0)×(p1−p0)(p_n - p_0)\times(p_1 - p_0). Потім ми обробляємо особливий випадок, коли pp лежить на прямій (p0,p1)(p_0, p_1). А далі ми можемо бінарним пошуком знайти останню точку з p1,…pnp_1,\dots p_n, яка не лежить проти годинникової стрілки від pp відносно p0p_0. Для окремої точки pip_i цю умову можна перевірити, переконавшись, що (pi−p0)×(p−p0)≤0(p_i - p_0)\times(p - p_0) \le 0. Після того, як ми знайшли таку точку pip_i, треба перевірити, чи лежить pp всередині трикутника p0,pi,pi+1p_0, p_i, p_{i + 1}. Щоб перевірити, чи належить вона трикутнику, ми можемо просто переконатися, що ∣(pi−p0)×(pi+1−p0)∣=∣(p0−p)×(pi−p)∣+∣(pi−p)×(pi+1−p)∣+∣(pi+1−p)×(p0−p)∣|(p_i - p_0)\times(p_{i + 1} - p_0)| = |(p_0 - p)\times(p_i - p)| + |(p_i - p)\times(p_{i + 1} - p)| + |(p_{i + 1} - p)\times(p_0 - p)|. Це перевіряє, чи площа трикутника p0,pi,pi+1p_0, p_i, p_{i+1} має точно той самий розмір, що й сума площ трикутника p0,pi,pp_0, p_i, p, трикутника p0,p,pi+1p_0, p, p_{i+1} та трикутника pi,pi+1,pp_i, p_{i+1}, p. Якщо pp лежить зовні, то сума цих трьох трикутників буде більшою за площу трикутника. Якщо ж вона лежить усередині, то буде рівною.

Реалізація​

Функція prepare гарантує, що лексикографічно найменша точка (найменше значення x, а при рівності — найменше значення y) буде p0p_0, та обчислює вектори pi−p0p_i - p_0. Після цього функція pointInConvexPolygon обчислює результат запиту. Ми додатково запам'ятовуємо точку p0p_0 і зсуваємо на неї всі точки запитів, щоб обчислювати правильну відстань, адже вектори не мають початкової точки. Зсуваючи точки запитів, ми можемо вважати, що всі вектори починаються в початку координат (0,0)(0, 0), і спростити обчислення відстаней та довжин.

struct pt {
long long x, y;
pt() {}
pt(long long _x, long long _y) : x(_x), y(_y) {}
pt operator+(const pt &p) const { return pt(x + p.x, y + p.y); }
pt operator-(const pt &p) const { return pt(x - p.x, y - p.y); }
long long cross(const pt &p) const { return x * p.y - y * p.x; }
long long dot(const pt &p) const { return x * p.x + y * p.y; }
long long cross(const pt &a, const pt &b) const { return (a - *this).cross(b - *this); }
long long dot(const pt &a, const pt &b) const { return (a - *this).dot(b - *this); }
long long sqrLen() const { return this->dot(*this); }
};

bool lexComp(const pt &l, const pt &r) {
return l.x < r.x || (l.x == r.x && l.y < r.y);
}

int sgn(long long val) { return val > 0 ? 1 : (val == 0 ? 0 : -1); }

vector<pt> seq;
pt translation;
int n;

bool pointInTriangle(pt a, pt b, pt c, pt point) {
long long s1 = abs(a.cross(b, c));
long long s2 = abs(point.cross(a, b)) + abs(point.cross(b, c)) + abs(point.cross(c, a));
return s1 == s2;
}

void prepare(vector<pt> &points) {
n = points.size();
int pos = 0;
for (int i = 1; i < n; i++) {
if (lexComp(points[i], points[pos]))
pos = i;
}
rotate(points.begin(), points.begin() + pos, points.end());

n--;
seq.resize(n);
for (int i = 0; i < n; i++)
seq[i] = points[i + 1] - points[0];
translation = points[0];
}

bool pointInConvexPolygon(pt point) {
point = point - translation;
if (seq[0].cross(point) != 0 &&
sgn(seq[0].cross(point)) != sgn(seq[0].cross(seq[n - 1])))
return false;
if (seq[n - 1].cross(point) != 0 &&
sgn(seq[n - 1].cross(point)) != sgn(seq[n - 1].cross(seq[0])))
return false;

if (seq[0].cross(point) == 0)
return seq[0].sqrLen() >= point.sqrLen();

int l = 0, r = n - 1;
while (r - l > 1) {
int mid = (l + r) / 2;
int pos = mid;
if (seq[pos].cross(point) >= 0)
l = mid;
else
r = mid;
}
int pos = l;
return pointInTriangle(seq[pos], seq[pos + 1], pt(0, 0), point);
}

Задачі​