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

Алгоритмы C++

.pdf
Скачиваний:
686
Добавлен:
15.03.2016
Размер:
6 Mб
Скачать

Однако нетрудно убедиться в том, что все такие числа j получаются из i последовательными заменами самого

правого (самого младшего) нуля в двоичном представлении. Например, для i = 10 мы получим, что j = 11, 15, 31, 63 и т.д. Как ни странно, такой операции (замена самого младшего нуля на единицу) также соответствует очень простая формула:

H(X) = X | (X+1),

где | - это операция побитового логического "ИЛИ".

Реализация дерева Фенвика для суммы для одномерного случая

vector<int> t; int n;

void init (int nn)

{

n = nn; t.assign (n, 0);

}

int sum (int r)

{

int result = 0;

for (; r >= 0; r = (r & (r+1)) - 1) result += t[r];

return result;

}

void inc (int i, int delta)

{

for (; i < n; i = (i | (i+1))) t[i] += delta;

}

int sum (int l, int r)

{

return sum (r) - sum (l-1);

}

void init (vector<int> a)

{

init ((int) a.size());

for (unsigned i = 0; i < a.size(); i++) inc (i, a[i]);

}

Реализация дерева Фенвика для минимума для одномерного случая

Следует сразу заметить, что, поскольку дерево Фенвика позволяет найти значение функции в произвольном отрезке [0; R], то мы никак не сможем найти минимум на отрезке [L;R], где L > 0. Далее, все изменения значений должны происходить только в сторону уменьшения (опять же, поскольку никак не получится обратить функцию min).

Это значительные ограничения.

vector<int> t; int n;

const int INF = 1000*1000*1000;

void init (int nn)

{

n = nn;

t.assign (n, INF);

}

int getmin (int r)

{

int result = INF;

for (; r >= 0; r = (r & (r+1)) - 1) result = min (result, t[r]);

return result;

}

void update (int i, int new_val)

{

for (; i < n; i = (i | (i+1))) t[i] = min (t[i], new_val);

}

void init (vector<int> a)

{

init ((int) a.size());

for (unsigned i = 0; i < a.size(); i++) update (i, a[i]);

}

Реализация дерева Фенвика для суммы для двумерного случая

Как уже отмечалось, дерево Фенвика легко обобщается на многомерный случай.

vector <vector <int> > t; int n, m;

int sum (int x, int y)

{

int result = 0;

for (int i = x; i >= 0; i = (i & (i+1)) - 1)

for (int j = y; j >= 0; j = (j & (j+1)) - 1) result += t[i][j];

return result;

}

void inc (int x, int y, int delta)

{

for (int i = x; i < n; i = (i | (i+1)))

for (int j = y; j < m; j = (j | (j+1))) t[i][j] += delta;

}

Система непересекающихся множеств

Система непересекающихся множеств (disjoint set union, DSU) - это структура данных, которая может хранить несколько элементов, разделённых на несколько множеств (каждый элемент принадлежит ровно одному множеству, отсюда и название структуры). Каждое множество характеризуется своим представителем - одним из его элементов. Структура данных поддерживает следующие операции:

MakeSet (X)

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

За O(1)

FindSet (X)

Ищет множество, которому принадлежит элемент X, и возвращает его представителя. За О(1) - амортизированная оценка

Union (X, Y)

Ищет множества, к которым принадлежат элементы X и Y, и объединяет их в одно множество. Возвращает элемент, который становится представителем нового множества.

За O(1) - амортизированная оценка

Большинство фактов об этой структуре данных были установлены Таржаном (Tarjan) приблизительно в 1975 г.

Применение

Эта структура данных имеет несколько важных применений:

Эффективная реализация алгоритма Крускала нахождения минимального остова

Эффективная реализация для задачи минимума в автономном режиме

(т.е. нужно реализовать структуру данных, которая позволяет добавлять элементы из множества {1,..,N} и извлекать минимум; задача автономна в том смысле, что вся последовательность запросов известна к началу выполнения алгоритма)

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

Примечание. Асимптотика

Следует заметить, что у операций FindSet и Union асимптотика несколько хуже, чем O(1). На самом деле, асимптотика

для них равна O (alpha(N)), где alpha(N) - инверсия функции Аккермана:

alpha(N) = min { k : AK(1) >= N }, где

AK(J) = AK-1J+1(J) при K > 0, и A0(J) = J+1

Несколько первых значений функции alpha:

alpha(0)..alpha(2) = 0 alpha(3) = 1 alpha(4)..alpha(7) = 2 alpha(8)..alpha(2047) = 3

alpha(2048)..alpha(16512) = 4

Отсюда видно, что для всех мыслимых применений alpha(N) <= 4, а потому её можно считать константой, и приравнивать O (1).

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

Пусть элементы X - это некоторые числа. Вся структура данных хранится в виде двух массивов: P и Rank.

Массив P содержит предков, т.е. P[X] - это предок элемента X. Фактически, мы имеем древовидную структуру данных: двигаясь по предкам от любого элемента X, мы рано или поздно придём к представителю множества, к которому принадлежит X. В частности, если P[X] = X для некоторого X, то это означает, что X является представителем множества, к которому он принадлежит, и, очевидно, X является корнем дерева.

Массив Rank хранит ранги представителей, т.е. его значения имеют смысл только для элементовпредставителей. Ранг некоторого элемента-представителя X - это верхняя граница его высоты в его дереве. Ранги используются в качестве эвристики в операции Union.

Теперь рассмотрим реализацию операций:

MakeSet (X)

Эта операция очень проста - мы указываем, что P[X] = X, а ранг X равен 1.

FindSet (X)

Будем двигаться от X по предкам, и рано или поздно мы найдём представителя. Однако важный момент -

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

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

