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

Локалізація точки за O(logn)O(log n)

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

Коли підходить цей алгоритм?
  • Вам потрібно для багатьох точок визначити, у якій грані планарного розбиття лежить кожна (а не перелічити самі грані)? (якщо потрібно знайти грані графа → Пошук граней планарного графа)
  • Допустима офлайн-обробка: усі точки запитів відомі заздалегідь, щоб запустити замітальну пряму?
  • Якщо розбиття — це один опуклий многокутник, чи не вистачить простішого тесту належності за O(log⁡n)O(\log n)?

Алгоритм​

Спочатку для кожної точки запиту p (x0,y0)p\ (x_0, y_0) ми хочемо знайти таке ребро, що якщо точка належить хоч якомусь ребру, то вона лежить саме на знайденому ребрі; інакше це ребро має перетинати пряму x=x0x = x_0 у деякій єдиній точці (x0,y)(x_0, y), де y<y0y < y_0, причому це yy є максимальним серед усіх таких ребер. Наступне зображення показує обидва випадки.

Image of Goal

Ми розв'яжемо цю задачу офлайн за допомогою алгоритму замітальної прямої. Перебиратимемо x-координати точок запитів і кінців ребер у порядку зростання та підтримуватимемо множину ребер ss. Для кожної x-координати ми заздалегідь додамо деякі події.

Події будуть чотирьох типів: add, remove, vertical, get. Для кожного вертикального ребра (обидва кінці мають однакову x-координату) ми додамо одну подію vertical для відповідної x-координати. Для кожного іншого ребра ми додамо одну подію add для мінімуму x-координат кінців і одну подію remove для максимуму x-координат кінців. Нарешті, для кожної точки запиту ми додамо одну подію get для її x-координати.

Для кожної x-координати ми відсортуємо події за їхніми типами в порядку (vertical, get, remove, add). Наступне зображення показує всі події у відсортованому порядку для кожної x-координати.

Image of Events

Під час процесу замітальної прямої ми підтримуватимемо дві множини. Множину tt для всіх невертикальних ребер і окрему множину vertvert спеціально для вертикальних. Ми очищатимемо множину vertvert на початку обробки кожної x-координати.

Тепер обробімо події для фіксованої x-координати.

  • Якщо ми отримали подію vertical, ми просто вставимо мінімальну y-координату кінців відповідного ребра до vertvert.
  • Якщо ми отримали подію remove або add, ми видалимо відповідне ребро з tt або додамо його до tt.
  • Нарешті, для кожної події get ми маємо перевірити, чи лежить точка на якомусь вертикальному ребрі, виконавши бінарний пошук у vertvert. Якщо точка не лежить на жодному вертикальному ребрі, ми маємо знайти відповідь для цього запиту в tt. Для цього ми знову робимо бінарний пошук. Щоб обробити деякі вироджені випадки (наприклад, у випадку трикутника (0, 0)(0,~0), (0, 2)(0,~2), (1,1)(1, 1), коли ми запитуємо точку (0, 0)(0,~0)), ми маємо відповісти на всі події get ще раз після того, як обробили всі події для цієї x-координати, і обрати кращу з двох відповідей.

Тепер оберімо компаратор для множини tt. Цей компаратор має перевіряти, чи не лежить одне ребро вище за інше для кожної x-координати, яку вони обидва покривають. Припустимо, що ми маємо два ребра (a,b)(a, b) і (c,d)(c, d). Тоді компаратор такий (у псевдокоді):

val=sgn((b−a)×(c−a))+sgn((b−a)×(d−a))val = sgn((b - a)\times(c - a)) + sgn((b - a)\times(d - a))
if val≠0val \neq 0
then return val>0val > 0
val=sgn((d−c)×(a−c))+sgn((d−c)×(b−c))val = sgn((d - c)\times(a - c)) + sgn((d - c)\times(b - c))
return val<0val < 0

