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

Пошук граней планарного графа

Розглянемо граф GG з nn вершинами та mm ребрами, який можна намалювати на площині так, щоб два ребра перетиналися лише у спільній вершині (якщо вона існує). Такі графи називають планарними. Тепер припустимо, що нам дано планарний граф разом з його прямолінійним вкладенням, тобто для кожної вершини vv ми маємо відповідну точку (x,y)(x, y), і всі ребра намальовані як відрізки прямих між цими точками без перетинів (таке вкладення завжди існує). Ці відрізки розбивають площину на кілька областей, які називають гранями. Рівно одна з граней є необмеженою. Цю грань називають зовнішньою, а інші грані — внутрішніми.

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

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

Деякі факти про планарні графи

У цьому розділі ми наводимо кілька фактів про планарні графи без доведення. Читачам, які цікавляться доведеннями, варто звернутися до Graph Theory by R. Diestel (див. також відеолекції про планарність на основі цієї книги) чи до якоїсь іншої книги.

Теорема Ейлера

Теорема Ейлера стверджує, що будь-яке коректне вкладення зв'язного планарного графа з nn вершинами, mm ребрами та ff гранями задовольняє рівність:

nm+f=2n - m + f = 2

А в загальнішому випадку кожен планарний граф з kk компонентами зв'язності задовольняє:

nm+f=1+kn - m + f = 1 + k

Кількість ребер планарного графа.

Якщо n3n \ge 3, то максимальна кількість ребер планарного графа з nn вершинами дорівнює 3n63n - 6. Це число досягається для будь-якого зв'язного планарного графа, у якому кожна грань обмежена трикутником. У термінах складності цей факт означає, що m=O(n)m = O(n) для будь-якого планарного графа.

Кількість граней планарного графа.

Як прямий наслідок наведеного вище факту, якщо n3n \ge 3, то максимальна кількість граней планарного графа з nn вершинами дорівнює 2n42n - 4.

Мінімальний степінь вершини в планарному графі.

Кожен планарний граф має вершину степеня 5 або менше.

Алгоритм

Спершу відсортуємо суміжні ребра для кожної вершини за полярним кутом. Тепер обходитимемо граф у такий спосіб. Припустимо, що ми зайшли у вершину uu через ребро (v,u)(v, u), і (u,w)(u, w) — це наступне ребро після (v,u)(v, u) у відсортованому списку суміжності вершини uu. Тоді наступною вершиною буде ww. Виявляється, що якщо ми почнемо цей обхід з деякого ребра (v,u)(v, u), то обійдемо рівно одну з граней, суміжних з (v,u)(v, u), причому конкретна грань залежить від того, чи перший наш крок іде з uu до vv, чи з vv до uu.

Тепер алгоритм цілком очевидний. Ми маємо перебрати всі ребра графа і запустити обхід для кожного ребра, яке не було відвідане жодним з попередніх обходів. У такий спосіб ми знайдемо кожну грань рівно один раз, і кожне ребро буде пройдено двічі (по разу в кожному напрямку).

Пошук наступного ребра

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

Пошук зовнішньої грані

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

Складність

Цілком зрозуміло, що складність алгоритму становить O(mlogm)O(m \log m) через сортування, а оскільки m=O(n)m = O(n), то насправді вона дорівнює O(nlogn)O(n \log n). Як було сказано раніше, без сортування складність стає O(n)O(n).

Що, якщо граф не є зв'язним?

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

Planar graph with holes

Реалізація

Наведена нижче реалізація повертає вектор вершин для кожної грані, причому зовнішня грань іде першою. Внутрішні грані повертаються в порядку проти годинникової стрілки, а зовнішня грань — за годинниковою стрілкою.

Для простоти ми знаходимо наступне ребро бінарним пошуком за кутом.

struct Point {
int64_t x, y;

Point(int64_t x_, int64_t y_): x(x_), y(y_) {}

Point operator - (const Point & p) const {
return Point(x - p.x, y - p.y);
}

int64_t cross (const Point & p) const {
return x * p.y - y * p.x;
}

int64_t cross (const Point & p, const Point & q) const {
return (p - *this).cross(q - *this);
}

int half () const {
return int(y < 0 || (y == 0 && x < 0));
}
};