Union (X, Y)

Сначала мы заменяем элементы X и Y на представителей их множеств, просто вызывая функцию FindSet. Мы объединяем два множества, присваивая P[X] = Y или P[Y] = X. Однако выбор - что чему присваивается - осуществляется с помощью эвристики. Если ранги элементов X и Y отличны, то мы делаем корень с бо'льшим рангом родительским по отношению к корню с меньшим рангом. Если же ранги обоих элементов совпадают, то мы выбираем родителя произвольным образом, и увеличиваем его ранг на 1.

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

Реализация

vector<int> p, rank;

void init (int max_n)

{

p.resize (max_n);

for (int i=0; i<max_n; ++i) p[i] = i;

rank.resize (max_n);

}

void make_set (int x)

{

p[x] = x; rank[x] = 0;

}

int find_set (int x)

{

if (x == p[x]) return x; return p[x] = find_set (p[x]);

}

void unite (int x, int y)

{

x = find_set (x); y = find_set (y);

if (rank[x] > rank[y]) p[y] = x;

else

{

p[x] = y;

if (rank[x] == rank[y]) ++rank[y];

}

}

Рандомизация

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

vector<int> p;

void init (int max_n)

{

p.resize (max_n);

for (int i=0; i<max_n; ++i) p[i] = i;

}

void make_set (int x)

{

p[x] = x;

}

int find_set (int x)

{

if (x == p[x]) return x; return p[x] = find_set (p[x]);

}

void unite (int x, int y)

{

x = find_set (x); y = find_set (y); if (rand() & 1)

p[y] = x;

else

p[x] = y;

}

Дерево отрезков

Дерево отрезков - структура данных, которая позволяет реализовать за O (log N) операции следующего типа: нахождение суммы/минимума элементов массива в заданном отрезке (A[L..R], где L и R - это параметры запроса), изменение одного элемента массива, изменение/прибавление элементов на отрезке (A[L..R]). При этом объём дополнительно используемой памяти составляет O (N), или, если быть точным, не более 4 N.

Описание

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

Построим бинарное дерево T следующим образом. Корень дерева будет храниться в элементе T[1]. Он

будет содержать сумму элементов A[0..N-1], т.е. всего массива. Левый сын корня будет храниться в элементе T[2]

и содержать сумму первой половины массива A: A[0..N/2], а правый сын - в элементе T[3] и содержать сумму элементов A[N/2+1..N-1]. В общем случае, если T[i]-ый элемент содержит сумму элементов с L-го по R-ый, то его левым сыном будет элемент T[i*2] и содержать сумму A[L..(L+R)/2], а его правым сыном будет T[i*2+1] и содержать сумму A[(L+R)/2+1.. R]. Исключение, разумеется, составляют листья дерева - вершины, в которых L = R.

Далее, нетрудно заметить, что это дерево будет содержать 4 N элементов (а высота дерева будет порядка O (log N)). Поскольку значение в каждом элементе дерева однозначно определяется значениями в его сыновьях, то каждый элемент вычисляется за O (1), а всё дерево строится за O (N).

Рассмотрим теперь операцию суммы на некотором отрезке [L; R]. Мы встаём в корень дерева (i=1), и рекурсивно движемся вниз по этому дереву. Если в какой-то момент оказывается, что L и R совпадают с границами отрезка текущего элемента, то мы просто возвращаем значение текущего элемента T. Иначе, если отрезок [L; R] целиком попадает в отрезок левого или правого сына текущего элемента, то мы рекурсивно вызываем себя из этого сына и найденное значение возвращаем. Наконец, если отрезок [L; R] частично принадлежит и отрезку левого сына, и отрезку правого сына, то делим отрезок [L; R] на два отрезка [L; M] и [M+1; R] так, чтобы первый отрезок

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

O (log N).

Теперь рассмотрим операцию изменения значения некоторого элемента с индексом K. Будем спускаться по дереву от корня, ища тот лист, который содержит значение элемента A[K]. Когда мы найдём этот элемент, просто изменим соответствующее значение в массиве T и будем подниматься от текущего элемента обратно к

корню, пересчитывая текущие значения T. Понятно, что таким образом мы изменим все значения в дереве, которые

нужно изменить. Итого асимптотика O (log N).

Наконец, рассмотрим операцию изменения на отрезке. Для реализации этой операции нам понадобится немного модифицировать дерево. Пусть каждый элемент дерева, помимо суммы, будет содержать значение Val[i]: если все элементы массива A в текущем отрезке равны друг другу, то Val[i] будет содержать это значение, а иначе он будет содержать некое значение "неопределённость". Изначально его можно просто заполнить

значениями "неопределённость". А при выполнении операции изменения на отрезке мы будем спускаться по дереву, как в вышеописанном алгоритме суммирования, и если в какой-то момент L и R совпали с границами текущего отрезка, то мы присвоим Val[i] новое значение, которое мы хотим записать. Понятно, что теперь надо будет

модифицировать операцию суммирования - если она в какой-то момент встречает Val[i], отличное от "неопределённости", то она прекращает спуск по дереву и сразу возвращает нужное значение - действительно, результат уже определён значением Val[i], а вот если мы продолжим спуск, то уже будем считывать неправильные, старые значения.

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

все встретившиеся значения Add.

Реализация

Например, рассмотрим дерево отрезков для суммы с одиночной модификацией:

vector<long long> t; int n;

void build (const vector<int> & a, int i = 1, int l = 0, int r = n-1) { if (i == 1)

t.resize (n*4 + 1); if (l == r)

t[i] = a[l];

else {

int m = (l + r) / 2; build (a, i*2, l, m); build (a, i*2+1, m+1, r); t[i] = t[i*2] + t[i*2+1];

}

}

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