Дерево отрезков
Дерево отрезков — структура данных, позволяющая находить значение некоторой ассоциативной функции на произвольном отрезке массива за асимптотику . Наиболее часто в качестве берутся функции суммы, произведения, максимум и минимум.
Описание структуры[править | править код]
Дерево отрезков представляет собой корневое дерево, листьями которого являются элементы исходного массива, а другие вершины имеют по 2 ребенка. Каждой вершине в соответствие поставлен интервал, являющийся объединением интервалов ее детей (если у вершины есть дети), либо интервал, содержащий конкретный элемент массива (для листьев). Кроме того, для каждой вершины хранится значение некоторой ассоциативной функции на данном интервале. Данное дерево будет иметь логарифмическую высоту, так как количество уровней не будет превышать
Дерево отрезков в памяти[править | править код]
Пусть наш массив имеет элементов: .
Выберем такое, что . Дополним наш массив справа нейтральными элементами так, чтобы его длина равнялась . Тогда для хранения дерева отрезков, построенного на элементах массива , нам понадобится массив из ячеек.
Нулевую ячейку в массиве мы использовать не будем, а ячейки с первой по -ю будут соответствовать вершинам двоичного дерева с соответствующими номерами. Обычно используется нумерация вершин дерева отрезков в порядке обхода в ширину, то есть корень дерева имеет номер 1, а левый и правый сыновья вершины с номером имеют номера и соответственно. При такой нумерации вершина с номером при будет соответствовать отрезку , то есть в ячейке будет храниться число .
Далее в статье будет использоваться именно такая нумерация вершин дерева отрезков. В качестве альтернативы можно нумеровать вершины в порядке обхода в глубину, тогда левый и правый сыновья вершины будут иметь номера и , где — отрезок, соответствующий вершине . При этом, если строить дерево отрезков сразу по исходному массиву , а не расширять его до длины (в таком дереве не все длины отрезков будут степенями двойки и не все листья будут расположены на максимальной глубине), то для его хранения будет достаточно всего ячеек в массиве . При хранении же дерева, вершины которого занумерованы в порядке обхода в ширину, длина массива может достигать .
Построение дерева отрезков[править | править код]
Ниже приведён код на C++ рекурсивной функции построения дерева отрезков для суммы на элементах массива . Сложность построения дерева составляет действий.
void build() { build(1, 0, (1 << h) - 1); } void build(int v, int L, int R) { if (L == R){ b[v] = a[L]; } else { int C = (L + R) / 2; build(v * 2, L, C); build(v * 2 + 1, C + 1, R); b[v] = b[v * 2] + b[v * 2 + 1]; } }
Дерево отрезков с одиночной модификацией[править | править код]
Изменение элемента[править | править код]
Пусть мы изменили значение . Тогда нам нужно обновить значения в ячейках , , ,..., . Таким образом, на изменение элемента уйдёт действий.
Ниже приведён код на C++ рекурсивной процедуры обновления дерева отрезков для суммы при изменении -го элемента в исходном массиве .
void update(int i, int newValue) { update(1, 0, (1 << h) - 1, i, newValue); } void update(int v, int L, int R, int i, int newValue) { if (L == R){ b[v] = newValue; a[i] = newValue; } else { int C = (L + R) / 2; if (i <= C){ update(v * 2, L, C, i, newValue); } else { update(v * 2 + 1, C + 1, R, i, newValue); } b[v] = b[v * 2] + b[v * 2 + 1]; } }
Подсчёт функции для отрезка[править | править код]
Для подсчёта функции от элементов используется следующая рекурсивная функция от аргументов , вычисляющая значение функции для отрезка . Здесь — такая вершина дерева, что в ячейке находится значение функции для отрезка .
Если отрезки и не пересекаются, то ответ равен нейтральному элементу для функции (0 для суммы, 1 для произведения, для максимума, для минимума).
Если , то ответ равен .
Иначе разобьём отрезок пополам, положив . Сведём задачу к вычислению функции для отрезков и . Тогда ответ равен .
Таким образом, вычисление функции на отрезке сводится к вычислению функции от элементов массива , соответствующих некоторым отрезкам .
Покажем, что при вычислении функции будет произведено получение результатов. На каждой глубине мы вернём значение не более чем из двух вершин дерева. От противного, положим, что их не менее трёх. Но тогда два вызова из двух соседних вершин могли быть заменены на один вызов из их общего родителя. Следовательно, на каждой глубине мы вернём не более двух значений. Из-за построения высота дерева не превосходит . Следовательно, будет совершено не более возвратов.
Аналогичными рассуждениями показывается, что за один запрос в дереве мы обойдём не более вершин.
Ниже представлен код на C++ для вычисления суммы на отрезке .
int getSum(int l, int r) { return getSum(1, 0, (1 << h) - 1, l, r); } int getSum(int v, int L, int R, int l, int r) { if (L > r || R < l){ return 0; } if (l <= L && R <= r){ return b[v]; } int C = (L + R) / 2; return getSum(v * 2, L, C, l, r) + getSum(v * 2 + 1, C + 1, R, l, r); }
Дерево отрезков с модификацией на интервале[править | править код]
Предположим, что мы хотим изменить значение не одной ячейки массива , а целого интервала (например, увеличить значения всех ячеек из интервала на заданное число ). Тогда хранения только массива недостаточно. Однако деревья отрезков, способные вычислять сумму и максимум, можно реализовать с хранением двух массивов одинаковой длины и рекурсивной реализацией операции изменения.
Дерево отрезков для суммы (RSQ)[править | править код]
Будем хранить массивы и с той же адресацией, что и массив (см. выше).
Рекурсивная процедура будет состоять в увеличении значения всех элементов на отрезке на число . может быть как положительным, так и отрицательным. Здесь — такая вершина дерева, что в ячейке находится сумма всех элементов на отрезке .
Ниже приведён код процедуры на C++.
void modify(int l, int r, int X) { modify(1, 0, (1 << h) - 1, l, r, X); } void modify(int v, int L, int R, int l, int r, int X) { if (L > r || R < l){ return; } if (l <= L && R <= r){ sum[v] += X * (R - L + 1); add[v] += X; } else { int C = (L + R) / 2; modify(v * 2, L, C, l, r, X); modify(v * 2 + 1, C + 1, R, l, r, X); sum[v] = sum[v * 2] + sum[v * 2 + 1] + add[v] * (R - L + 1); } }
Рекурсивная функция вычисления суммы на отрезке модифицируется следующим образом. У неё появляется ещё один аргумент , характеризующий, на сколько нужно увеличить все числа на отрезке при подсчёте суммы.
int getSum(int l, int r) { return getSum(1, 0, (1 << h) - 1, l, r, 0); } int getSum(int v, int L, int R, int l, int r, int additive) { if (L > r || R < l){ return 0; } if (l <= L && R <= r){ return sum[v] + additive * (R - L + 1); } int C = (L + R) / 2; return getSum(v * 2, L, C, l, r, additive + add[v]) + getSum(v * 2 + 1, C + 1, R, l, r, additive + add[v]); }
Cложность операций и составляет .
Дерево отрезков для максимума (RMQ)[править | править код]
Аналогично предыдущему случаю будем хранить массивы и . Процедура будет иметь тот же смысл и те же аргументы.
void modify(int l, int r, int X) { modify(1, 0, (1 << h) - 1, l, r, X); } void modify(int v, int L, int R, int l, int r, int X) { if (L > r || R < l){ return; } if (l <= L && R <= r){ max[v] += X; add[v] += X; } else { int C = (L + R) / 2; modify(v * 2, L, C, l, r, X); modify(v * 2 + 1, C + 1, R, l, r, X); max[v] = std::max(max[v * 2], max[v * 2 + 1]) + add[v]; } }
Рекурсивная функция вычисления максимума на отрезке реализуется аналогично функции дерева отрезков для суммы.
int getMax(int l, int r) { return getMax(1, 0, (1 << h) - 1, l, r, 0); } int getMax(int v, int L, int R, int l, int r, int additive) { if (L > r || R < l){ return -INF; // Минус бесконечность, т.е. число, которое заведомо меньше любых чисел на нашем отрезке. Например, если все числа неотрицательны, то можно положить INF = 0. } if (l <= L && R <= r){ return max[v] + additive; } int C = (L + R) / 2; int max1 = getMax(v * 2, L, C, l, r, additive + add[v]); int max2 = getMax(v * 2 + 1, C + 1, R, l, r, additive + add[v]); return std::max(max1, max2); }
Сложность операций и составляет .
Решение RMQ с помощью Sparse Table[править | править код]
Также задачу RMQ можно решать с помощью Sparse table. Эта структура данных позволяет находить максимум/минимум на отрезке за O(1) с препроцессингом за время O(n log n).
Препроцессинг:
Обозначим — максимум/минимум на отрезке от до . Массив можно заполнить динамически следующим образом:
1) ;
2) или соответственно.
Вычисление:
Ответ на отрезке равен (соответственно ), где lg — максимальная степень двойки, не превосходящая .
Пример:
Рассматриваем диапазон от 1 до 5. Максимальная степень двойки, которая помещается на него, это два, но она не покрывает весь диапазон, а только часть от 1 до 4. Максимум на этом отрезке можно получить, сравнив значения f[1,2] и f[2,2] (максимумы на отрезках от 1 до 4 и от 2 до 5).
Решение за O(1) с препроцессингом и памятью O(N)[править | править код]
Для такого решения достаточно свести задачу RMQ к задаче LCA, построив декартово дерево из элементов вида , то есть — ключ, — приоритет. Приоритеты должны быть упорядочены снизу вверх, то есть в корне должен стоять элемент с наименьшим . Очевидно, такое дерево легко построить за , так как все ключи у нас упорядочены (это идущие друг за другом индексы элементов массива).
После этого ответом на любой запрос будет LCA двух вершин и . Индекс LCA будет лежать в , так как декартово дерево по ключу — двоичное дерево. Благодаря тому, что декартово дерево — куча по приоритету, эта же вершина будет иметь наименьший приоритет (значение элемента) из всех, находящихся в
Для задачи LCA известны несколько возможных решений за с препроцессингом и памятью . Такое решение является асимптотически оптимальным.
Ссылки[править | править код]
- Дерево отрезков, его реализация на C++ и задачи, им решаемые, на сайте e-maxx.ru
- Реализация Дерева отрезков на Java