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

e-maxx_algo

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

Наименьший общий предок. Нахождение за O (log N) (метод двоичного подъёма)

Пусть дано дерево G. На вход поступают запросы вида (V1, V2), для каждого запроса требуется найти их наименьшего общего предка, т.е. вершину V, которая лежит на пути от корня до V1, на пути от корня до V2, и из всех таких вершин следует выбирать самую нижнюю. Иными словами, искомая вершина V - предок и V1, и V2, и среди всех таких общих предков выбирается нижний. Очевидно, что наименьший общий предок вершин V1 и V2 - это их общий предок, лежащий на кратчайшем пути из V1 в V2. В частности, например, если V1 является предком V2, то V1 является их наименьшим общим предком.

На английском эта задача называется задачей LCA - Least Common Ancestor.

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

Асимптотика полученного алгоритма будет равна: препроцессинг за O (N log N) и ответ на каждый запрос за O (log N).

Алгоритм

Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Обозначим этот массив через P, т.е. P[i][j] -

это 2j-й предок вершины i, i = 1..N, j = 0..•logN•. Также для каждой вершины найдём времена захода в неё и выхода поиска в глубину (см. "Поиск в глубину") - это нам понадобится, чтобы определять за O (1), является ли одна вершина

предком другой (не обязательно непосредственным). Такой препроцессинг можно выполнить за O (N log N).

Пусть теперь поступил очередной запрос - пара вершин (A,B). Сразу проверим, не является ли одна вершина предком другой - в таком случае она и является результатом. Если A не предок B, и B не предок A, то будем подниматься по предкам A, пока не найдём самую высокую (т.е. наиболее близкую к корню) вершину, которая ещё

не является предком (не обязательно непосредственным) B (т.е. такую вершину X, что X не предок B, а P[X][0] - предок B). При этом находить эту вершину X будем за O (log N), пользуясь массивом P.

Опишем этот процесс подробнее. Пусть L = •logN•. Пусть сначала I = L. Если P[A][I] не является предком B, то присваиваем A = P[A][I], и уменьшаем I. Если же P[A][I] является предком B, то просто уменьшаем I. Очевидно, что когда I станет меньше нуля, вершина A как раз и будет являться искомой вершиной - т.е. такой, что A не предок B, но P[A][0]

- предок B.

Теперь, очевидно, ответом на LCA будет являться P[A][0] - т.е. наименьшая вершина среди предков исходной вершины A, являющаяся также и предком B.

Асимптотика. Весь алгоритм ответа на запрос состоит из изменения I от L = •logN• до 0, а также проверки на каждом шаге за O(1), является ли одна вершина предком другой. Следовательно, на каждый запрос будет найден ответ за O (log N).

Реализация

int n, l;

vector < vector<int> > g; vector<int> tin, tout; int timer;

vector < vector<int> > up;

void dfs (int v, int p = 0) { tin[v] = ++timer; up[v][0] = p;

for (int i=1; i<=l; ++i)

up[v][i] = up[up[v][i-1]][i-1]; for (size_t i=0; i<g[v].size(); ++i) {

int to = g[v][i]; if (to != p)

dfs (to, v);

}

tout[v] = ++timer;

}

bool upper (int a, int b) {

return tin[a] <= tin[b] && tout[a] >= tout[b];

}

int lca (int a, int b) {

if (upper (a, b)) return a; if (upper (b, a)) return b; for (int i=l; i>=0; --i)

if (! upper (up[a][i], b)) a = up[a][i];

return up[a][0];

}

int main() {

... чтение n и g ...

tin.resize (n), tout.resize (n), up.resize (n); l = 1;

while ((1<<l) <= n) ++l;

for (int i=0; i<n; ++i) up[i].resize (l+1); dfs (0);

for (;;) {

int a, b; // текущий запрос

int res = lca (a, b); // ответ на запрос

}

}

Наименьший общий предок. Нахождение за O (1) с препроцессингом O (N) (алгоритм Фарах-Колтона и Бендера)

