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

Вертикальна декомпозиція

Огляд

Вертикальна декомпозиція — це потужна техніка, яку застосовують у різноманітних геометричних задачах. Загальна ідея полягає в тому, щоб розрізати площину на кілька вертикальних смуг з певними «хорошими» властивостями і розв'язати задачу для цих смуг незалежно. Проілюструємо цю ідею на кількох прикладах.

Коли підходить цей алгоритм?
  • Вашу геометричну задачу можна звести до незалежної обробки вертикальних смуг, утворених проведенням прямих через вершини та точки перетину?
  • Усередині кожної смуги конфігурація стає простою (відрізки перетинають смугу зліва направо й не перетинаються всередині), тобто зводиться до неперетинних трапецій чи дужкового балансу?
  • Якщо ви рахуєте площу об'єднання осе-паралельних прямокутників чи довжину об'єднання відрізків, чи не простіша спеціалізована замітальна пряма? (напр. Довжина об'єднання відрізків)

Площа об'єднання трикутників

Припустимо, що на площині задано nn трикутників і нам потрібно знайти площу їх об'єднання. Задача була б легкою, якби трикутники не перетиналися, тому позбудьмося цих перетинів, розбивши площину на вертикальні смуги шляхом проведення вертикальних прямих через усі вершини й усі точки перетину сторін різних трикутників. Таких прямих може бути O(n2)O(n^2), тож ми отримали O(n2)O(n^2) смуг. Тепер розглянемо деяку вертикальну смугу. Кожен невертикальний відрізок або перетинає її зліва направо, або не перетинає взагалі. Крім того, жодні два відрізки не перетинаються строго всередині смуги. Це означає, що частина об'єднання трикутників, яка лежить усередині цієї смуги, складається з неперетинних трапецій, основи яких лежать на сторонах смуги. Ця властивість дозволяє нам обчислити площу всередині кожної смуги за допомогою такого scanline-алгоритму. Кожен відрізок, що перетинає смугу, є або верхнім, або нижнім, залежно від того, чи лежить внутрішня область відповідного трикутника вище чи нижче від відрізка. Ми можемо уявити кожен верхній відрізок як відкриваючу дужку, а кожен нижній відрізок — як закриваючу дужку, і декомпозувати смугу на трапеції, розбиваючи дужкову послідовність на менші правильні дужкові послідовності. Цей алгоритм потребує O(n3logn)O(n^3\log n) часу і O(n2)O(n^2) пам'яті.

Оптимізація 1

Спершу ми зменшимо час роботи до O(n2logn)O(n^2\log n). Замість того щоб генерувати трапеції для кожної смуги, зафіксуймо деяку сторону трикутника (відрізок s=(s0,s1)s = (s_0, s_1)) і знайдімо множину смуг, де цей відрізок є стороною деякої трапеції. Зауважимо, що в цьому випадку нам потрібно лише знайти смуги, де баланс дужок під (або над, у випадку нижнього відрізка) ss дорівнює нулю. Це означає, що замість запуску вертикальної замітальної прямої для кожної смуги ми можемо запустити горизонтальну замітальну пряму для всіх частин інших відрізків, які впливають на баланс дужок відносно ss. Для простоти ми покажемо, як це зробити для верхнього відрізка; алгоритм для нижніх відрізків аналогічний. Розгляньмо деякий інший невертикальний відрізок t=(t0,t1)t = (t_0, t_1) і знайдімо перетин [x1,x2][x_1, x_2] проєкцій ss і tt на OxOx. Якщо цей перетин порожній або складається з однієї точки, tt можна відкинути, оскільки ss і tt не перетинають внутрішню область однієї і тієї ж смуги. Інакше розгляньмо перетин II відрізків ss і tt. Є три випадки.

  1. I=I = \varnothing

    У цьому випадку tt лежить або вище, або нижче від ss на [x1,x2][x_1, x_2]. Якщо tt лежить вище, це не впливає на те, чи є ss стороною деякої трапеції. Якщо tt лежить нижче від ss, ми маємо додати 11 або 1-1 до балансу дужкових послідовностей для всіх смуг на [x1,x2][x_1, x_2], залежно від того, чи є tt верхнім, чи нижнім.

  2. II складається з єдиної точки pp

    Цей випадок можна звести до попереднього, розбивши [x1,x2][x_1, x_2] на [x1,px][x_1, p_x] і [px,x2][p_x, x_2].

  3. II — деякий відрізок ll

    Цей випадок означає, що частини ss і tt для x[x1,x2]x\in[x_1, x_2] збігаються. Якщо tt нижній, ss очевидно не є стороною трапеції. Інакше може статися, що і ss, і tt можуть розглядатися як сторона деякої трапеції. Щоб розв'язати цю неоднозначність, ми можемо вирішити, що стороною слід вважати лише відрізок з найменшим індексом (тут ми припускаємо, що сторони трикутників якимось чином пронумеровані). Отже, якщо index(s)<index(t)index(s) < index(t), цей випадок слід проігнорувати, інакше нам слід позначити, що ss ніколи не може бути стороною на [x1,x2][x_1, x_2] (наприклад, додавши відповідну подію з балансом 2-2).

Ось графічне зображення цих трьох випадків.

Visual

Насамкінець варто зауважити щодо обробки всіх додавань 11 або 1-1 на всіх смугах у [x1,x2][x_1, x_2]. Для кожного додавання ww на [x1,x2][x_1, x_2] ми можемо створити події (x1,w), (x2,w)(x_1, w),\ (x_2, -w) й обробити всі ці події замітальною прямою.

Оптимізація 2

Зауважимо, що якщо ми застосуємо попередню оптимізацію, нам більше не потрібно явно знаходити всі смуги. Це зменшує споживання пам'яті до O(n)O(n).

Перетин опуклих многокутників

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

Реалізація

Нижче наведено код, який обчислює площу об'єднання набору трикутників за O(n2logn)O(n^2\log n) часу і O(n)O(n) пам'яті.

typedef double dbl;

const dbl eps = 1e-9;

inline bool eq(dbl x, dbl y){
return fabs(x - y) < eps;
}

inline bool lt(dbl x, dbl y){
return x < y - eps;
}

inline bool gt(dbl x, dbl y){
return x > y + eps;
}

inline bool le(dbl x, dbl y){
return x < y + eps;
}

inline bool ge(dbl x, dbl y){
return x > y - eps;
}

struct pt{
dbl x, y;
inline pt operator - (const pt & p)const{
return pt{x - p.x, y - p.y};
}
inline pt operator + (const pt & p)const{
return pt{x + p.x, y + p.y};
}
inline pt operator * (dbl a)const{
return pt{x * a, y * a};
}
inline dbl cross(const pt & p)const{
return x * p.y - y * p.x;
}
inline dbl dot(const pt & p)const{
return x * p.x + y * p.y;
}
inline bool operator == (const pt & p)const{
return eq(x, p.x) && eq(y, p.y);
}
};

struct Line{
pt p[2];
Line(){}
Line(pt a, pt b):p{a, b}{}
pt vec()const{
return p[1] - p[0];
}
pt& operator [](size_t i){
return p[i];
}
};

inline bool lexComp(const pt & l, const pt & r){
if(fabs(l.x - r.x) > eps){
return l.x < r.x;
}
else return l.y < r.y;
}

vector<pt> interSegSeg(Line l1, Line l2){
if(eq(l1.vec().cross(l2.vec()), 0)){
if(!eq(l1.vec().cross(l2[0] - l1[0]), 0))
return {};
if(!lexComp(l1[0], l1[1]))
swap(l1[0], l1[1]);
if(!lexComp(l2[0], l2[1]))
swap(l2[0], l2[1]);
pt l = lexComp(l1[0], l2[0]) ? l2[0] : l1[0];
pt r = lexComp(l1[1], l2[1]) ? l1[1] : l2[1];
if(l == r)
return {l};
else return lexComp(l, r) ? vector<pt>{l, r} : vector<pt>();
}
else{
dbl s = (l2[0] - l1[0]).cross(l2.vec()) / l1.vec().cross(l2.vec());
pt inter = l1[0] + l1.vec() * s;
if(ge(s, 0) && le(s, 1) && le((l2[0] - inter).dot(l2[1] - inter), 0))
return {inter};
else
return {};
}
}
inline char get_segtype(Line segment, pt other_point){
if(eq(segment[0].x, segment[1].x))
return 0;
if(!lexComp(segment[0], segment[1]))
swap(segment[0], segment[1]);
return (segment[1] - segment[0]).cross(other_point - segment[0]) > 0 ? 1 : -1;
}

dbl union_area(vector<tuple<pt, pt, pt> > triangles){
vector<Line> segments(3 * triangles.size());
vector<char> segtype(segments.size());
for(size_t i = 0; i < triangles.size(); i++){
pt a, b, c;
tie(a, b, c) = triangles[i];
segments[3 * i] = lexComp(a, b) ? Line(a, b) : Line(b, a);
segtype[3 * i] = get_segtype(segments[3 * i], c);
segments[3 * i + 1] = lexComp(b, c) ? Line(b, c) : Line(c, b);
segtype[3 * i + 1] = get_segtype(segments[3 * i + 1], a);
segments[3 * i + 2] = lexComp(c, a) ? Line(c, a) : Line(a, c);
segtype[3 * i + 2] = get_segtype(segments[3 * i + 2], b);
}
vector<dbl> k(segments.size()), b(segments.size());
for(size_t i = 0; i < segments.size(); i++){
if(segtype[i]){
k[i] = (segments[i][1].y - segments[i][0].y) / (segments[i][1].x - segments[i][0].x);
b[i] = segments[i][0].y - k[i] * segments[i][0].x;
}
}
dbl ans = 0;
for(size_t i = 0; i < segments.size(); i++){
if(!segtype[i])
continue;
dbl l = segments[i][0].x, r = segments[i][1].x;
vector<pair<dbl, int> > evts;
for(size_t j = 0; j < segments.size(); j++){
if(!segtype[j] || i == j)
continue;
dbl l1 = segments[j][0].x, r1 = segments[j][1].x;
if(ge(l1, r) || ge(l, r1))
continue;
dbl common_l = max(l, l1), common_r = min(r, r1);
auto pts = interSegSeg(segments[i], segments[j]);
if(pts.empty()){
dbl yl1 = k[j] * common_l + b[j];
dbl yl = k[i] * common_l + b[i];
if(lt(yl1, yl) == (segtype[i] == 1)){
int evt_type = -segtype[i] * segtype[j];
evts.emplace_back(common_l, evt_type);
evts.emplace_back(common_r, -evt_type);
}
}
else if(pts.size() == 1u){
dbl yl = k[i] * common_l + b[i], yl1 = k[j] * common_l + b[j];
int evt_type = -segtype[i] * segtype[j];
if(lt(yl1, yl) == (segtype[i] == 1)){
evts.emplace_back(common_l, evt_type);
evts.emplace_back(pts[0].x, -evt_type);
}
yl = k[i] * common_r + b[i], yl1 = k[j] * common_r + b[j];
if(lt(yl1, yl) == (segtype[i] == 1)){
evts.emplace_back(pts[0].x, evt_type);
evts.emplace_back(common_r, -evt_type);
}
}
else{
if(segtype[j] != segtype[i] || j > i){
evts.emplace_back(common_l, -2);
evts.emplace_back(common_r, 2);
}
}
}
evts.emplace_back(l, 0);
sort(evts.begin(), evts.end());
size_t j = 0;
int balance = 0;
while(j < evts.size()){
size_t ptr = j;
while(ptr < evts.size() && eq(evts[j].first, evts[ptr].first)){
balance += evts[ptr].second;
++ptr;
}
if(!balance && !eq(evts[j].first, r)){
dbl next_x = ptr == evts.size() ? r : evts[ptr].first;
ans -= segtype[i] * (k[i] * (next_x + evts[j].first) + 2 * b[i]) * (next_x - evts[j].first);
}
j = ptr;
}
}
return ans/2;
}

Задачі