Тепер для кожного запиту ми маємо відповідне ребро. Як знайти грань? Якщо ми не змогли знайти ребро, це означає, що точка лежить у зовнішній грані. Якщо точка належить знайденому ребру, грань не єдина. Інакше є два кандидати — грані, які обмежені цим ребром. Як перевірити, котра з них є відповіддю? Зауважимо, що ребро невертикальне. Тоді відповіддю є грань, яка лежить над цим ребром. Знайдімо таку грань для кожного невертикального ребра. Розгляньмо обхід кожної грані проти годинникової стрілки. Якщо під час цього обходу ми збільшили x-координату, проходячи через ребро, то ця грань і є гранню, яку нам потрібно знайти для цього ребра.

Зауваження​

Насправді, за допомогою персистентних дерев цей підхід можна використати, щоб відповідати на запити онлайн.

Реалізація​

Наступний код реалізовано для цілих чисел, але його легко змінити, щоб він працював з числами типу double (змінивши методи порівняння та тип точки). Ця реалізація припускає, що розбиття коректно зберігається всередині DCEL, а зовнішня грань має номер −1-1.
Для кожного запиту повертається пара (1,i)(1, i), якщо точка лежить строго всередині грані з номером ii, і пара (0,i)(0, i), якщо точка лежить на ребрі з номером ii.

typedef long long ll;

bool ge(const ll& a, const ll& b) { return a >= b; }
bool le(const ll& a, const ll& b) { return a <= b; }
bool eq(const ll& a, const ll& b) { return a == b; }
bool gt(const ll& a, const ll& b) { return a > b; }
bool lt(const ll& a, const ll& b) { return a < b; }
int sgn(const ll& x) { return le(x, 0) ? eq(x, 0) ? 0 : -1 : 1; }

struct pt {
ll x, y;
pt() {}
pt(ll _x, ll _y) : x(_x), y(_y) {}
pt operator-(const pt& a) const { return pt(x - a.x, y - a.y); }
ll dot(const pt& a) const { return x * a.x + y * a.y; }
ll dot(const pt& a, const pt& b) const { return (a - *this).dot(b - *this); }
ll cross(const pt& a) const { return x * a.y - y * a.x; }
ll cross(const pt& a, const pt& b) const { return (a - *this).cross(b - *this); }
bool operator==(const pt& a) const { return a.x == x && a.y == y; }
};

struct Edge {
pt l, r;
};

bool edge_cmp(Edge* edge1, Edge* edge2)
{
const pt a = edge1->l, b = edge1->r;
const pt c = edge2->l, d = edge2->r;
int val = sgn(a.cross(b, c)) + sgn(a.cross(b, d));
if (val != 0)
return val > 0;
val = sgn(c.cross(d, a)) + sgn(c.cross(d, b));
return val < 0;
}

enum EventType { DEL = 2, ADD = 3, GET = 1, VERT = 0 };

struct Event {
EventType type;
int pos;
bool operator<(const Event& event) const { return type < event.type; }
};

