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

Мангеттенська відстань

Означення

Для точок pp і qq на площині ми можемо визначити відстань між ними як суму різниць їхніх координат xx та yy:

d(p,q)=xpxq+ypyqd(p,q) = |x_p - x_q| + |y_p - y_q|

Визначена в такий спосіб, відстань відповідає так званій мангеттенській (таксомоторній) геометрії, у якій точки вважаються перехрестями в добре спланованому місті на кшталт Мангеттена, де рухатися можна лише вулицями — горизонтально або вертикально, як показано на зображенні нижче:

Manhattan Distance

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

Із цією відстанню можна виконувати деякі цікаві трюки та алгоритми, і кілька з них ми покажемо тут.

Коли підходить цей алгоритм?
  • Відстані у вашій задачі вимірюються як xpxq+ypyq|x_p - x_q| + |y_p - y_q| (рух лише вздовж осей), а не евклідово? (якщо потрібна евклідова найближча пара → Пошук найближчої пари точок)
  • Вам потрібен один із трюків саме для L1L_1-метрики: найвіддаленіша пара, перетворення в метрику Чебишова чи мангеттенське MST?
  • Для MST: ваги ребер — це мангеттенські відстані між точками, і вам вистачить O(nlogn)O(n \log n) кандидатних ребер з наступним алгоритмом Краскала?

Найвіддаленіша пара точок у мангеттенській відстані

Маємо nn точок PP і хочемо знайти пару точок p,qp,q, які найбільш віддалені одна від одної, тобто максимізувати xpxq+ypyq|x_p - x_q| + |y_p - y_q|.

Поміркуймо спершу в одному вимірі, тож y=0y=0. Головне спостереження полягає в тому, що ми можемо перебрати, чи дорівнює xpxq|x_p - x_q| значенню xpxqx_p - x_q, чи xp+xq-x_p + x_q, адже якщо ми «промахнемося зі знаком» абсолютної величини, то отримаємо лише менше значення, тому це не може вплинути на відповідь. Більш формально виконується:

xpxq=max(xpxq,xp+xq)|x_p - x_q| = \max(x_p - x_q, -x_p + x_q)

Тож, наприклад, ми можемо спробувати взяти таке pp, що xpx_p має знак плюс, і тоді qq повинно мати від'ємний знак. У такий спосіб ми хочемо знайти:

maxp,qP(xp+(xq))=maxpP(xp)+maxqP(xq).\max\limits_{p, q \in P}(x_p + (-x_q)) = \max\limits_{p \in P}(x_p) + \max\limits_{q \in P}( - x_q ).

Зауважимо, що цю ідею можна поширити далі на 2 (або більше!) вимірів. Для dd вимірів нам потрібно перебрати 2d2^d можливих значень знаків. Наприклад, якщо ми у 22 вимірах і перебираємо варіант, коли pp має обидва знаки плюс, ми хочемо знайти:

maxp,qP[(xp+(xq))+(yp+(yq))]=maxpP(xp+yp)+maxqP(xqyq).\max\limits_{p, q \in P} [(x_p + (-x_q)) + (y_p + (-y_q))] = \max\limits_{p \in P}(x_p + y_p) + \max\limits_{q \in P}(-x_q - y_q).

Оскільки ми зробили pp і qq незалежними, тепер легко знайти pp і qq, які максимізують цей вираз.

Код нижче узагальнює це на dd вимірів і працює за O(n2dd)O(n \cdot 2^d \cdot d).

long long ans = 0;
for (int msk = 0; msk < (1 << d); msk++) {
long long mx = LLONG_MIN, mn = LLONG_MAX;
for (int i = 0; i < n; i++) {
long long cur = 0;
for (int j = 0; j < d; j++) {
if (msk & (1 << j)) cur += p[i][j];
else cur -= p[i][j];
}
mx = max(mx, cur);
mn = min(mn, cur);
}
ans = max(ans, mx - mn);
}

Поворот точок і відстань Чебишова

Добре відомо, що для всіх m,nRm, n \in \mathbb{R},

m+n=max(m+n,mn).|m| + |n| = \text{max}(|m + n|, |m - n|).

Щоб довести це, нам достатньо проаналізувати знаки mm і nn. Це залишаємо як вправу.

Ми можемо застосувати цю рівність до формули мангеттенської відстані й з'ясувати, що

d((x1,y1),(x2,y2))=x1x2+y1y2=max((x1+y1)(x2+y2),(y1x1)(y2x2)).d((x_1, y_1), (x_2, y_2)) = |x_1 - x_2| + |y_1 - y_2| = \text{max}(|(x_1 + y_1) - (x_2 + y_2)|, |(y_1 - x_1) - (y_2 - x_2)|).

