Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

e-maxx_algo

.pdf
Скачиваний:
122
Добавлен:
03.06.2015
Размер:
6.19 Mб
Скачать

for (int i=0; i<n; ++i) if (!used[i])

dfs1 (i);

comp.assign (n, -1);

for (int i=0, j=0; i<n; ++i) { int v = order[n-i-1]; if (comp[v] == -1)

dfs2 (v, j++);

}

for (int i=0; i<n; ++i)

if (comp[i] == comp[i^1]) { puts ("NO SOLUTION"); return 0;

}

for (int i=0; i<n; ++i) {

int ans = comp[i] > comp[i^1] ? i : i^1; printf ("%d ", ans);

}

}

Heavy-light декомпозиция

Heavy-light декомпозиция — это достаточно общий приём, который позволяет эффективно решать многие задачи, сводящиеся к запросам на дереве.

Простейший пример задач такого вида — это следующая задача. Дано дерево, каждой вершине которого приписано какое-то число. Поступают запросы вида , где и — номера вершин дерева, и требуется

узнать максимальное число на пути между вершинами и .

Описание алгоритма

Итак, пусть дано дерево с вершинами, подвешенное за некоторый корень.

Суть этой декомпозиции в том, чтобы разбить дерево на несколько путей таким образом, чтобы для любой вершины получалось, что если мы будем подниматься от к корню, то по пути сменим не более путей. Кроме того, все пути должны не пересекаться друг с другом по рёбрам.

Понятно, что если мы научимся искать такую декомпозицию для любого дерева, это позволит свести любой запрос вида "узнать что-то на пути из в " к нескольким запросам вида "узнать что-то на отрезке -го пути".

Построение heavy-light декомпозиции

Посчитаем для каждой вершины размер её поддерева

(т.е. это количество вершин в поддереве вершины

, включая саму вершину).

 

 

Далее, рассмотрим все рёбра, ведущие к сыновьям какой-либо вершины . Назовём ребро

тяжёлым, если

оно ведёт в вершину такую, что:

 

 

 

 

 

 

 

 

Все остальные рёбра назовём лёгкими. Очевидно, что из одной вершины вниз может исходить максимум одно тяжёлое ребро (т.к. в противном случае у вершины было бы два сына размера , что с учётом

самой вершины даёт размер , т.е. пришли к противоречию).

Теперь построим саму декомпозицию дерева на непересекающиеся пути. Рассмотрим все вершины, из которых не выходит вниз ни одного тяжёлого ребра, и будем идти от каждой из них вверх, пока не дойдём до корня дерева или не пройдём лёгкое ребро. В результате мы получим несколько путей — покажем, что это и есть искомые пути heavylight декомпозиции.

Доказательство корректности алгоритма

Во-первых, заметим, что полученные алгоритмом пути будут непересекающимися. В самом деле, если бы два каких-то пути имели бы общее ребро, это бы означало, что из какой-то вершины исходит вниз два тяжёлых ребра, чего быть не может.

Во-вторых, покажем, что спускаясь от корня дерева до произвольной вершины, мы сменим по пути не

более

путей. В самом деле, проход вниз по лёгкому ребру уменьшает размер текущего поддерева более

чем вдвое:

 

 

 

Таким образом, мы не могли пройти более

лёгких рёбер. Однако переходить с одного пути на другой мы

можем только через лёгкое ребро (т.к. каждый путь, кроме заканчивающихся в корне, содержит лёгкое ребро в конце; а попасть сразу посередине пути мы не можем).

Следовательно, по пути от корня до любой вершины мы не можем сменить более

путей, что и

требовалось доказать.

 

Применения при решении задач

При решении задач иногда бывает удобнее рассматривать heavy-light как набор вершинно-непересекающихся путей (а не рёберо-непересекающихся). Для этого достаточно из каждого

пути исключить последнее ребро, если оно являются лёгким ребром — тогда никакие свойства не нарушатся, но

теперь каждая вершина будет принадлежать ровно одному пути.