Пусть дано дерево G. На вход поступают запросы вида (V1, V2), для каждого запроса требуется найти их наименьшего общего предка, т.е. вершину V, которая лежит на пути от корня до V1, на пути от корня до V2, и из всех таких вершин следует выбирать самую нижнюю. Иными словами, искомая вершина V - предок и V1, и V2, и среди всех таких общих предков выбирается нижний. Очевидно, что наименьший общий предок вершин V1 и V2 - это их общий предок, лежащий на кратчайшем пути из V1 в V2. В частности, например, если V1 является предком V2, то V1 является их наименьшим общим предком.

На английском эта задача называется задачей LCA - Least Common Ancestor.

Описываемый здесь алгоритм Фарах-Колтона и Бендера (Farach-Colton, Bender) является асимптотически оптимальным, и при этом сравнительно простым (по сравнению с другими алгоритмами, например, Шибера-Вишкина).

Алгоритм

Воспользуемся классическим сведением задачи LCA к задаче RMQ (минимум на отрезке) (более подробно см. Наименьший общий предок. Нахождение за O (sqrt (N)) и O (log N) с препроцессингом O (N)). Научимся теперь

решать задачу RMQ в данном частном случае с препроцессингом O (N) и O (1) на запрос.

Заметим, что задача RMQ, к которой мы свели задачу LCA, является весьма специфичной: любые два соседних элемента в массиве отличаются ровно на единицу (поскольку элементы массива - это не что иное как высоты вершин, посещаемых в порядке обхода, и мы либо идём в потомка, тогда следующий элемент будет на 1 больше, либо идём в предка, тогда следующий элемент будет на 1 меньше). Собственно алгоритм Фарах-Колтона и Бендера как раз и представляет собой решение такой задачи RMQ.

Обозначим через A массив, над которым выполняются запросы RMQ, а N - размер этого массива.

Построим сначала алгоритм, решающий эту задачу с препроцессингом O (N log N) и O (1) на запрос. Это сделать легко: создадим так называемую Sparse Table T[l,i], где каждый элемент T[l,i] равен минимуму A

на промежутке [l; l+2i). Очевидно, 0 <= i <= •log N•, и потому размер Sparse Table будет O (N log N). Построить её

также легко за O (N log N), если заметить, что T[l,i] = min (T[l,i-1], T[l+2i-1,i-1]). Как теперь отвечать на каждый запрос RMQ за O (1)? Пусть поступил запрос (l,r), тогда ответом будет min (T[l,sz], T[r-2sz+1,sz]), где sz - наибольшая степень двойки,

не превосходящая r-l+1. Действительно, мы как бы берём отрезок (l,r) и покрываем его двумя отрезками длины 2sz - один начинающийся в l, а другой заканчивающийся в r (причём эти отрезки перекрываются, что в данном случае нам нисколько не мешает). Чтобы действительно достигнуть асимптотики O (1) на запрос, мы должны предпосчитать значения sz для всех возможных длин от 1 до N.

Теперь опишем, как улучшить этот алгоритм до асимптотики O (N).

Разобьём массив A на блоки размером K = 0.5 log2 N. Для каждого блока посчитаем минимальный элемент в нём и

его позицию (поскольку для решения задачи LCA нам важны не сами минимумы, а их позиции). Пусть B - это массив размером N / K, составленный из этих минимумов в каждом блоке. Построим по массиву B Sparse Table, как описано выше, при этом размер Sparse Table и время её построения будут равны:

N/K log N/K = (2N / log N) log (2N / log N) =

= (2N / log N) (1 + log (N / log N)) <= 2N / log N + 2N = O (N)

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

(не включительно), и наконец минимум в блоке r, от начала блока до r. На запрос "минимум в блоках" мы уже можем отвечать за O (1) с помощью Sparse Table, остались только запросы RMQ внутри блоков.

Здесь мы воспользуемся "+-1 свойством". Заметим, что, если внутри каждого блока от каждого его элемента отнять первый элемент, то все блоки будут однозначно определяться последовательностью длины K-1, состоящей из чисел +- 1. Следовательно, количество различных блоков будет равно:

2K-1 = 20.5 log N - 1 = 0.5 sqrt(N)

Итак, количество различных блоков будет O (sqrt (N)), и потому мы можем предпосчитать результаты RMQ внутри