Останній вираз у попередній рівності — це відстань Чебишова точок (x1+y1,y1x1)(x_1 + y_1, y_1 - x_1) і (x2+y2,y2x2)(x_2 + y_2, y_2 - x_2). Це означає, що після застосування перетворення

α:(x,y)(x+y,yx),\alpha : (x, y) \to (x + y, y - x),

мангеттенська відстань між точками pp і qq перетворюється на відстань Чебишова між α(p)\alpha(p) і α(q)\alpha(q).

Також ми можемо помітити, що α\alpha — це спіральна подібність (поворот площини з наступним розтягуванням відносно центра OO) з центром (0,0)(0, 0), кутом повороту 4545^{\circ} за годинниковою стрілкою та розтягуванням у 2\sqrt{2} разів.

Ось зображення, яке допоможе уявити це перетворення:

Chebyshev transformation

Мангеттенське мінімальне кістякове дерево

Задача про мангеттенське MST полягає в тому, щоб для заданих точок на площині знайти ребра, які з'єднують усі точки й мають мінімальну сумарну вагу. Вага ребра, що з'єднує дві точки, — це мангеттенська відстань між ними. Для простоти вважаємо, що всі точки розташовані в різних місцях. Тут ми показуємо спосіб знайти MST за O(nlogn)O(n \log{n}), відшукуючи для кожної точки її найближчого сусіда в кожному октанті, як показано на зображенні нижче. Це дасть нам O(n)O(n) кандидатних ребер, які, як ми покажемо нижче, гарантовано містять MST. Останній крок — застосувати якийсь стандартний алгоритм пошуку MST, наприклад, алгоритм Краскала з використанням системи неперетинних множин.

8 octants picture 8 октантів відносно точки S

Наведений тут алгоритм уперше було представлено в статті H. Zhou, N. Shenoy, and W. Nichollos (2002). Існує також інший відомий алгоритм, який використовує підхід «розділяй і володарюй» від J. Stolfi, і він теж дуже цікавий та відрізняється лише способом пошуку найближчого сусіда в кожному октанті. Обидва мають однакову складність, але наведений тут простіше реалізувати, і він має менший константний множник.

Спершу зрозуміймо, чому достатньо розглядати лише найближчого сусіда в кожному октанті. Ідея полягає в тому, щоб показати, що для точки ss і будь-яких двох інших точок pp і qq в тому самому октанті виконується d(p,q)<max(d(s,p),d(s,q))d(p, q) < \max(d(s, p), d(s, q)). Це важливо, бо показує, що якби існувало MST, у якому ss з'єднана й з pp, і з qq, ми могли б видалити одне з цих ребер і додати ребро (p,q)(p,q), що зменшило б сумарну вартість. Щоб довести це, без втрати загальності припустимо, що pp і qq лежать в октанті R1R_1, який визначається умовами: xsxx_s \leq x і xsys>xyx_s - y_s > x - y, а далі розглянемо кілька випадків. Зображення нижче дає певну інтуїцію щодо того, чому це правда.

unique nearest neighbor Інтуїтивно, обмеженість октанта робить неможливим, щоб pp і qq були обидва ближчі до ss, ніж одна до одної

Отже, головне питання — як знайти найближчого сусіда в кожному октанті для кожної з nn точок.

Найближчий сусід у кожному октанті за O(n log n)

Для простоти зосередимося на октанті NNE (R1R_1 на зображенні вище). Усі інші напрямки можна знайти тим самим алгоритмом, повертаючи вхідні дані.

Ми застосуємо підхід із замітальною прямою. Ми обробляємо точки з південного заходу на північний схід, тобто за неспадним x+yx + y. Ми також тримаємо множину точок, які ще не мають свого найближчого сусіда, і називаємо її «активною множиною». Нижче ми додаємо зображення, які допоможуть уявити алгоритм.

manhattan-mst-sweep Чорним зі стрілкою показано напрямок замітальної прямої. Усі точки нижче цієї лінії — в активній множині, а точки вище ще не оброблено. Зеленим позначено точки, які лежать в октанті обробленої точки. Червоним — точки, які не лежать у шуканому октанті.
manhattan-mst-sweep На цьому зображенні ми бачимо активну множину після обробки точки pp. Зауважте, що 22 зелені точки з попереднього зображення мали pp у своєму північно-північно-східному октанті й більше не належать до активної множини, бо вже знайшли свого найближчого сусіда.