Ниже мы рассмотрим несколько типичных задач, которые можно решать с помощью heavy-light декомпозиции.

Отдельно стоит обратить внимание на задачу сумма чисел на пути, поскольку это пример задачи, которая может быть решена и более простыми техниками.

Максимальное число на пути между двумя вершинами

Дано дерево, каждой вершине которого приписано какое-то число. Поступают запросы вида

, где и —

номера вершин дерева, и требуется узнать максимальное число на пути между вершинами

и .

Построим заранее heavy-light декомпозицию. Над каждым получившимся путём построим дерево отрезков для максимума, что позволит искать вершину с максимальным приписанным числом в указанном сегменте указанного пути

за

. Хотя число путей в heavy-light декомпозиции может достигать

, суммарный размер всех путей

есть величина , поэтому и суммарный размер деревьев отрезков также будет линейным.

Теперь, для того чтобы отвечать на поступивший запрос

найдём наименьшего общего предка

этих

вершин (например, методом двоичного подъёма). Теперь задача свелась к двум запросам:

и

, на каждый

из которых мы можем ответить таким образом: найдём, в каком пути лежит нижняя вершина, сделаем запрос к этому пути, перейдём в вершину-конец этого пути, снова определим, в каком мы пути оказались и сделаем запрос к нему, и так далее, пока не дойдём до пути, содержащего .

Аккуратно следует быть со случаем, когда, например, и оказались в одном пути — тогда запрос максимума к этому пути надо делать не на суффиксе, а на внутреннем подотрезке.

Таким образом, в процессе ответа на один подзапрос мы пройдём по путям, в каждом из них сделав запрос максимума на суффиксе или на префиксе/подотрезке (запрос на префиксе/подотрезке мог быть только один раз).

Так мы получили решение за на один запрос.

Если ещё дополнительно предпосчитать на каждом пути максимумы на всех суффиксах, то получится решение за — т.к. запрос максимума не на суффиксе случается только один раз, когда мы доходим до вершины .

Сумма чисел на пути между двумя вершинами

Дано дерево, каждой вершине которого приписано какое-то число. Поступают запросы вида , где и

номера вершин дерева, и требуется узнать сумму чисел на пути между вершинами и . Возможен вариант этой задачи, когда дополнительно бывают запросы изменения числа, приписанного той или иной вершине.

Хотя эту задачу можно решать с помощью heavy-light декомпозиции, построив над каждым путём дерево отрезков для суммы (или просто предпосчитав частичные суммы, если в задаче отсутствуют запросы изменения), эта задача может быть решена более простыми техниками.

Если запросы модификации отсутствуют, то узнавать сумму на пути между двумя вершинами можно параллельно с поиском LCA двух вершин в алгоритме двоичного подъёма — для этого достаточно во время препроцессинга для

LCA подсчитывать не только -ых предков каждой вершины, но и сумму чисел на пути до этого предка.

Есть и принципиально другой подход к этой задаче — рассмотреть эйлеров обход дерева, и построить дерево отрезков над ним. Этот алгоритм рассматривается в статье с решением похожей задачи. (А если запросы

модификации отсутствуют — то достаточно обойтись предпосчётом частичных сумм, без дерева отрезков.) Оба этих способа дают относительно простые решения с асимптотикой на один запрос.

Перекраска рёбер пути между двумя вершинами

Дано дерево, каждое ребро изначально покрашено в белый цвет.

Поступают запросы вида

, где и —

номера вершин, — цвет, что означает, что все рёбра на пути из

в надо перекрасить в цвет

. Требуется после

всех перекрашиваний сообщить, сколько в итоге получилось рёбер каждого цвета.

 

Решение — просто сделать дерево отрезков с покраской на отрезке над набором путей heavy-light декомпозиции.

Каждый запрос перекраски на пути

превратится в два подзапроса

и

, где — наименьший

общий предок вершин и (найденный, например, алгоритмом двоичного подъёма), а каждый из этих подзапросов — в запросов к деревьям отрезков над путями.

Итого получается решение с асимптотикой на один запрос.