всех различных блоков за O (sqrt(N) K2) = O (sqrt(N) log2 N) = O (N). С точки зрения реализации, мы можем каждый блок характеризовать битовой маской длины K-1 (которая, очевидно, поместится в стандартный тип int), и

хранить предпосчитанные RMQ в некотором массиве R[mask,l,r] размера O (sqrt(N) log2 N).

Итак, мы научились предпосчитывать результаты RMQ внутри каждого блока, а также RMQ над самими блоками, всё

в сумме за O (N), а отвечать на каждый запрос RMQ за O (1) - пользуясь только предвычисленными значениями, в худшем случае четырьмя: в блоке l, в блоке r, и на блоках между l и r не включительно.

Реализация

В начале программы указаны константы MAXN, LOG_MAXLIST и SQRT_MAXLIST, определяющие максимальное число вершин в графе, которые при необходимости надо увеличить.

const int MAXN = 100*1000; const int MAXLIST = MAXN * 2; const int LOG_MAXLIST = 18; const int SQRT_MAXLIST = 447;

const int MAXBLOCKS = MAXLIST / ((LOG_MAXLIST+1)/2) + 1;

int n, root; vector<int> g[MAXN];

int h[MAXN]; // vertex height vector<int> a; // dfs list

int a_pos[MAXN]; // positions in dfs list int block; // block size = 0.5 log A.size()

int bt[MAXBLOCKS][LOG_MAXLIST+1]; // sparse table on blocks (relative minimum positions in blocks)

int bhash[MAXBLOCKS]; // block hashes

int brmq[SQRT_MAXLIST][LOG_MAXLIST/2][LOG_MAXLIST/2]; // rmq inside each block, indexed by block hash

int log2[2*MAXN]; // precalced logarithms (floored values)

// walk graph

void dfs (int v, int curh) { h[v] = curh;

a_pos[v] = (int)a.size(); a.push_back (v);

for (size_t i=0; i<g[v].size(); ++i) if (h[g[v][i]] == -1) {

dfs (g[v][i], curh+1); a.push_back (v);

}

}

int log (int n) { int res = 1;

while (1<<res < n) ++res; return res;

}

//compares two indices in a inline int min_h (int i, int j) {

return h[a[i]] < h[a[j]] ? i : j;

}

//O(N) preprocessing

void build_lca() {

int sz = (int)a.size(); block = (log(sz) + 1) / 2;

int blocks = sz / block + (sz % block ? 1 : 0);

// precalc in each block and build sparse table memset (bt, 255, sizeof bt);

for (int i=0, bl=0, j=0; i<sz; ++i, ++j) { if (j == block)

j = 0, ++bl;

if (bt[bl][0] == -1 || min_h (i, bt[bl][0]) == i) bt[bl][0] = i;

}

for (int j=1; j<=log(sz); ++j)

for (int i=0; i<blocks; ++i) { int ni = i + (1<<(j-1)); if (ni >= blocks)

bt[i][j] = bt[i][j-1];

else

bt[i][j] = min_h (bt[i][j-1], bt[ni][j-1]);

}

// calc hashes of blocks

memset (bhash, 0, sizeof bhash);

for (int i=0, bl=0, j=0; i<sz||j<block; ++i, ++j) { if (j == block)

j = 0, ++bl;

if (j > 0 && (i >= sz || min_h (i-1, i) == i-1)) bhash[bl] += 1<<(j-1);

}

// precalc RMQ inside each unique block memset (brmq, 255, sizeof brmq);

for (int i=0; i<blocks; ++i) { int id = bhash[i];

if (brmq[id][0][0] != -1) continue; for (int l=0; l<block; ++l) {

brmq[id][l][l] = l;

for (int r=l+1; r<block; ++r) { brmq[id][l][r] = brmq[id][l][r-1]; if (i*block+r < sz)

brmq[id][l][r] =

min_h (i*block+brmq[id][l]

[r], i*block+r) - i*block;

}

}

}

// precalc logarithms

for (int i=0, j=0; i<sz; ++i) { if (1<<(j+1) <= i) ++j; log2[i] = j;

}

}

//answers RMQ in block #bl [l;r] in O(1) inline int lca_in_block (int bl, int l, int r) {

