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

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

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

Коли підходить цей алгоритм?
  • Вам потрібно для багатьох точок визначити, у якій грані планарного розбиття лежить кожна (а не перелічити самі грані)? (якщо потрібно знайти грані графа → Пошук граней планарного графа)
  • Допустима офлайн-обробка: усі точки запитів відомі заздалегідь, щоб запустити замітальну пряму?
  • Якщо розбиття — це один опуклий многокутник, чи не вистачить простішого тесту належності за O(logn)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((ba)×(ca))+sgn((ba)×(da))val = sgn((b - a)\times(c - a)) + sgn((b - a)\times(d - a))
if val0val \neq 0
then return val>0val > 0
val=sgn((dc)×(ac))+sgn((dc)×(bc))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;
}

Задачі