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

Тріангуляція Делоне та діаграма Вороного

Розглянемо множину {pi}\{p_i\} точок на площині. Діаграма Вороного V({pi})V(\{p_i\}) множини {pi}\{p_i\} — це розбиття площини на nn областей ViV_i, де Vi={pR2; ρ(p,pi)=min ρ(p,pk)}V_i = \{p\in\mathbb{R}^2;\ \rho(p, p_i) = \min\ \rho(p, p_k)\}. Комірки діаграми Вороного — це многокутники (можливо, нескінченні). Тріангуляція Делоне D({pi})D(\{p_i\}) множини {pi}\{p_i\} — це така тріангуляція, у якій кожна точка pip_i лежить поза описаним колом будь-якого трикутника TD({pi})T \in D(\{p_i\}) або на його межі.

Є один неприємний вироджений випадок, коли діаграма Вороного не є зв'язною, а тріангуляція Делоне не існує. Це трапляється тоді, коли всі точки колінеарні.

Коли підходить цей алгоритм?
  • Потрібна тріангуляція множини точок із максимально «товстими» трикутниками (максимізується мінімальний кут) або двоїста їй діаграма Вороного?
  • Тобі потрібне саме розбиття площини / найближчі сусіди, а не лише опукла межа множини? (якщо ні → Побудова опуклої оболонки)
  • Вхідні точки не всі колінеарні (інакше тріангуляція не існує і випадок треба обробляти окремо)?

Властивості

Тріангуляція Делоне максимізує мінімальний кут серед усіх можливих тріангуляцій.

Мінімальне евклідове кістякове дерево множини точок є підмножиною ребер її тріангуляції Делоне.

Двоїстість

Припустимо, що точки {pi}\{p_i\} не колінеарні й серед {pi}\{p_i\} немає чотирьох точок, що лежать на одному колі. Тоді V({pi})V(\{p_i\}) і D({pi})D(\{p_i\}) двоїсті, тож якщо ми отримали одне з них, то можемо отримати інше за O(n)O(n). Що робити, якщо це не так? Колінеарний випадок легко обробити окремо. В іншому разі двоїстими є VV і DD', де DD' отримується з DD видаленням усіх ребер, для яких два трикутники на цьому ребрі мають спільне описане коло.

Побудова Делоне та Вороного

Завдяки двоїстості нам потрібен швидкий алгоритм для обчислення лише одного з VV і DD. Ми опишемо, як побудувати D({pi})D(\{p_i\}) за O(nlogn)O(n\log n). Тріангуляцію будуватимемо за алгоритмом «розділяй і володарюй» Гібаса та Столфі (Guibas, Stolfi).

Структура даних quad-edge

Під час роботи алгоритму DD зберігатиметься у структурі даних quad-edge (структура «чотириребро»). Цю структуру зображено на рисунку:

Quad-Edge

В алгоритмі ми використовуватимемо такі функції над ребрами:

  1. make_edge(a, b)
    Ця функція створює ізольоване ребро з точки a у точку b разом із оберненим до нього ребром і обома двоїстими ребрами.
  2. splice(a, b)
    Це ключова функція алгоритму. Вона міняє місцями a->Onext з b->Onext та a->Onext->Rot->Onext з b->Onext->Rot->Onext.
  3. delete_edge(e)
    Ця функція видаляє e з тріангуляції. Щоб видалити e, ми можемо просто викликати splice(e, e->Oprev) і splice(e->Rev, e->Rev->Oprev).
  4. connect(a, b)
    Ця функція створює нове ребро e з a->Dest у b->Org так, щоб a, b, e мали одну й ту саму ліву грань. Для цього ми викликаємо e = make_edge(a->Dest, b->Org), splice(e, a->Lnext) і splice(e->Rev, b).

Алгоритм

Алгоритм обчислить тріангуляцію й поверне два quad-edge: ребро опуклої оболонки проти годинникової стрілки, що виходить із найлівішої вершини, і ребро опуклої оболонки за годинниковою стрілкою, що виходить із найправішої вершини.

Відсортуймо всі точки за x, а якщо x1=x2x_1 = x_2 — то за y. Розв'яжемо задачу для деякого відрізка (l,r)(l, r) (початково (l,r)=(0,n1)(l, r) = (0, n - 1)). Якщо rl+1=2r - l + 1 = 2, ми додамо ребро (p[l],p[r])(p[l], p[r]) і повернемо результат. Якщо rl+1=3r - l + 1 = 3, то спочатку додамо ребра (p[l],p[l+1])(p[l], p[l + 1]) та (p[l+1],p[r])(p[l + 1], p[r]). Також ми маємо з'єднати їх за допомогою splice(a->Rev, b). Тепер потрібно замкнути трикутник. Наша наступна дія залежатиме від орієнтації точок p[l],p[l+1],p[r]p[l], p[l + 1], p[r]. Якщо вони колінеарні, ми не можемо утворити трикутник, тож просто повертаємо (a, b->Rev). Інакше ми створюємо нове ребро c, викликаючи connect(b, a). Якщо точки орієнтовані проти годинникової стрілки, повертаємо (a, b->Rev). Інакше повертаємо (c->Rev, c).