Задачи в online judges

Список задач, которые можно решить, используя heavy-light декомпозицию:

TIMUS #1553 "Caves and Tunnels" [сложность: средняя]

IPSC 2009 L "Let there be rainbows!"

[сложность: средняя]

SPOJ #2798 "Query on a tree again!"

[сложность: средняя]

Codeforces Beta Round #88 E "Дерево или не дерево" [сложность: высокая]

Длина объединения отрезков на прямой за O (N log N)

Даны N отрезков на прямой, т.е. каждый отрезок задаётся парой координат (X1, X2). Рассмотрим объединение этих отрезков и найдём его длину.

Алгоритм был предложен Кли (Klee) в 1977 году. Алгоритм работает за O (N log N). Было доказано, что этот алгоритм является быстрейшим (асимптотически).

Описание

Положим все координаты концов отрезков в массив X и отсортируем его по значению координаты.

Дополнительное условие при сортировке - при равенстве координат первыми должны идти левые концы. Кроме того, для каждого элемента массива будем хранить, относится он к левому или к правому концу отрезка. Теперь пройдёмся по всему массиву, имея счётчик C перекрывающихся отрезков. Если C отлично от нуля, то к результату добавляем разницу Xi - Xi-1. Если текущий элемент относится к левому концу, то увеличиваем счётчик C, иначе уменьшаем его.

Реализация

unsigned segments_union_measure (const vector <pair <int,int> > & a)

{

unsigned n = a.size();

vector <pair <int,bool> > x (n*2); for (unsigned i=0; i<n; i++)

{

x[i*2] = make_pair (a[i].first, false); x[i*2+1] = make_pair (a[i].second, true);

}

sort (x.begin(), x.end());

unsigned result = 0; unsigned c = 0;

for (unsigned i=0; i<n*2; i++)

{

if (c && i)

result += unsigned (x[i].first - x[i-1].first); if (x[i].second)

++c;

else

--c;

}

return result;

}

Знаковая площадь треугольника и предикат "По часовой стрелке"

Определение

Пусть даны три точки , , . Найдём значение знаковой площади треугольника , т.е. площади этого треугольника, взятой со знаком плюс или минус в зависимости от типа поворота, образуемого точками , , : против часовой стрелки или по ней соответственно.

Понятно, что, если мы научимся вычислять такую знаковую ("ориентированную") площадь, то сможем и находить обычную площадь любого треугольника, а также сможем проверять, по часовой стрелке или против направлена какаялибо тройка точек.

Вычисление

Воспользуемся понятием косого (псевдоскалярного) произведения векторов. Оно как раз равно удвоенной знаковой площади треугольника:

где угол берётся ориентированным, т.е. это угол вращения между этими векторами против часовой стрелки. (Модуль косого произведения двух векторов равен модулю векторного произведения их.)

Косое произведение вычисляется как величина определителя, составленного из координат точек:

Раскрывая определитель, можно получить такую формулу:

Можно сгруппировать третье слагаемое с первыми двумя, избавившись от одного умножения:

Последнюю формулу удобно записывать и запоминать в матричном виде, как следующий определитель:

Реализация

Функция, вычисляющая удвоенную знаковую площадь треугольника:

int triangle_area_2 (int x1, int y1, int x2, int y2, int x3, int y3) { return (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1);

}

Функция, возвращающая обычную площадь треугольника:

double triangle_area (int x1, int y1, int x2, int y2, int x3, int y3) { return abs (triangle_area_2 (x1, y1, x2, y2, x3, y3)) / 2.0;

}

Функция, проверяющая, образует ли указанная тройка точек поворот по часовой стрелке:

bool clockwise (int x1, int y1, int x2, int y2, int x3, int y3) { return triangle_area_2 (x1, y1, x2, y2, x3, y3) < 0;

}

Функция, проверяющая, образует ли указанная тройка точек поворот против часовой стрелки:

bool counter_clockwise (int x1, int y1, int x2, int y2, int x3, int y3) { return triangle_area_2 (x1, y1, x2, y2, x3, y3) > 0;

}

