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

12.04.2021

Дерево отрезков — структура данных, позволяющая находить значение некоторой ассоциативной функции f {displaystyle f} на произвольном отрезке a [ i ] , a [ i + 1 ] , … , a [ j ] {displaystyle a[i],a[i+1],dots ,a[j]} массива за асимптотику O ( log ⁡ n ) {displaystyle O(log n)} . Наиболее часто в качестве f {displaystyle f} берутся функции суммы, произведения, максимум и минимум.

Описание структуры

Дерево отрезков представляет собой корневое дерево, листьями которого являются элементы исходного массива, а другие вершины имеют по 2 ребенка. Каждой вершине в соответствие поставлен интервал, являющийся объединением интервалов ее детей (если у вершины есть дети), либо интервал, содержащий конкретный элемент массива (для листьев). Кроме того, для каждой вершины хранится значение некоторой ассоциативной функции f {displaystyle f} на данном интервале. Данное дерево будет иметь логарифмическую высоту, так как количество уровней не будет превышать log 2 ⁡ n {displaystyle log _{2}n}

Дерево отрезков в памяти

Пусть наш массив a {displaystyle a} имеет n {displaystyle n} элементов: a [ 0 ] , a [ 1 ] , … , a [ n − 1 ] {displaystyle a[0],a[1],dots ,a[n-1]} .

Выберем h {displaystyle h} такое, что 2 h ≥ n {displaystyle 2^{h}geq n} . Дополним наш массив справа нейтральными элементами так, чтобы его длина равнялась 2 h {displaystyle 2^{h}} . Тогда для хранения дерева отрезков, построенного на элементах массива a {displaystyle a} , нам понадобится массив b {displaystyle b} из 2 h + 1 {displaystyle 2^{h+1}} ячеек.

Нулевую ячейку в массиве b {displaystyle b} мы использовать не будем, а ячейки с первой по ( 2 h + 1 − 1 ) {displaystyle (2^{h+1}-1)} -ю будут соответствовать вершинам двоичного дерева с соответствующими номерами. Обычно используется нумерация вершин дерева отрезков в порядке обхода в ширину, то есть корень дерева имеет номер 1, а левый и правый сыновья вершины с номером v {displaystyle v} имеют номера 2 v {displaystyle 2v} и 2 v + 1 {displaystyle 2v+1} соответственно. При такой нумерации вершина с номером 2 k + u {displaystyle 2^{k}+u} при 0 ≤ u < 2 k {displaystyle 0leq u<2^{k}} будет соответствовать отрезку [ u 2 h − k ; ( u + 1 ) 2 h − k − 1 ] {displaystyle [u2^{h-k};(u+1)2^{h-k}-1]} , то есть в ячейке b [ 2 k + u ] {displaystyle b[2^{k}+u]} будет храниться число f ( a [ u 2 h − k ] , a [ u 2 h − k + 1 ] , … , a [ ( u + 1 ) 2 h − k − 1 ] ) {displaystyle f(a[u2^{h-k}],a[u2^{h-k}+1],dots ,a[(u+1)2^{h-k}-1])} .


Далее в статье будет использоваться именно такая нумерация вершин дерева отрезков. В качестве альтернативы можно нумеровать вершины в порядке обхода в глубину, тогда левый и правый сыновья вершины v {displaystyle v} будут иметь номера v + 1 {displaystyle v+1} и v + 2 ( ⌊ L + R 2 ⌋ − L + 1 ) {displaystyle v+2(lfloor {dfrac {L+R}{2}} floor -L+1)} , где [ L ; R ] {displaystyle [L;R]} — отрезок, соответствующий вершине v {displaystyle v} . При этом, если строить дерево отрезков сразу по исходному массиву a {displaystyle a} , а не расширять его до длины 2 h {displaystyle 2^{h}} (в таком дереве не все длины отрезков будут степенями двойки и не все листья будут расположены на максимальной глубине), то для его хранения будет достаточно всего 2 n {displaystyle 2n} ячеек в массиве b {displaystyle b} . При хранении же дерева, вершины которого занумерованы в порядке обхода в ширину, длина массива b {displaystyle b} может достигать 4 n − 4 {displaystyle 4n-4} .