Тепер припустимо, що rl+14r - l + 1 \ge 4. Спочатку рекурсивно розв'яжемо L=(l,l+r2)L = (l, \frac{l + r}{2}) та R=(l+r2+1,r)R = (\frac{l + r}{2} + 1, r). Тепер нам потрібно злити ці тріангуляції в одну. Зауважимо, що наші точки відсортовані, тож під час злиття ми додаватимемо ребра з L до R (так звані перехресні ребра) і видалятимемо деякі ребра з L до L та з R до R. Яка структура перехресних ребер? Усі ці ребра мають перетинати пряму, паралельну осі y та проведену через значення x, за яким відбувається розбиття. Це задає лінійний порядок на перехресних ребрах, тож ми можемо говорити про послідовні перехресні ребра, найнижче перехресне ребро тощо. Алгоритм додаватиме перехресні ребра у порядку зростання. Зауважимо, що будь-які два сусідні перехресні ребра матимуть спільний кінець, а третя сторона трикутника, який вони визначають, іде з L до L або з R до R. Назвімо поточне перехресне ребро базовим (base). Наступник базового ребра іде або з лівого кінця базового до одного з R-сусідів правого кінця, або навпаки. Розглянемо описане коло базового та попереднього перехресного ребра. Припустимо, що це коло перетворюється на інші кола, які мають базове ребро своєю хордою, але лежать далі в напрямку осі Oy. Наше коло якийсь час підійматиметься вгору, але якщо базове ребро не є верхньою дотичною до L і R, ми натрапимо на точку, що належить або L, або R, і вона породить новий трикутник без жодної точки в його описаному колі. Нове L-R ребро цього трикутника і є наступним доданим перехресним ребром. Щоб робити це ефективно, ми обчислюємо два ребра lcand і rcand так, щоб lcand указувало на першу точку з L, на яку ми натрапимо в цьому процесі, а rcand — на першу точку з R. Потім ми вибираємо ту з них, на яку натрапимо першою. Початково базове ребро вказує на нижню дотичну до L і R.

Реалізація

Зауважимо, що реалізація функції in_circle є специфічною для GCC.

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& a) { return a >= 0 ? a ? 1 : 0 : -1; }

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

const pt inf_pt = pt(1e18, 1e18);

struct QuadEdge {
pt origin;
QuadEdge* rot = nullptr;
QuadEdge* onext = nullptr;
bool used = false;
QuadEdge* rev() const {
return rot->rot;
}
QuadEdge* lnext() const {
return rot->rev()->onext->rot;
}
QuadEdge* oprev() const {
return rot->onext->rot;
}
pt dest() const {
return rev()->origin;
}
};

QuadEdge* make_edge(pt from, pt to) {
QuadEdge* e1 = new QuadEdge;
QuadEdge* e2 = new QuadEdge;
QuadEdge* e3 = new QuadEdge;
QuadEdge* e4 = new QuadEdge;
e1->origin = from;
e2->origin = to;
e3->origin = e4->origin = inf_pt;
e1->rot = e3;
e2->rot = e4;
e3->rot = e2;
e4->rot = e1;
e1->onext = e1;
e2->onext = e2;
e3->onext = e4;
e4->onext = e3;
return e1;
}

void splice(QuadEdge* a, QuadEdge* b) {
swap(a->onext->rot->onext, b->onext->rot->onext);
swap(a->onext, b->onext);
}

void delete_edge(QuadEdge* e) {
splice(e, e->oprev());
splice(e->rev(), e->rev()->oprev());
delete e->rev()->rot;
delete e->rev();
delete e->rot;
delete e;
}

QuadEdge* connect(QuadEdge* a, QuadEdge* b) {
QuadEdge* e = make_edge(a->dest(), b->origin);
splice(e, a->lnext());
splice(e->rev(), b);
return e;
}

bool left_of(pt p, QuadEdge* e) {
return gt(p.cross(e->origin, e->dest()), 0);
}

bool right_of(pt p, QuadEdge* e) {
return lt(p.cross(e->origin, e->dest()), 0);
}

template <class T>
T det3(T a1, T a2, T a3, T b1, T b2, T b3, T c1, T c2, T c3) {
return a1 * (b2 * c3 - c2 * b3) - a2 * (b1 * c3 - c1 * b3) +
a3 * (b1 * c2 - c1 * b2);
}