vector<Edge*> sweepline(vector<Edge*> planar, vector<pt> queries)
{
using pt_type = decltype(pt::x);

// збираємо всі x-координати
auto s =
set<pt_type, std::function<bool(const pt_type&, const pt_type&)>>(lt);
for (pt p : queries)
s.insert(p.x);
for (Edge* e : planar) {
s.insert(e->l.x);
s.insert(e->r.x);
}

// зіставляємо всі x-координати з ідентифікаторами
int cid = 0;
auto id =
map<pt_type, int, std::function<bool(const pt_type&, const pt_type&)>>(
lt);
for (auto x : s)
id[x] = cid++;

// створюємо події
auto t = set<Edge*, decltype(*edge_cmp)>(edge_cmp);
auto vert_cmp = [](const pair<pt_type, int>& l,
const pair<pt_type, int>& r) {
if (!eq(l.first, r.first))
return lt(l.first, r.first);
return l.second < r.second;
};
auto vert = set<pair<pt_type, int>, decltype(vert_cmp)>(vert_cmp);
vector<vector<Event>> events(cid);
for (int i = 0; i < (int)queries.size(); i++) {
int x = id[queries[i].x];
events[x].push_back(Event{GET, i});
}
for (int i = 0; i < (int)planar.size(); i++) {
int lx = id[planar[i]->l.x], rx = id[planar[i]->r.x];
if (lx > rx) {
swap(lx, rx);
swap(planar[i]->l, planar[i]->r);
}
if (lx == rx) {
events[lx].push_back(Event{VERT, i});
} else {
events[lx].push_back(Event{ADD, i});
events[rx].push_back(Event{DEL, i});
}
}

// виконуємо алгоритм замітальної прямої
vector<Edge*> ans(queries.size(), nullptr);
for (int x = 0; x < cid; x++) {
sort(events[x].begin(), events[x].end());
vert.clear();
for (Event event : events[x]) {
if (event.type == DEL) {
t.erase(planar[event.pos]);
}
if (event.type == VERT) {
vert.insert(make_pair(
min(planar[event.pos]->l.y, planar[event.pos]->r.y),
event.pos));
}
if (event.type == ADD) {
t.insert(planar[event.pos]);
}
if (event.type == GET) {
auto jt = vert.upper_bound(
make_pair(queries[event.pos].y, planar.size()));
if (jt != vert.begin()) {
--jt;
int i = jt->second;
if (ge(max(planar[i]->l.y, planar[i]->r.y),
queries[event.pos].y)) {
ans[event.pos] = planar[i];
continue;
}
}
Edge* e = new Edge;
e->l = e->r = queries[event.pos];
auto it = t.upper_bound(e);
if (it != t.begin())
ans[event.pos] = *(--it);
delete e;
}
}

for (Event event : events[x]) {
if (event.type != GET)
continue;
if (ans[event.pos] != nullptr &&
eq(ans[event.pos]->l.x, ans[event.pos]->r.x))
continue;

Edge* e = new Edge;
e->l = e->r = queries[event.pos];
auto it = t.upper_bound(e);
delete e;
if (it == t.begin())
e = nullptr;
else
e = *(--it);
if (ans[event.pos] == nullptr) {
ans[event.pos] = e;
continue;
}
if (e == nullptr)
continue;
if (e == ans[event.pos])
continue;
if (id[ans[event.pos]->r.x] == x) {
if (id[e->l.x] == x) {
if (gt(e->l.y, ans[event.pos]->r.y))
ans[event.pos] = e;
}
} else {
ans[event.pos] = e;
}
}
}
return ans;
}

struct DCEL {
struct Edge {
pt origin;
Edge* nxt = nullptr;
Edge* twin = nullptr;
int face;
};
vector<Edge*> body;
};

vector<pair<int, int>> point_location(DCEL planar, vector<pt> queries)
{
vector<pair<int, int>> ans(queries.size());
vector<Edge*> planar2;
map<intptr_t, int> pos;
map<intptr_t, int> added_on;
int n = planar.body.size();
for (int i = 0; i < n; i++) {
if (planar.body[i]->face > planar.body[i]->twin->face)
continue;
Edge* e = new Edge;
e->l = planar.body[i]->origin;
e->r = planar.body[i]->twin->origin;
added_on[(intptr_t)e] = i;
pos[(intptr_t)e] =
lt(planar.body[i]->origin.x, planar.body[i]->twin->origin.x)
? planar.body[i]->face
: planar.body[i]->twin->face;
planar2.push_back(e);
}
auto res = sweepline(planar2, queries);
for (int i = 0; i < (int)queries.size(); i++) {
if (res[i] == nullptr) {
ans[i] = make_pair(1, -1);
continue;
}
pt p = queries[i];
pt l = res[i]->l, r = res[i]->r;
if (eq(p.cross(l, r), 0) && le(p.dot(l, r), 0)) {
ans[i] = make_pair(0, added_on[(intptr_t)res[i]]);
continue;
}
ans[i] = make_pair(1, pos[(intptr_t)res[i]]);
}
for (auto e : planar2)
delete e;
return ans;
}

Задачі​