return brmq[bhash[bl]][l][r] + bl*block;

}

//answers LCA in O(1)

int lca (int v1, int v2) {

int l = a_pos[v1], r = a_pos[v2]; if (l > r) swap (l, r);

int bl = l/block, br = r/block; if (bl == br)

return a[lca_in_block(bl,l%block,r%block)]; int ans1 = lca_in_block(bl,l%block,block-1);

int ans2 = lca_in_block(br,0,r%block); int ans = min_h (ans1, ans2);

if (bl < br - 1) {

int pw2 = log2[br-bl-1]; int ans3 = bt[bl+1][pw2];

int ans4 = bt[br-(1<<pw2)][pw2];

ans = min_h (ans, min_h (ans3, ans4));

}

return a[ans];

}

Задача RMQ (Range Minimum Query - минимум на отрезке). Решение за O (1) с препроцессингом

O (N)

Дан массив A[1..N]. Поступают запросы вида (L, R), на каждый запрос требуется найти минимум в массиве A, начиная с позиции L и заканчивая позицией R. Массив A изменяться в процессе работы не может, т.е. здесь описано решение статической задачи RMQ.

Здесь описано асимтпотически оптимальное решение. Оно несколько стоит особняком от других алгоритмов решения RMQ, поскольку оно сильно отличается от них: оно сводит задачу RMQ к задаче LCA, а затем использует алгоритм Фарах-Колтона и Бендера, который сводит задачу LCA обратно к RMQ (но уже частного вида)

и решает её.

Алгоритм

Построим по массиву A декартово дерево, где у каждой вершины ключом будет позиция i, а приоритетом - само число A [i] (предполагается, что в декартовом дереве приоритеты упорядочены от меньшего в корне к большим). Такое

дерево можно построить за O (N). Тогда запрос RMQ(l,r) эквивалентен запросу LCA(l',r'), где l' - вершина, соответствующая элементу A[l], r' - соответствующая A[r]. Действительно, LCA найдёт вершину, которая по

ключу находится между l' и r', т.е. по позиции в массиве A будет между l и r, и при этом вершину, наиболее близкую к корню, т.е. с наименьшим приоритетом, т.е. наименьшим значением.

Задачу LCA мы можем решать за O (1) с препроцессингом O (N) с помощью алгоритма Фарах-Колтона и Бендера, который, что интересно, сводит задачу LCA обратно к задаче RMQ, но уже частного вида.

Наименьший общий предок. Нахождение за в оффлайн (алгоритм Тарьяна)

Дано дерево

 

с вершинами и дано

запросов вида

. Для каждого запроса

требуется

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

 

и , т.е. такую вершину , которая наиболее удалена от корня дерева,

и при этом является предком обеих вершин

и .

 

 

Мы рассматриваем задачу в режиме оффлайн, т.е. считая, что все запросы известны заранее. Описываемый

ниже алгоритм позволяет ответить на все

 

запросов за суммарное время

, т.е. при достаточно

большом

за

на запрос.

 

 

 

 

Алгоритм Тарьяна

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

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

, а вершина уже была посещена, или наоборот.

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

для какого-то запроса

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

этих двух вершин.

 

 

Заметим, что

 

является либо самой вершиной , либо одним из её предков. Получается, нам надо

найти самую нижнюю вершину среди предков (включая её саму), для которой вершина

является потомком.

Заметим, что при фиксированном по такому признаку (т.е. какой наименьший предок

является и предком какой-

то вершины) вершины дерева дерева распадаются на совокупность непересекающихся классов. Для каждого

предка

вершины

её класс содержит саму эту вершину, а также все поддеревья с корнями в тех её

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

Нам надо научиться эффективно поддерживать все эти классы, для чего мы и применим структуру данных "Система непересекающихся множеств". Каждому классу будет соответствовать в этой структуре множество, причём

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

Рассмотрим подробно реализацию обхода в глубину. Пусть мы стоим в некоторой вершине

. Поместим её в

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

. Как обычно в обходе в

глубину, перебираем все исходящие рёбра

. Для каждого такого

мы сначала должны вызвать обход в

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

реализуется операцией