bool in_circle(pt a, pt b, pt c, pt d) {
// Якщо є __int128, обчислюємо напряму.
// Інакше обчислюємо кути.
#if defined(__LP64__) || defined(_WIN64)
__int128 det = -det3<__int128>(b.x, b.y, b.sqrLength(), c.x, c.y,
c.sqrLength(), d.x, d.y, d.sqrLength());
det += det3<__int128>(a.x, a.y, a.sqrLength(), c.x, c.y, c.sqrLength(), d.x,
d.y, d.sqrLength());
det -= det3<__int128>(a.x, a.y, a.sqrLength(), b.x, b.y, b.sqrLength(), d.x,
d.y, d.sqrLength());
det += det3<__int128>(a.x, a.y, a.sqrLength(), b.x, b.y, b.sqrLength(), c.x,
c.y, c.sqrLength());
return det > 0;
#else
auto ang = [](pt l, pt mid, pt r) {
ll x = mid.dot(l, r);
ll y = mid.cross(l, r);
long double res = atan2((long double)x, (long double)y);
return res;
};
long double kek = ang(a, b, c) + ang(c, d, a) - ang(b, c, d) - ang(d, a, b);
if (kek > 1e-8)
return true;
else
return false;
#endif
}

pair<QuadEdge*, QuadEdge*> build_tr(int l, int r, vector<pt>& p) {
if (r - l + 1 == 2) {
QuadEdge* res = make_edge(p[l], p[r]);
return make_pair(res, res->rev());
}
if (r - l + 1 == 3) {
QuadEdge *a = make_edge(p[l], p[l + 1]), *b = make_edge(p[l + 1], p[r]);
splice(a->rev(), b);
int sg = sgn(p[l].cross(p[l + 1], p[r]));
if (sg == 0)
return make_pair(a, b->rev());
QuadEdge* c = connect(b, a);
if (sg == 1)
return make_pair(a, b->rev());
else
return make_pair(c->rev(), c);
}
int mid = (l + r) / 2;
QuadEdge *ldo, *ldi, *rdo, *rdi;
tie(ldo, ldi) = build_tr(l, mid, p);
tie(rdi, rdo) = build_tr(mid + 1, r, p);
while (true) {
if (left_of(rdi->origin, ldi)) {
ldi = ldi->lnext();
continue;
}
if (right_of(ldi->origin, rdi)) {
rdi = rdi->rev()->onext;
continue;
}
break;
}
QuadEdge* basel = connect(rdi->rev(), ldi);
auto valid = [&basel](QuadEdge* e) { return right_of(e->dest(), basel); };
if (ldi->origin == ldo->origin)
ldo = basel->rev();
if (rdi->origin == rdo->origin)
rdo = basel;
while (true) {
QuadEdge* lcand = basel->rev()->onext;
if (valid(lcand)) {
while (in_circle(basel->dest(), basel->origin, lcand->dest(),
lcand->onext->dest())) {
QuadEdge* t = lcand->onext;
delete_edge(lcand);
lcand = t;
}
}
QuadEdge* rcand = basel->oprev();
if (valid(rcand)) {
while (in_circle(basel->dest(), basel->origin, rcand->dest(),
rcand->oprev()->dest())) {
QuadEdge* t = rcand->oprev();
delete_edge(rcand);
rcand = t;
}
}
if (!valid(lcand) && !valid(rcand))
break;
if (!valid(lcand) ||
(valid(rcand) && in_circle(lcand->dest(), lcand->origin,
rcand->origin, rcand->dest())))
basel = connect(rcand, basel->rev());
else
basel = connect(basel->rev(), lcand->rev());
}
return make_pair(ldo, rdo);
}

vector<tuple<pt, pt, pt>> delaunay(vector<pt> p) {
sort(p.begin(), p.end(), [](const pt& a, const pt& b) {
return lt(a.x, b.x) || (eq(a.x, b.x) && lt(a.y, b.y));
});
auto res = build_tr(0, (int)p.size() - 1, p);
QuadEdge* e = res.first;
vector<QuadEdge*> edges = {e};
while (lt(e->onext->dest().cross(e->dest(), e->origin), 0))
e = e->onext;
auto add = [&p, &e, &edges]() {
QuadEdge* curr = e;
do {
curr->used = true;
p.push_back(curr->origin);
edges.push_back(curr->rev());
curr = curr->lnext();
} while (curr != e);
};
add();
p.clear();
int kek = 0;
while (kek < (int)edges.size()) {
if (!(e = edges[kek++])->used)
add();
}
vector<tuple<pt, pt, pt>> ans;
for (int i = 0; i < (int)p.size(); i += 3) {
ans.push_back(make_tuple(p[i], p[i + 1], p[i + 2]));
}
return ans;
}

Задачі

Відеоматеріали