Построение дерева отрезков

Ниже приведён код на C++ рекурсивной функции b u i l d {displaystyle mathop { m {build}} } построения дерева отрезков для суммы на элементах массива a {displaystyle a} . Сложность построения дерева составляет O ( 2 h ) = O ( n ) {displaystyle O(2^{h})=O(n)} действий.

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]; } }

Дерево отрезков с одиночной модификацией

Изменение элемента

Пусть мы изменили значение a [ i ] {displaystyle a[i]} . Тогда нам нужно обновить значения в ячейках b [ 2 h + i ] {displaystyle b[2^{h}+i]} , b [ 2 h − 1 + i / 2 ] {displaystyle b[2^{h-1}+i/2]} , b [ 2 h − 2 + i / 4 ] {displaystyle b[2^{h-2}+i/4]} ,..., b [ 1 ] {displaystyle b[1]} . Таким образом, на изменение элемента уйдёт O ( h ) = O ( log ⁡ ( n ) ) {displaystyle O(h)=O(log(n))} действий.

Ниже приведён код на C++ рекурсивной процедуры u p d a t e {displaystyle mathop { m {update}} } обновления дерева отрезков для суммы при изменении i {displaystyle i} -го элемента в исходном массиве a {displaystyle a} .

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]; } }

Подсчёт функции для отрезка

Для подсчёта функции f {displaystyle f} от элементов a [ l ] , a [ l + 1 ] , ⋯ , a [ r ] {displaystyle a[l],a[l+1],cdots ,a[r]} используется следующая рекурсивная функция c o u n t {displaystyle mathop { m {count}} } от аргументов v , L , R , l , r {displaystyle v,L,R,l,r} , вычисляющая значение функции f {displaystyle f} для отрезка [ L ; R ] ∩ [ l ; r ] {displaystyle [L;R]cap [l;r]} . Здесь v {displaystyle v} — такая вершина дерева, что в ячейке b [ v ] {displaystyle b[v]} находится значение функции f {displaystyle f} для отрезка [ L ; R ] {displaystyle [L;R]} .

Если отрезки [ L ; R ] {displaystyle [L;R]} и [ l ; r ] {displaystyle [l;r]} не пересекаются, то ответ равен нейтральному элементу для функции f {displaystyle f} (0 для суммы, 1 для произведения, − ∞ {displaystyle -infty } для максимума, + ∞ {displaystyle +infty } для минимума).

Если [ L ; R ] ⊂ [ l ; r ] {displaystyle [L;R]subset [l;r]} , то ответ равен b [ v ] {displaystyle b[v]} .

Иначе разобьём отрезок [ L ; R ] {displaystyle [L;R]} пополам, положив C = ⌊ L + R 2 ⌋ {displaystyle C=lfloor {dfrac {L+R}{2}} floor } . Сведём задачу к вычислению функции f {displaystyle f} для отрезков [ L ; C ] ∩ [ l ; r ] {displaystyle [L;C]cap [l;r]} и [ C + 1 ; R ] ∩ [ l ; r ] {displaystyle [C+1;R]cap [l;r]} . Тогда ответ равен f ( c o u n t ( 2 v , L , C , l , r ) , c o u n t ( 2 v + 1 , C + 1 , R , l , r ) ) {displaystyle f({ m {count}}(2v,L,C,l,r),{ m {count}}(2v+1,C+1,R,l,r))} .

Таким образом, вычисление функции на отрезке [ l ; r ] {displaystyle [l;r]} сводится к вычислению функции от элементов массива b {displaystyle b} , соответствующих некоторым отрезкам [ 2 k u ; 2 k ( u + 1 ) − 1 ] {displaystyle [2^{k}u;2^{k}(u+1)-1]} .