Проверка двух отрезков на пересечение

Даны два отрезка и (они могут вырождаться в точки). Требуется проверить, пересекаются они или нет. Если дополнительно требуется найти саму точку (точки) пересечения, то см. соответствующую статью.

Первый способ: ориентированная площадь треугольника

Воспользуемся Ориентированной площадью треугольника и предикат 'По часовой стрелке'. Действительно, чтобы

отрезки

и

пересекались, необходимо и достаточно, чтобы точки

и находились по разные стороны

прямой

, и, аналогично, точки и

— по разные стороны прямой

. Проверить это можно,

вычисляя ориентированные площади соответствующих треугольников и сравнивая их знаки.

Единственное, на что следует обратить внимание — граничные случаи, когда какие-то точки попадают на саму прямую. При этом возникает единственный особый случай, когда вышеописанные проверки ничего не дадут — случай, когда оба отрезка лежат на одной прямой. Этот случай надо рассмотреть отдельно. Для этого достаточно проверить, что проекции этих двух отрезков на оси и пересекаются (часто эту проверку называют "проверкой на bounding box").

В целом, этот способ — более простой, чем тот, что будет приведён ниже (производящий пересечение двух прямых), и имеет меньше особых случаев, однако главный его недостаток — в том, что он не находит саму точку пересечения.

Реализация:

struct pt {

int x, y;

};

inline int area (pt a, pt b, pt c) {

return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);

}

inline bool intersect_1 (int a, int b, int c, int d) { if (a > b) swap (a, b);

if (c > d) swap (c, d); return max(a,c) <= min(b,d);

}

bool intersect (pt a, pt b, pt c, pt d) { return intersect_1 (a.x, b.x, c.x, d.x)

&&intersect_1 (a.y, b.y, c.y, d.y)

&&area(a,b,c) * area(a,b,d) <= 0

&&area(c,d,a) * area(c,d,b) <= 0;

}

В целях оптимизации проверка на bounding box вынесена в начало, до вычисления площадей — поскольку это более "лёгкая" проверка.

Само собой, этот код применим и для случая вещественных координат, просто все сравнения с нулём

,

следует производить по эпсилону (и избегать перемножения двух вещественнозначных значений

перемножая вместо этого их знаки).

 

Второй способ: пересечение двух прямых

Вместо пересечения отрезков выполним пересечение двух прямых, в результате, если прямые не параллельны,

получим какую-то точку, которую надо проверить на принадлежность обоим отрезкам; для этого достаточно проверить, что эта точка принадлежит обоим отрезкам в проекции на ось и на ось .

Если же прямые оказались параллельными, то, если они не совпадают, то отрезки точно не пересекаются. Если же прямые совпали, то отрезки лежат на одной прямой, и для проверки их пересечения достаточно проверить, что пересекаются их проекции на ось и .

Остаётся ещё особый случай, когда один или оба отрезка вырождаются в точки: в таком случае говорить о прямых некорректно, и этот метод будет неприменим (этот случай надо будет разбирать отдельно).

Реализация (без учёта случая вырожденных отрезков):

struct pt {

int x, y;

};

const double EPS = 1E-9;

inline int det (int a, int b, int c, int d) { return a * d - b * c;

}

inline bool between (int a, int b, double c) {

return min(a,b) <= c + EPS && c <= max(a,b) + EPS;

}

inline bool intersect_1 (int a, int b, int c, int d) { if (a > b) swap (a, b);

if (c > d) swap (c, d); return max(a,c) <= min(b,d);

}

bool intersect (pt a, pt b, pt c, pt d) {

int A1 = a.y-b.y, B1 = b.x-a.x, C1 = -A1*a.x - B1*a.y; int A2 = c.y-d.y, B2 = d.x-c.x, C2 = -A2*c.x - B2*c.y; int zn = det (A1, B1, A2, B2);

if (zn != 0) {

double x = - det (C1, B1, C2, B2) * 1. / zn; double y = - det (A1, C1, A2, C2) * 1. / zn;

return between (a.x, b.x, x) && between (a.y, b.y, y)

&& between (c.x, d.x, x) && between (c.y, d.y, y);

}

else

return det (A1, C1, A2, C2) == 0 && det (B1, C1, B2, C2) == 0

&&intersect_1 (a.x, b.x, c.x, d.x)

&&intersect_1 (a.y, b.y, c.y, d.y);

}

