A ideia aqui é resolver o problema RSQ em que são necessárias atualizações nos elementos do array e também muitas consultas são feitas. Usando uma Segment Tree (ST), teremos as seguintes etapas:
A maneira mais fácil de explicar uma ST é através de um exemplo. Suponha que os elementos do array são:
5 2 8 9 3 6
Nesse caso, temos n=6.
Iremos armazenar os elementos em um vector e ignoraremos a primeira posição, só para facilitar a construção e navegação da árvore. Assim, o primeiro elemento será o elemento de índice 1:
a = [-1, 5, 2, 8, 9, 3, 6]
A ideia da ST é contruir quebrar o array em vários intervalos. Então, com base nesses intervalos, construimos uma árvore onde cada nó guarda a soma dos elementos dos intervalos.
Vamos adotar uma estratégia de dividir e conquistar, sempre quebrando o intervalo atual ao meio. O primeiro intervalo é o array inteiro, representado pelos índices: [1, 7[ , onde 1 é o índice 1 do array (intervalo fechado, ou seja, o elemento do índice 1 está incluido no intervalo) e 7 é o último índice daquele intervalo, mas nesse caso o intervalo é aberto, ou seja, o elemento do índice 7 não está incluído, sendo portanto, o elemento de índice 6 o último incluído.
Esse nó vai armazenar a soma do intervalo [1,7[. Mas ainda não temos o valor dessa soma. Vamos dividir esse intervalo em 2:
Para cada nó filho, iremos repetir esse procedimento, sempre quebrando o intervalo em dois. Logo, para o nó [1,4[, quebraremos em dois intervalos: [1,2[ e [2,4[
Faremos isso até que o intervalo contenha apenas um elemento. Nesse exemplo, o intervalo [1, 2[ contém apenas um elemento, que é o próprio um (lembre que o intervalo é aberto em 2). Como cada nó guarda o valor da soma do intervalo, o valor da soma de um intervalo com um só elemento é o próprio elemento. Logo, o valor do nó [1,2[ será o valor do elemento da posição 1, nesse caso: 5. E a nossa árvore ficará assim:
Ainda não discutimos como iremos representar essa árvore. Você pode usar ponteiros se quiser, mas eu vou utilizar arrays. Sendo assim, o nó raiz ficará na posição 1, e os seus filhos na posição 2*1 e 2*1 +1. Assim, um nó x, terá filhos nas posições 2*x e 2*x +1.
Agora nossa árvore ficará assim:
Como se trata de uma RSQ, os nós guardam os valores das somas dos intervalos. Logo, como estamos dividindo o intervalo em 2, para obtermos o valor da some de um intervalo que foi dividido, basta somar os dois intervalos. Por exemplo, o nó [2,4[ guardará o valor do nó [2,3[ + o valor do nó [3,4[.
Sendo assim, agora podemos atualizar o valor dos nós intermediários:
Prontinho, nossa árvore está construída.
Agora falta discutir como iremos usá-la para fazer as consultas.
Suponha que a nossa consulta seja: rsq(1,7).
Ou seja, queremos a soma de todos os elementos das posições 1 a 6.
Essa seria o melhor caso de consulta. Pois o nó raiz da nossa árvore guarda exatamente essa informação. Portanto, basta retornar o valor do nó raiz.
Suponha que a consulta seja: rsq(1,3). Nesse caso, estamos interessados no valor da soma dos elementos { a[1]. a[2] }
O nó raiz, contém a soma de um intervalo maior do que o que procuramos, portanto, precisamos descer mais na árvore.
Vamos examinar o nó esquerdo primeiro: [1,4[ . O intervalo que esse nó representa é dos elementos { a[1], a[2], a[3] }, portanto, também é maior do que o que desejamos. Vamos descer mais um nível, também começando pelo nó esquerdo.
Chegamos no nó [1, 2[ . Esse nó guarda o valor da soma do conjunto: { a[1] } . Dessa vez, o intervalo que encontramos é menor que o intervalo procurado. E além disso, o intervalo encontrado está contido no intervalo procurado. Ou seja, esse valor encontrado precisará ser usado e somado com os demais valores encontrados. Assim, vamos utilizar esse valor. E vamos explorar o outro filho: [2,4[.
Esse outro filho guarda o valor do intervalo { a[2], a[3] }. Só para lembrar, o interavlo procurado é {a[1], a[2]} . Assim, o intervalo desse nó, contém valores que não nos interessam (a[3]), mas ao mesmo tempo contém valores que interessam (a[2]). Sendo assim, continuamos descendo na árvore.
Chegamos no nó [2, 3[ . Esse nó contém o valor do intervalo { a[2] }. Logo, esse valor nos interessa e vamos retornar o valor desse nó. Ao retornar, vamos analisar o outro filho:
O nó [3,4[ guarda o intervalo { a[3] } . Esse valor está fora do intervalo que queremos saber, portanto não nos interessa. Assim, voltamos a subir na árvore retornando os valores que nos interessam:
Assim, retornamos ao nó raiz. Ainda falta avaliar o filho direito. Esse filho direito guarda o intervalo { a[4], a[5], a[6] }. Ou seja, todos os valores do filho direito não nos interessam. O intervalo do filho direito está completamente fora do interavalo procurado. Sendo assim, podemos já descartar esse filho, não sendo necessário descer na árvore.
E assim, temos o valor do rsq(1,3) = 7.
Essa estrutura tem o seu pior caso para a consulta rsq da ordem de O (log(n)).
Agora veja o código abaixo com detalhes para ver como a construção e a consulta ocorrem na prática.
/*Segment TreeRodrigo PaesRSQ: Range SUM Queries: soma de intervalos com atualização dinâmica.Caso os valores do array não precisem ser atualizados, utilizarsparse table.Essa árvore possui implementação baseada em: http://codeforces.com/blog/entry/15729*/#include <bits/stdc++.h>using namespace std;typedef vector<int> vi;#define pb push_backvi st; //segment treevi a; // list of elements/*Constrói a segment tree com base no vector a.O(n)*/void build(int idx=1, int left=1, int right=a.size()){ if (right - left < 2) // se o intervalo tiver 1 elemento, ou zero { st[idx] = a[left]; return; } int mid = (left+right) / 2; build(idx * 2, left, mid); build(idx * 2+1, mid, right); st[idx] = st[idx*2] + st[idx*2+1];}/*Em uma segment tree, cada nó da árvore guarda o valor da soma de um determinadointervalo.Ou seja, cada posição idx da árvore guarda o valor da soma do intervalo [left, right[(fechado em left, aberto em right).x = posição inicial do intervalo a ser somado (intervalo fechado)y = posição final do intervalo a ser somado (intervalo aberto)idx = índice atual da segment treeleft = início do intervalo da soma do nó idxright = fim do intervalo da soma do nó idxO(log(n))*/int sum(int x, int y, int idx = 1, int left = 1, int right = a.size()){ if ( x >= right || y <= left) { /* se o início do intervalo desejado (x) for maior ou igual ao right significa que o intervalo está à direita desse nó idx e, portanto, fora do intervalo desse nó. O mesmo raciocínio se aplica ao fim. Ou seja, se o fim do intervalo é menor que o início do intervalo do nó idx significa que o intervalo está à esquerda do nó. */ return 0; } if ( x <= left && y >= right) { /* Nesse caso temos: left right | | | | x y O intervalo do nó está contido no intervalo desejado, assim, não faz sentido dividir o intervalo em pedaços ainda menores, ou seja, não faz sentido continuar descendo na árvore ainda mais pois a informação mais agregada já está contida nesse nó */ return st[idx]; } /* Se chegamos aqui, significa que o intervalo do nó contém a soma de elementos que não são desejados pelo inervalo x,y. Ou seja, pode ser que [x,y[ seja menor que [left,right[, o que indica que [left,right[ tem um valor maior do que o necessário. Nesse caso, temos que descer na árvore até encontrar os intervalos que contenham exatamanete a informação que precisamos. */ int mid = (left+right) / 2; return sum (x,y, idx*2, left, mid) + sum(x, y, idx*2+1, mid, right);}/*Caso uma posição do array tenha o seu valor modificado, ao invés demodificar diretamente no array, deve-se chamar essa função modifypara alterar também a árvore.Percebe que a alteração deve ser propagada por todos os nós da árvoreque possuem aquele valor somado.O(log(n))*/void modify(int pos, int value, int idx = 1, int left=1, int right = a.size()){ /* modifica apenas o valor relativo ao anterior. Por exemplo, se antes a[pos] fosse 10 e agora é 5, então temos que subtrair 5 das somas. 5 - 10 = -5 */ st[idx] += ( value - a[pos] ); if (right - left < 2) { a[pos] = value; return; } int mid = (left+right) / 2; // Só precisa alterar um ramo if (pos < mid) { modify(pos, value, idx*2, left, mid); } else { modify(pos, value, idx*2+1, mid, right); }}int main(){ a.pb(-1); // Ignora a posição 0 // Elementos do array a.pb(1); a.pb(2); a.pb(3); a.pb(4); a.pb(5); a.pb(6); st.resize(a.size() *4); // 4n para o pior caso build(); for (auto it : st) { cout << it <<" "; } cout << endl; cout << sum(1,5) << endl; // espera-se 1+2+3+4 = 10 modify(2,10); // passou de 2 para 10 cout << sum(1,5) << endl; // espera-se: 1+10+3+4 = 18 return 0;}