Покажем, что при вычислении функции будет произведено получение 2 log ⁡ ( n ) {displaystyle 2log(n)} результатов. На каждой глубине мы вернём значение не более чем из двух вершин дерева. От противного, положим, что их не менее трёх. Но тогда два вызова из двух соседних вершин могли быть заменены на один вызов из их общего родителя. Следовательно, на каждой глубине мы вернём не более двух значений. Из-за построения высота дерева не превосходит log ⁡ ( n ) {displaystyle log(n)} . Следовательно, будет совершено не более 2 log ⁡ ( n ) {displaystyle 2log(n)} возвратов.

Аналогичными рассуждениями показывается, что за один запрос в дереве мы обойдём не более 4 log ⁡ ( n ) {displaystyle 4log(n)} вершин.

Ниже представлен код на C++ для вычисления суммы на отрезке [ l ; r ] {displaystyle [l;r]} .

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); }

Дерево отрезков с модификацией на интервале

Предположим, что мы хотим изменить значение не одной ячейки массива a {displaystyle a} , а целого интервала a [ l ] , ⋯ , a [ r ] {displaystyle a[l],cdots ,a[r]} (например, увеличить значения всех ячеек из интервала на заданное число X {displaystyle X} ). Тогда хранения только массива b {displaystyle b} недостаточно. Однако деревья отрезков, способные вычислять сумму и максимум, можно реализовать с хранением двух массивов одинаковой длины и рекурсивной реализацией операции изменения.

Дерево отрезков для суммы (RSQ)

Будем хранить массивы s u m {displaystyle sum} и a d d {displaystyle add} с той же адресацией, что и массив b {displaystyle b} (см. выше).


Рекурсивная процедура m o d i f y ( v , L , R , l , r , X ) {displaystyle { m {modify}}(v,L,R,l,r,X)} будет состоять в увеличении значения всех элементов на отрезке [ L ; R ] ∩ [ l ; r ] {displaystyle [L;R]cap [l;r]} на число X {displaystyle X} . X {displaystyle X} может быть как положительным, так и отрицательным. Здесь v {displaystyle v} — такая вершина дерева, что в ячейке s u m [ v ] {displaystyle sum[v]} находится сумма всех элементов на отрезке [ L ; R ] {displaystyle [L;R]} .

Ниже приведён код процедуры m o d i f y {displaystyle { m {modify}}} на 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); } }

Рекурсивная функция g e t S u m {displaystyle { m {getSum}}} вычисления суммы на отрезке [ L ; R ] ∩ [ l ; r ] {displaystyle [L;R]cap [l;r]} модифицируется следующим образом. У неё появляется ещё один аргумент a d d i t i v e {displaystyle additive} , характеризующий, на сколько нужно увеличить все числа на отрезке [ L ; R ] {displaystyle [L;R]} при подсчёте суммы.

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ложность операций m o d i f y {displaystyle { m {modify}}} и c o u n t {displaystyle { m {count}}} составляет O ( log ⁡ ( n ) ) {displaystyle O(log(n))} .

Дерево отрезков для максимума (RMQ)

Аналогично предыдущему случаю будем хранить массивы m a x {displaystyle max} и a d d {displaystyle add} . Процедура m o d i f y {displaystyle { m {modify}}} будет иметь тот же смысл и те же аргументы.

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]; } }

Рекурсивная функция g e t M a x {displaystyle { m {getMax}}} вычисления максимума на отрезке [ L ; R ] ∩ [ l ; r ] {displaystyle [L;R]cap [l;r]} реализуется аналогично функции g e t S u m {displaystyle { m {getSum}}} дерева отрезков для суммы.

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 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); }


Сложность операций m o d i f y {displaystyle { m {modify}}} и g e t M a x {displaystyle { m {getMax}}} составляет O ( log ⁡ ( n ) ) {displaystyle O(log(n))} .