Здесь сначала вычисляется коэффициент — знаменатель в формуле Крамера. Если , то коэффициенты и прямых пропорциональны, и прямые параллельны или совпадают. В этом случае надо проверить, совпадают они или нет, для чего надо проверить, что коэффициенты прямых пропорциональны с тем же коэффициентом, для чего достаточно вычислить два следующих определителя, если они оба равны нулю, то прямые совпадают:

 

 

 

Если же

, то прямые пересекаются, и по формуле Крамера находим точку пересечения

и проверяем

её принадлежность обоим отрезкам.

 

Следует отметить, что если исходные координаты точек уже были вещественнозначными, то следует нормировать прямые (т.е. привести их к такому состоянию, что сумма квадратов коэффициентов и равна единице), иначе погрешности при сравнении прямых на параллельность и на совпадение могут оказаться слишком большими.

Нахождение уравнения прямой для отрезка

Задача — по заданным координатам конца отрезка построить прямую, проходящую через него.

Мы считаем, что отрезок невырожден, т.е. имеет длину больше нуля (иначе, понятно, через него проходит бесконечно много различных прямых).

Двумерный случай

Пусть дан отрезок , т.е. известны координаты его концов , , , .

Требуется построить уравнение прямой на плоскости, проходящей через этот отрезок, т.е. найти коэффициенты , , в уравнении прямой:

Заметим, что искомых троек , проходящих через заданный отрезок, бесконечно много:

можно умножить все три коэффициента на произвольное ненулевое число и получить ту же самую прямую. Следовательно, наша задача — найти одну из таких троек.

Нетрудно убедиться (подстановкой этих выражений и координат точек и в уравнение прямой), что подходит следующий набор коэффициентов:

Целочисленный случай

Важным преимуществом такого способа построения прямой является то, что если координаты концов были целочисленными, то и полученные коэффициенты также будут целочисленными. В некоторых случаях это позволяет производить геометрические операции, вообще не прибегая к вещественным числам.

Однако есть и небольшой недостаток: для одной и той же прямой могут получаться разные тройки коэффициентов. Чтобы избежать этого, но не уходить от целочисленных коэффициентов, можно применить следующий приём,

часто называемый нормированием. Найдём наибольший общий делитель чисел

,

,

, поделим на

него все три коэффициента, а затем произведём нормировку знака: если

или

 

 

, то умножим

все три коэффициента на . В итоге мы придём к тому, что для одинаковых прямых будут получаться одинаковые тройки коэффициентов, что позволит легко проверять прямые на равенство.

Вещественнозначный случай

При работе с вещественными числами следует всегда помнить о погрешностях.

Коэффициенты и получаются у нас порядка исходных координат, коэффициент — уже порядка квадрата от них. Это уже может быть достаточно большими числами, а, например, при пересечении прямых они станут ещё

больше, что может привести к большим ошибкам округления уже при исходных координатах порядка .

Поэтому при работе с вещественными числами желательно производить так называемую нормировку прямой: а именно, делать коэффициенты такими, чтобы . Для этого надо вычислить число :

и разделить все три коэффициента , , на него.

Тем самым, порядок коэффициентов и уже не будет зависеть от порядка входных координат, а коэффициент будет того же порядка, что и входные координаты. На практике это приводит к значительному улучшению

точности вычислений.

Наконец, упомянем о сравнении прямых — ведь после такой нормировки для одной и той же прямой

могут получаться только две тройки коэффициентов: с точностью до умножения на

. Соответственно, если

мы произведём дополнительную нормировку с учётом знака (если

или

, то умножать

на

), то получающиеся коэффициенты будут уникальными.

 

 

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]