std::vector<std::vector<size_t>> find_faces(std::vector<Point> vertices, std::vector<std::vector<size_t>> adj) {
size_t n = vertices.size();
std::vector<std::vector<char>> used(n);
for (size_t i = 0; i < n; i++) {
used[i].resize(adj[i].size());
used[i].assign(adj[i].size(), 0);
auto compare = [&](size_t l, size_t r) {
Point pl = vertices[l] - vertices[i];
Point pr = vertices[r] - vertices[i];
if (pl.half() != pr.half())
return pl.half() < pr.half();
return pl.cross(pr) > 0;
};
std::sort(adj[i].begin(), adj[i].end(), compare);
}
std::vector<std::vector<size_t>> faces;
for (size_t i = 0; i < n; i++) {
for (size_t edge_id = 0; edge_id < adj[i].size(); edge_id++) {
if (used[i][edge_id]) {
continue;
}
std::vector<size_t> face;
size_t v = i;
size_t e = edge_id;
while (!used[v][e]) {
used[v][e] = true;
face.push_back(v);
size_t u = adj[v][e];
size_t e1 = std::lower_bound(adj[u].begin(), adj[u].end(), v, [&](size_t l, size_t r) {
Point pl = vertices[l] - vertices[u];
Point pr = vertices[r] - vertices[u];
if (pl.half() != pr.half())
return pl.half() < pr.half();
return pl.cross(pr) > 0;
}) - adj[u].begin() + 1;
if (e1 == adj[u].size()) {
e1 = 0;
}
v = u;
e = e1;
}
std::reverse(face.begin(), face.end());
Point p1 = vertices[face[0]];
__int128 sum = 0;
for (int j = 0; j < face.size(); ++j) {
Point p2 = vertices[face[j]];
Point p3 = vertices[face[(j + 1) % face.size()]];
sum += (p2 - p1).cross(p3 - p2);
}
if (sum <= 0) {
faces.insert(faces.begin(), face);
} else {
faces.emplace_back(face);
}
}
}
return faces;
}

Побудова планарного графа з відрізків

Іноді граф задають не явно, а як набір відрізків на площині, причому сам граф утворюється за рахунок перетинів цих відрізків, як показано на зображенні нижче. У цьому разі граф доводиться будувати вручну. Найпростіший спосіб зробити це такий. Зафіксуємо відрізок і перетнемо його з усіма іншими відрізками. Потім відсортуємо всі точки перетину разом з двома кінцями відрізка лексикографічно і додамо їх до графа як вершини. Також з'єднаємо ребром кожні дві сусідні вершини в лексикографічному порядку. Виконавши цю процедуру для всіх ребер, ми отримаємо граф. Звісно, ми маємо подбати про те, щоб двом однаковим точкам перетину завжди відповідала одна й та сама вершина. Найпростіше зробити це, зберігаючи точки у відображенні (map) за їхніми координатами, вважаючи рівними точки, координати яких відрізняються на мале число (скажімо, менше за 10910^{-9}). Цей алгоритм працює за O(n2logn)O(n^2 \log n).

Implicitly defined graph

Реалізація

using dbl = long double;

const dbl eps = 1e-9;

struct Point {
dbl x, y;

Point(){}
Point(dbl x_, dbl y_): x(x_), y(y_) {}

Point operator * (dbl d) const {
return Point(x * d, y * d);
}

Point operator + (const Point & p) const {
return Point(x + p.x, y + p.y);
}

Point operator - (const Point & p) const {
return Point(x - p.x, y - p.y);
}

dbl cross (const Point & p) const {
return x * p.y - y * p.x;
}

dbl cross (const Point & p, const Point & q) const {
return (p - *this).cross(q - *this);
}

dbl dot (const Point & p) const {
return x * p.x + y * p.y;
}

dbl dot (const Point & p, const Point & q) const {
return (p - *this).dot(q - *this);
}

bool operator < (const Point & p) const {
if (fabs(x - p.x) < eps) {
if (fabs(y - p.y) < eps) {
return false;
} else {
return y < p.y;
}
} else {
return x < p.x;
}
}

bool operator == (const Point & p) const {
return fabs(x - p.x) < eps && fabs(y - p.y) < eps;
}

bool operator >= (const Point & p) const {
return !(*this < p);
}
};

