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

Пошук пари відрізків, що перетинаються

Задано nn відрізків на площині. Потрібно перевірити, чи перетинаються хоча б два з них між собою. Якщо відповідь так, то вивести цю пару відрізків, що перетинаються; якщо відповідей кілька, достатньо вибрати будь-яку з них.

Наївний алгоритм розв'язання — перебрати всі пари відрізків за O(n2)O(n^2) і для кожної пари перевірити, чи перетинаються вони. У цій статті описано алгоритм зі складністю O(nlogn)O(n \log n), який ґрунтується на методі замітальної прямої.

Коли підходить цей алгоритм?
  • Відрізків багато (nn великий), і перебір усіх пар за O(n2)O(n^2) занадто повільний?
  • Тобі треба знайти перетин серед множини відрізків, а не перевірити одну конкретну пару? (якщо ні → Перевірка перетину двох відрізків)
  • Достатньо знайти будь-яку одну пару, що перетинається (а не всі точки перетину)?

Алгоритм

Подумки проведемо вертикальну пряму x=x = -\infty і почнемо рухати цю пряму вправо. Під час свого руху ця пряма зустрічатиметься з відрізками, і щоразу, коли відрізок перетинає нашу пряму, він перетинає її рівно в одній точці (вважатимемо, що вертикальних відрізків немає).

sweep line and line segment intersection

Отже, для кожного відрізка в якийсь момент часу його точка з'явиться на замітальній прямій, потім із рухом прямої ця точка рухатиметься, і нарешті в якийсь момент відрізок зникне з прямої.

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

relative order of the segments across sweep line

Цей порядок цікавий тим, що відрізки, які перетинаються, матимуть однакову yy-координату принаймні в один момент часу:

intersection point having same y-coordinate

Сформулюємо ключові твердження:

  • Щоб знайти пару відрізків, що перетинаються, достатньо розглядати лише сусідні відрізки при кожному фіксованому положенні замітальної прямої.
  • Достатньо розглядати замітальну пряму не в усіх можливих дійсних положеннях (+)(-\infty \ldots +\infty), а лише в тих положеннях, коли з'являються нові відрізки або зникають старі. Інакше кажучи, достатньо обмежитися лише положеннями, що дорівнюють абсцисам кінцевих точок відрізків.
  • Коли з'являється новий відрізок, достатньо вставити його у потрібне місце списку, отриманого для попереднього положення замітальної прямої. Нам потрібно лише перевірити перетин доданого відрізка з його безпосередніми сусідами у списку зверху та знизу.
  • Якщо відрізок зникає, достатньо вилучити його з поточного списку. Після цього необхідно перевірити перетин верхнього та нижнього сусідів у списку.
  • Інших змін у послідовності відрізків у списку, крім описаних, не існує. Жодних інших перевірок на перетин не потрібно.

Щоб зрозуміти істинність цих тверджень, достатньо таких зауважень:

  • Два відрізки, що не перетинаються, ніколи не змінюють свого відносного порядку.
    Справді, якщо один відрізок спочатку був вищим за інший, а потім став нижчим, то між цими двома моментами відбувся перетин цих двох відрізків.
  • Два відрізки, що не перетинаються, також не можуть мати однакові yy-координати.
  • Звідси випливає, що в момент появи відрізка ми можемо знайти позицію для цього відрізка в черзі, і нам більше не доведеться переставляти цей відрізок у черзі: його порядок відносно інших відрізків у черзі не зміниться.
  • Два відрізки, що перетинаються, у момент їхньої точки перетину будуть сусідами один одного в черзі.
  • Тому для пошуку пар відрізків, що перетинаються, достатньо перевірити перетин усіх і лише тих пар відрізків, які колись під час руху замітальної прямої хоча б раз були сусідами один одного.
    Легко помітити, що достатньо лише перевірити доданий відрізок з його верхнім і нижнім сусідами, а також при вилученні відрізка — його верхнього і нижнього сусіда (які після вилучення стануть сусідами один одного).
  • Слід зауважити, що при фіксованому положенні замітальної прямої ми маємо спочатку додати всі відрізки, що починаються в цій x-координаті, і лише потім вилучити всі відрізки, що тут закінчуються.
    Таким чином, ми не пропускаємо перетин відрізків у вершині: тобто такі випадки, коли два відрізки мають спільну вершину.
  • Зауважимо, що вертикальні відрізки насправді не впливають на коректність алгоритму.
    Ці відрізки відрізняються тим, що з'являються і зникають одночасно. Однак завдяки попередньому зауваженню ми знаємо, що всі відрізки спочатку будуть додані до черги, і лише потім будуть вилучені. Тому, якщо вертикальний відрізок перетинається з якимось іншим відрізком, відкритим у той момент (зокрема й вертикальним), це буде виявлено.
    У яке місце черги поставити вертикальні відрізки? Адже вертикальний відрізок не має однієї конкретної yy-координати, він тягнеться вздовж цілого відрізка по yy-координаті. Однак легко зрозуміти, що за yy-координату можна взяти будь-яку координату з цього відрізка.