структуры данных "система непересекающихся множеств", с последующей

установкой

для представителя множества (т.к. после объединения представитель класса

мог измениться). Наконец, после обработки всех рёбер мы перебираем все запросы вида

, и если была

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

 

вершина

 

. Нетрудно заметить, что для каждого запроса

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

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

в сумме для всех разумных затрачивают

операций. В-третьих, это для каждого запроса проверка условия

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

выполняется за

. Итоговая асимптотика получается

, что означает для достаточно больших

() ответ за на один запрос.

Реализация

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

const int MAXN = максимальное число вершин в графе; vector<int> g[MAXN], q[MAXN]; // граф и все запросы int dsu[MAXN], ancestor[MAXN];

bool u[MAXN];

int dsu_get (int v) {

return v == dsu[v] ? v : dsu[v] = dsu_get (dsu[v]);

}

void dsu_unite (int a, int b, int new_ancestor) { a = dsu_get (a), b = dsu_get (b);

if (rand() & 1) swap (a, b);

dsu[a] = b, ancestor[b] = new_ancestor;

}

void dfs (int v) {

dsu[v] = v, ancestor[v] = v; u[v] = true;

for (size_t i=0; i<g[v].size(); ++i) if (!u[g[v][i]]) {

dfs (g[v][i]);

dsu_unite (v, g[v][i], v);

}

for (size_t i=0; i<q[v].size(); ++i) if (u[q[v][i]]) {

printf ("%d %d -> %d\n", v+1, q[v][i]+1, ancestor[ dsu_get(q[v][i]) ]+1);

}

int main() {

... чтение графа ...

//чтение запросов for (;;) {

int a, b = ...; // очередной запрос

--a, --b; q[a].push_back (b); q[b].push_back (a);

}

//обход в глубину и ответ на запросы

dfs (0);

}

Максимальный поток методом Эдмондса-Карпа за O (N M2)

Пусть дан граф G, в котором выделены две вершины: исток S и сток T, а у каждого ребра определена

пропускная способность Cu,v. Поток F можно представить как поток вещества, которое могло бы пройти по сети от истока к стоку, если рассматривать граф как сеть труб с некоторыми пропускными способностями. Т.е. поток - функция Fu,

v, определённая на множестве рёбер графа.

Задача заключается в нахождении максимального потока. Здесь будет рассмотрен метод Эдмондса-Карпа, работающий за O (N M2), или (другая оценка) O (F M), где F - величина искомого потока. Алгоритм был предложен в 1972 году.

Алгоритм

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

Общая схема алгоритма Эдмондса-Карпа такова. Сначала полагаем поток равным нулю. Затем ищем дополняющий путь, т.е. простой путь из S в T по тем рёбрам, у которых остаточная пропускная способность

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

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

в следующем: для каждого ребра (u, v) дополняющего пути выполним: Fu, v += C, а Fv, u = - Fu, v (или, что то же самое, Fv, u

-= C).

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

Реализация

const int inf = 1000*1000*1000;

typedef vector<int> graf_line; typedef vector<graf_line> graf;

typedef vector<int> vint; typedef vector<vint> vvint;

int main()

{

int n; cin >> n;

vvint c (n, vint(n)); for (int i=0; i<n; i++)

for (int j=0; j<n; j++) cin >> c[i][j];

// исток - вершина 0, сток - вершина n-1

vvint f (n, vint(n)); for (;;)

{

vint from (n, -1); vint q (n);

int h=0, t=0;

q[t++] = 0; from[0] = 0;

for (int cur; h<t;)

{

cur = q[h++];

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

if (from[v] == -1 && c[cur][v]-f[cur][v] > 0)

{

q[t++] = v; from[v] = cur;

}

}

if (from[n-1] == -1) break;

int cf = inf;

for (int cur=n-1; cur!=0; )

{

int prev = from[cur];

cf = min (cf, c[prev][cur]-f[prev][cur]); cur = prev;

}

for (int cur=n-1; cur!=0; )

{

int prev = from[cur]; f[prev][cur] += cf; f[cur][prev] -= cf; cur = prev;

}

}

int flow = 0;

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

flow += f[0][i];

cout << flow;

}

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