struct Line{
Point p[2];

Line(Point l, Point r){p[0] = l; p[1] = r;}
Point& operator [](const int & i){return p[i];}
const Point& operator[](const int & i)const{return p[i];}
Line(const Line & l){
p[0] = l.p[0]; p[1] = l.p[1];
}
Point getOrth()const{
return Point(p[1].y - p[0].y, p[0].x - p[1].x);
}
bool hasPointLine(const Point & t)const{
return std::fabs(p[0].cross(p[1], t)) < eps;
}
bool hasPointSeg(const Point & t)const{
return hasPointLine(t) && t.dot(p[0], p[1]) < eps;
}
};

std::vector<Point> interLineLine(Line l1, Line l2){
if(std::fabs(l1.getOrth().cross(l2.getOrth())) < eps){
if(l1.hasPointLine(l2[0]))return {l1[0], l1[1]};
else return {};
}
Point u = l2[1] - l2[0];
Point v = l1[1] - l1[0];
dbl s = u.cross(l2[0] - l1[0])/u.cross(v);
return {Point(l1[0] + v * s)};
}

std::vector<Point> interSegSeg(Line l1, Line l2){
if (l1[0] == l1[1]) {
if (l2[0] == l2[1]) {
if (l1[0] == l2[0])
return {l1[0]};
else
return {};
} else {
if (l2.hasPointSeg(l1[0]))
return {l1[0]};
else
return {};
}
}
if (l2[0] == l2[1]) {
if (l1.hasPointSeg(l2[0]))
return {l2[0]};
else
return {};
}
auto li = interLineLine(l1, l2);
if (li.empty())
return li;
if (li.size() == 2) {
if (l1[0] >= l1[1])
std::swap(l1[0], l1[1]);
if (l2[0] >= l2[1])
std::swap(l2[0], l2[1]);
std::vector<Point> res(2);
if (l1[0] < l2[0])
res[0] = l2[0];
else
res[0] = l1[0];
if (l1[1] < l2[1])
res[1] = l1[1];
else
res[1] = l2[1];
if (res[0] == res[1])
res.pop_back();
if (res.size() == 2u && res[1] < res[0])
return {};
else
return res;
}
Point cand = li[0];
if (l1.hasPointSeg(cand) && l2.hasPointSeg(cand))
return {cand};
else
return {};
}

std::pair<std::vector<Point>, std::vector<std::vector<size_t>>> build_graph(std::vector<Line> segments) {
std::vector<Point> p;
std::vector<std::vector<size_t>> adj;
std::map<std::pair<int64_t, int64_t>, size_t> point_id;
auto get_point_id = [&](Point pt) {
auto repr = std::make_pair(
int64_t(std::round(pt.x * 1000000000) + 1e-6),
int64_t(std::round(pt.y * 1000000000) + 1e-6)
);
if (!point_id.count(repr)) {
adj.emplace_back();
size_t id = point_id.size();
point_id[repr] = id;
p.push_back(pt);
return id;
} else {
return point_id[repr];
}
};
for (size_t i = 0; i < segments.size(); i++) {
std::vector<size_t> curr = {
get_point_id(segments[i][0]),
get_point_id(segments[i][1])
};
for (size_t j = 0; j < segments.size(); j++) {
if (i == j)
continue;
auto inter = interSegSeg(segments[i], segments[j]);
for (auto pt: inter) {
curr.push_back(get_point_id(pt));
}
}
std::sort(curr.begin(), curr.end(), [&](size_t l, size_t r) { return p[l] < p[r]; });
curr.erase(std::unique(curr.begin(), curr.end()), curr.end());
for (size_t j = 0; j + 1 < curr.size(); j++) {
adj[curr[j]].push_back(curr[j + 1]);
adj[curr[j + 1]].push_back(curr[j]);
}
}
for (size_t i = 0; i < adj.size(); i++) {
std::sort(adj[i].begin(), adj[i].end());
// видаляємо ребра, що були додані кілька разів
adj[i].erase(std::unique(adj[i].begin(), adj[i].end()), adj[i].end());
}
return {p, adj};
}

Задачі