Таким чином, увесь алгоритм виконає не більше ніж 2n2n перевірок на перетин пари відрізків, і виконає O(n)O(n) операцій із чергою відрізків (O(1)O(1) операцій у момент появи та зникнення кожного відрізка).

Підсумкова асимптотична поведінка алгоритму отже становить O(nlogn)O(n \log n).

Реалізація

Наведемо повну реалізацію описаного алгоритму:

const double EPS = 1E-9;

struct pt {
double x, y;
};

struct seg {
pt p, q;
int id;

double get_y(double x) const {
if (abs(p.x - q.x) < EPS)
return p.y;
return p.y + (q.y - p.y) * (x - p.x) / (q.x - p.x);
}
};

bool intersect1d(double l1, double r1, double l2, double r2) {
if (l1 > r1)
swap(l1, r1);
if (l2 > r2)
swap(l2, r2);
return max(l1, l2) <= min(r1, r2) + EPS;
}

int vec(const pt& a, const pt& b, const pt& c) {
double s = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
return abs(s) < EPS ? 0 : s > 0 ? +1 : -1;
}

bool intersect(const seg& a, const seg& b)
{
return intersect1d(a.p.x, a.q.x, b.p.x, b.q.x) &&
intersect1d(a.p.y, a.q.y, b.p.y, b.q.y) &&
vec(a.p, a.q, b.p) * vec(a.p, a.q, b.q) <= 0 &&
vec(b.p, b.q, a.p) * vec(b.p, b.q, a.q) <= 0;
}

bool operator<(const seg& a, const seg& b)
{
double x = max(min(a.p.x, a.q.x), min(b.p.x, b.q.x));
return a.get_y(x) < b.get_y(x) - EPS;
}

struct event {
double x;
int tp, id;

event() {}
event(double x, int tp, int id) : x(x), tp(tp), id(id) {}

bool operator<(const event& e) const {
if (abs(x - e.x) > EPS)
return x < e.x;
return tp > e.tp;
}
};

set<seg> s;
vector<set<seg>::iterator> where;

set<seg>::iterator prev(set<seg>::iterator it) {
return it == s.begin() ? s.end() : --it;
}

set<seg>::iterator next(set<seg>::iterator it) {
return ++it;
}

pair<int, int> solve(const vector<seg>& a) {
int n = (int)a.size();
vector<event> e;
for (int i = 0; i < n; ++i) {
e.push_back(event(min(a[i].p.x, a[i].q.x), +1, i));
e.push_back(event(max(a[i].p.x, a[i].q.x), -1, i));
}
sort(e.begin(), e.end());

s.clear();
where.resize(a.size());
for (size_t i = 0; i < e.size(); ++i) {
int id = e[i].id;
if (e[i].tp == +1) {
set<seg>::iterator nxt = s.lower_bound(a[id]), prv = prev(nxt);
if (nxt != s.end() && intersect(*nxt, a[id]))
return make_pair(nxt->id, id);
if (prv != s.end() && intersect(*prv, a[id]))
return make_pair(prv->id, id);
where[id] = s.insert(nxt, a[id]);
} else {
set<seg>::iterator nxt = next(where[id]), prv = prev(where[id]);
if (nxt != s.end() && prv != s.end() && intersect(*nxt, *prv))
return make_pair(prv->id, nxt->id);
s.erase(where[id]);
}
}

return make_pair(-1, -1);
}

Головна функція тут — solve(), яка повертає відрізки, що перетинаються, якщо такі існують, або (1,1)(-1, -1), якщо перетинів немає.

Перевірка перетину двох відрізків виконується функцією intersect (), що використовує алгоритм на основі орієнтованої площі трикутника.

Черга відрізків — це глобальна змінна s, тип set<event>. Ітератори, що задають позицію кожного відрізка в черзі (для зручного вилучення відрізків із черги), зберігаються у глобальному масиві where.

Також уведено дві допоміжні функції prev() і next(), які повертають ітератори на попередній та наступний елементи (або end(), якщо такого не існує).

Константа EPS позначає похибку порівняння двох дійсних чисел (вона переважно використовується при перевірці двох відрізків на перетин).

Задачі

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