Коли ми додаємо нову точку pp, для кожної точки ss, яка має її у своєму октанті, ми можемо безпечно призначити pp найближчим сусідом. Це правда, бо їхня відстань дорівнює d(p,s)=xpxs+ypys=(xp+yp)(xs+ys)d(p,s) = |x_p - x_s| + |y_p - y_s| = (x_p + y_p) - (x_s + y_s), оскільки pp лежить у північно-північно-східному октанті. Оскільки всі наступні точки не матимуть меншого значення x+yx + y через крок сортування, pp гарантовано має найменшу відстань. Тоді ми можемо вилучити всі такі точки з активної множини, і нарешті додати pp до активної множини.

Наступне питання — як ефективно знайти, які точки ss мають pp у своєму північно-північно-східному октанті. Тобто які точки ss задовольняють:

  • xsxpx_s \leq x_p
  • xpyp<xsysx_p - y_p < x_s - y_s

Оскільки жодна точка в активній множині не лежить в області R1R_1 іншої, ми також маємо, що для двох точок q1q_1 і q2q_2 в активній множині xq1xq2x_{q_1} \neq x_{q_2}, і їхнє впорядкування означає xq1<xq2    xq1yq1xq2yq2x_{q_1} < x_{q_2} \implies x_{q_1} - y_{q_1} \leq x_{q_2} - y_{q_2}.

Ви можете спробувати уявити це на зображеннях вище, думаючи про впорядкування за xyx - y як про «замітальну пряму», яка йде з північного заходу на південний схід, тобто перпендикулярно до намальованої.

Це означає, що якщо ми тримаємо активну множину впорядкованою за xx, кандидати ss розташовані поспіль. Тоді ми можемо знайти найбільший xsxpx_s \leq x_p і обробляти точки в порядку спадання xx, доки не порушиться друга умова xpyp<xsysx_p - y_p < x_s - y_s (насправді ми можемо дозволити, щоб xpyp=xsysx_p - y_p = x_s - y_s, і це опрацьовує випадок точок з однаковими координатами). Зауважте, що оскільки ми вилучаємо з множини одразу після обробки, це даватиме амортизовану складність O(nlog(n))O(n \log(n)). Тепер, коли ми маємо найближчу точку в північно-східному напрямку, ми повертаємо точки й повторюємо. Можна показати, що насправді в такий спосіб ми також знаходимо найближчу точку в південно-західному напрямку, тож можна повторювати лише 4 рази замість 8.

Підсумовуючи, ми:

  • Сортуємо точки за x+yx + y у неспадному порядку;
  • Для кожної точки ми проходимо активну множину, починаючи з точки з найбільшим xx таким, що xxpx \leq x_p, і перериваємо цикл, якщо xpypxsysx_p - y_p \geq x_s - y_s. Для кожної допустимої точки ss ми додаємо ребро (s,p,d(s,p))(s,p, d(s,p)) до нашого списку;
  • Додаємо точку pp до активної множини;
  • Повертаємо точки й повторюємо, доки не пройдемо всі октанти.
  • Застосовуємо алгоритм Краскала до списку ребер, щоб отримати MST.

Нижче ви можете знайти реалізацію, засновану на тій, що з KACTL.

struct point {
long long x, y;
};

// Повертає список ребер у форматі (вага, u, v).
// Передача цього списку до алгоритму Краскала дасть мангеттенське MST.
vector<tuple<long long, int, int>> manhattan_mst_edges(vector<point> ps) {
vector<int> ids(ps.size());
iota(ids.begin(), ids.end(), 0);
vector<tuple<long long, int, int>> edges;
for (int rot = 0; rot < 4; rot++) { // для кожного повороту
sort(ids.begin(), ids.end(), [&](int i, int j){
return (ps[i].x + ps[i].y) < (ps[j].x + ps[j].y);
});
map<int, int, greater<int>> active; // (xs, id)
for (auto i : ids) {
for (auto it = active.lower_bound(ps[i].x); it != active.end();
active.erase(it++)) {
int j = it->second;
if (ps[i].x - ps[i].y > ps[j].x - ps[j].y) break;
assert(ps[i].x >= ps[j].x && ps[i].y >= ps[j].y);
edges.push_back({(ps[i].x - ps[j].x) + (ps[i].y - ps[j].y), i, j});
}
active[ps[i].x] = i;
}
for (auto &p : ps) { // поворот
if (rot & 1) p.x *= -1;
else swap(p.x, p.y);
}
}
return edges;
}

Задачі