Решение RMQ с помощью Sparse Table

Также задачу RMQ можно решать с помощью Sparse table. Эта структура данных позволяет находить максимум/минимум на отрезке за O(1) с препроцессингом за время O(n log n).

Препроцессинг:

Обозначим f ⁡ [ i , k ] {displaystyle mathop { m {f}} [i,k]} — максимум/минимум на отрезке от i {displaystyle i} до i + 2 k − 1 {displaystyle i+2^{k}-1} . Массив f ⁡ [ i , k ] {displaystyle mathop { m {f}} [i,k]} можно заполнить динамически следующим образом:

1) f ⁡ [ i , 0 ] = a [ i ] {displaystyle mathop { m {f}} [i,0]=a[i]} ;

2) f ⁡ [ i , k ] = max ( f ⁡ [ i , k − 1 ] , f ⁡ [ i + 2 k − 1 , k − 1 ] ) {displaystyle mathop { m {f}} [i,k]=max(mathop { m {f}} [i,k-1],mathop { m {f}} [i+2^{k-1},k-1])} или f ⁡ [ i , k ] = min ( f ⁡ [ i , k − 1 ] , f ⁡ [ i + 2 k − 1 , k − 1 ] ) {displaystyle mathop { m {f}} [i,k]=min(mathop { m {f}} [i,k-1],mathop { m {f}} [i+2^{k-1},k-1])} соответственно.

Вычисление:

Ответ на отрезке [ l , r ] {displaystyle [l,r]} равен max ( f ⁡ [ l , l g ] , f ⁡ [ r − 2 l g + 1 , l g ] ) {displaystyle max(mathop { m {f}} [l,lg],mathop { m {f}} [r-2^{lg}+1,lg])} (соответственно min ( f ⁡ [ l , l g ] , f ⁡ [ r − 2 l g + 1 , l g ] ) {displaystyle min(mathop { m {f}} [l,lg],mathop { m {f}} [r-2^{lg}+1,lg])} ), где lg — максимальная степень двойки, не превосходящая r − l + 1 {displaystyle r-l+1} .

Пример:

Рассматриваем диапазон от 1 до 5. Максимальная степень двойки, которая помещается на него, это два, но она не покрывает весь диапазон, а только часть от 1 до 4. Максимум на этом отрезке можно получить, сравнив значения f[1,2] и f[2,2] (максимумы на отрезках от 1 до 4 и от 2 до 5).

Решение за O(1) с препроцессингом и памятью O(N)

Для такого решения достаточно свести задачу RMQ к задаче LCA, построив декартово дерево из элементов вида ( i , a [ i ] ) {displaystyle (i,a[i])} , то есть i {displaystyle i} — ключ, a [ i ] {displaystyle a[i]} — приоритет. Приоритеты должны быть упорядочены снизу вверх, то есть в корне должен стоять элемент с наименьшим a [ i ] {displaystyle a[i]} . Очевидно, такое дерево легко построить за O ( N ) {displaystyle O(N)} , так как все ключи у нас упорядочены (это идущие друг за другом индексы элементов массива).

После этого ответом на любой запрос будет LCA двух вершин ( l , a [ l ] ) {displaystyle (l,a[l])} и ( r , a [ r ] ) {displaystyle (r,a[r])} . Индекс LCA будет лежать в [ l ; r ] {displaystyle [l;r]} , так как декартово дерево по ключу — двоичное дерево. Благодаря тому, что декартово дерево — куча по приоритету, эта же вершина будет иметь наименьший приоритет (значение элемента) из всех, находящихся в [ l ; r ] {displaystyle [l;r]}

Для задачи LCA известны несколько возможных решений за O ( 1 ) {displaystyle O(1)} с препроцессингом и памятью O ( N ) {displaystyle O(N)} . Такое решение является асимптотически оптимальным.