1.4.3 Ažuriranje segmenata

I segmentna i Fenvikova drveta podržavaju efikasno izračunavanje zbirova određenih segmenata niza (engl. range-query), pa samim tim i određivanje pojedinačnih vrednosti niza (engl. point-query), kao i ažuriranje1 pojedinačnih elemenata niza (engl. point-update), dok ažuriranje celih segmenata niza odjednom (engl. range-update) nije direktno podržano. Naime, ako se ono svede na pojedinačno ažuriranje svih elemenata unutar segmenta, dobija se loša složenost (u najgorem slučaju vrši se \(n\) ažuriranja koja imaju pojedinačnu složenost \(O(\log{n})\), pa je ukupna složenost \(O(n\log{n})\)). Sa druge strane, čuvanje niza razlika omogućava efikasno ažuriranje celih segmenata (engl. range-update), pa samim tim i pojedinačnih elemenata (engl. point-update), ali ne i efikasno određivanje pojedinačnih elemenata niza (engl. point-query) niti zbirova segmenata (engl. range-query).

point query range query point update range update
Razlike susednih elemenata loše / \(O(n)\) loše / \(O(n)\) dobro / \(O(1)\) dobro / \(O(1)\)
Prefiksni zbirovi dobro / \(O(1)\) dobro / \(O(1)\) loše / \(O(n)\) loše / \(O(n)\)
Fenvikovo drvo dobro / \(O(\log{n})\) dobro / \(O(\log{n})\) dobro / \(O(\log{n})\) loše / \(O(n\log{n})\)
Segmentno drvo dobro / \(O(1)\) dobro / \(O(\log{n})\) dobro / \(O(\log{n})\) loše / \(O(n\log{n})\)

U nastavku ćemo videti nekoliko načina da se naprave strukture koje efikasno podržavaju sve navedene operacije.

Drvo nad nizom razlika

Problem. Definisati strukturu podataka koja obezbeđuje efikasno ažuriranje segmenata datog niza određenih pozicijama \([a, b]\) (range update), pa samim tim i ažuriranje pojedinačnih elemenata niza (point update) uvećavanjem svih elemenata za datu vrednost, kao i efikasno određivanje vrednosti pojedinačnih elemenata niza (point query).

Primetimo da se ne zahteva mogućnost efikasnog određivanja statistika segmenata (range query).

Osnovna ideja rešenja je da se održava niz razlika susednih elemenata polaznog niza i da se taj niz razlika čuva u Fenvikovom drvetu (ili, analogno, u segmentnom drvetu).

Na ovaj način, i ažuriranje celih segmenata niza odjednom i očitavanje pojedinačnih elemenata možemo postići algoritmima složenosti \(O(\log{n})\), što nije bilo moguće samo uz korišćenje niza razlika (uvećavanja svih elemenata nekog segmenta je tada bilo složenosti \(O(1)\), ali je očitavanje vrednosti iz niza bilo složenosti \(O(n)\)). Dakle, ako Fenvikovo ili segmentno drvo izgradimo nad nizom razlika umesto nad originalnim nizom, gubimo mogućnost efikasnog izračunavanja statistika segmenata (range query), ali dobijamo mogućnost ažuriranja segmenata (range update).

point query range query point update range update
Drvo razlika dobro / \(O(\log{n})\) loše / \(O(n \log{n})\) dobro / \(O(\log{n})\) dobro / \(O(\log{n})\)

Dva drveta razlika

Na kraju, opisaćemo nekoliko struktura podataka koje omogućavaju efikasno izvršavanje sve četiri vrste upita.

Problem. Definisati strukturu podataka koja obezbeđuje efikasno ažuriranje segmenata datog niza određenih pozicijama \([a, b]\) (samim tim i ažuriranje pojedinačnih elemenata niza) uvećavanjem svih elemenata za datu vrednost, kao i efikasno određivanje zbirova segmenata određenih pozicijama \([a, b]\) (samim tim i određivanje vrednosti pojedinačnih elemenata niza).

Efikasno uvećanje svih elemenata u datom segmentu za istu vrednost i izračunavanje zbirova segmenata moguće je implementirati održavanjem dva Fenvikova ili segmentna drveta.

Prvo od njih će čuvati razlike originalnog niza i omogući će nam da efikasno uvećavamo segmente i izračunavamo pojedinačne elemente originalnog niza.

Ako je originalni niz dužine inicijalizovan nulama, nakon uvećanja svih elemenata sa pozicija iz segmenta \([a, b]\) za vrednost \(v\) dobijamo niz:

\(a\) \(b\)
\(0\) \(0\) \(v\) \(v\) \(v\) \(v\) \(0\) \(0\)

koji se može predstaviti nizom razlika:

\(a\) \(b\) \(b+1\)
\(0\) \(0\) \(v\) \(0\) \(0\) \(0\) \(-v\) \(0\)

Sabiranjem prefiksa niza razlika možemo dobiti bilo koji pojedinačni element početnog niza (ako je niz razlika u Fenvikovom ili segmentnom drvetu, složenosti ovog postupka je \(O(\log{n})\)).

Cilj nam je da izračunamo prefiksne zbirove originalnog niza (njega ne možemo čuvati u Fenvikovom ili segmentnom drvetu, zato što nam je potrebno ažuriranje njegovih segmenata, a ne pojedinačnih elemenata).

Prefiksni zbirovi originalnog niza su:

\(a\) \(b\)
\(0\) \(0\) \(v\) \(2v\) \((b-a)v\) \((b-a+1)v\) \((b-a+1)v\) \((b-a+1)v\)

Dakle, važi:

\[P_k = \sum_{i=1}^k A_i=\left\{ \begin{array}{ll} 0 & k < a \\ (k-(a-1))\cdot v & a\le k\le b \\ (b-a+1)\cdot v & k>b \end{array} \right. \]

Ključna ideja je da uporedimo ove tražene zbirove sa vrednostima niza koji na svakoj poziciji \(k\) sadrži vrednost \(k\cdot A_k\), tj. da razmotrimo razlike \(X_k = kA_k - P_k\).

Drugim rečima, svaki zbir prefiksa \(P_k\) smo razložili na razliku \(kA_k - X_k\):

\[P_k = \sum_{i=1}^k A_i=k \cdot A_k - X_k =\left\{ \begin{array}{ll} k\cdot 0 - 0 & k < a \\ k\cdot v - (a-1)\cdot v & a\le k \le b \\ k\cdot 0 - (-(b - a + 1))\cdot v & k>b \end{array} \right. \]

Niz \(X_k\), dakle, nakon ažuriranja originalnog niza ima sledeće vrednosti:

\(a\) \(b\)
\(0\) \(0\) \((a-1)v\) \((a-1)v\) \((a-1)v\) \((a-1)v\) \(-(b-a+1)v\) \(-(b-a+1)v\)

Ako bismo u svakom trenutku poznavali vrednosti u nizu \(X_k\), tada bismo vrednosti \(P_k\) lako mogli izračunati pomoću formule \(P_k = kA_k - X_k\). Vrednosti \(A_k\) lako izračunavamo pomoću Fenvikovog ili segmentnog drveta u kome čuvamo razlike tog niza. Međutim, i niz \(X_k\) je takav da mu se pri svakom ažuriranju povezani segmenti uvećavaju tj. umanjuju za istu vrednost, tako da se i on može lako rekonstruisati ako bi se njegove razlike čuvale u drugom Fenvikovom ili segmentnom drvetu. Naime, niz razlika ovog niza se menja ovako:

\(a\) \(b\) \(b+1\)
\(0\) \(0\) \((a-1)v\) \(0\) \(0\) \(0\) \(-bv\) \(0\) \(0\)

Dakle, održavamo dva drveta: \(D_A\) u kome čuvamo razlike niza \(A\) i \(D_X\) u kome čuvamo razlike niza \(X\). Prvo inicijalizujemo razlikama niza \(A\), a drugo nulama.

Operacija uvećanja svih elemenata sa pozicija iz \([a, b]\) za vrednost \(v\) se svodi na sledeće operacije nad drvetima:

Operacija izračunavanje vrednosti niza \(A\) na poziciji \(k\) tj. vrednosti \(A_k\) se svodi na izračunavanje prefiksne sume prvih \(k\) elemenata Fenvikovog drveta \(D_A\).

Operacija izračunavanja zbira prefiksa elemenata niza \(A\) dužine \(k\) se svodi na izračunavanje vrednosti

Operacija izračunavanja zira segmenta sa pozicija \([a, b]\) niza \(A\) se svodi na izračunavanje vrednosti:

Pri tom je \(P_0 = 0\).

Dakle, na ovaj način možemo postići dobru složenost za sva 4 tipa operacija.

point query range query point update range update
Dva drveta razlika dobro / \(O(\log{n})\) dobro / \(O(\log{n})\) dobro / \(O(\log{n})\) dobro / \(O(\log{n})\)

Pomoću narednog apleta možete proveriti svoje razumevanje ove strukture podataka.

Primer 1.4.20. Prikažimo jedan primer upotrebe ove strukture podataka. Pretpostavimo da niz \(A\) ima 10 elemenata i da su u početku svi jednaki nuli. U programu bi se održavala samo drveta nad nizovima \(D_A\) i \(D_X\), a ilustracije radi mi ćemo prikazati i sadržaj niza \(A\), njegovih prefiksnih suma \(P\) i pomoćnog niza \(X\). Elementi originalnog niza (kao i nizova razlika) smešteni su na pozicijama od 1 do 10.

\(0\) \(1\) \(2\) \(3\) \(4\) \(5\) \(6\) \(7\) \(8\) \(9\) \(10\)
\(D_A\) - \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\)
\(D_X\) - \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\)
\(A\) - \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\)
\(kA_k\) - \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\)
\(X\) - \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\)
\(P\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\) \(0\)

Uvećajmo element \(A_5\) za \(4\). To se može svesti na uvećavanje elemenata segmenta \([a, b] = [5, 5]\) za \(v=4\).

\(0\) \(1\) \(2\) \(3\) \(4\) \(5\) \(6\) \(7\) \(8\) \(9\) \(10\)
\(D_A\) - \(0\) \(0\) \(0\) \(0\) \(4\) \(-4\) \(0\) \(0\) \(0\) \(0\)
\(D_X\) - \(0\) \(0\) \(0\) \(0\) \(16\) \(-20\) \(0\) \(0\) \(0\) \(0\)
\(A\) - \(0\) \(0\) \(0\) \(0\) \(4\) \(0\) \(0\) \(0\) \(0\) \(0\)
\(kA_k\) - \(0\) \(0\) \(0\) \(0\) \(20\) \(0\) \(0\) \(0\) \(0\) \(0\)
\(X\) - \(0\) \(0\) \(0\) \(0\) \(16\) \(-4\) \(-4\) \(-4\) \(-4\) \(-4\)
\(P\) \(0\) \(0\) \(0\) \(0\) \(0\) \(4\) \(4\) \(4\) \(4\) \(4\) \(4\)

Umanjimo sve elemente niza \(A\) za \(1\). To se može svesti na uvećavanje elemenata segmenta \([a, b] = [1, 10]\) za \(v=-1\).

\(0\) \(1\) \(2\) \(3\) \(4\) \(5\) \(6\) \(7\) \(8\) \(9\) \(10\)
\(D_A\) - \(-1\) \(0\) \(0\) \(0\) \(4\) \(-4\) \(0\) \(0\) \(0\) \(0\)
\(D_X\) - \(0\) \(0\) \(0\) \(0\) \(16\) \(-20\) \(0\) \(0\) \(0\) \(0\)
\(A\) - \(-1\) \(-1\) \(-1\) \(-1\) \(3\) \(-1\) \(-1\) \(-1\) \(-1\) \(-1\)
\(kA_k\) - \(-1\) \(-2\) \(-3\) \(-4\) \(15\) \(-6\) \(-7\) \(-8\) \(-9\) \(-10\)
\(X\) - \(0\) \(0\) \(0\) \(0\) \(16\) \(-4\) \(-4\) \(-4\) \(-4\) \(-4\)
\(P\) \(0\) \(-1\) \(-2\) \(-3\) \(-4\) \(-1\) \(-2\) \(-3\) \(-4\) \(-5\) \(-6\)

Odredimo \(S_{[3, 7]}\) tj. zbir elemenata niza u segmentu \([3, 7]\). Važi da je \(S_{[3, 7]} = P_7 - P_2\), pa je potrebno da odredimo \(P_7\) i \(P_2\).

\(P_7\) se određuje tako što se pomoću Fenvikovog drveta efikasno odredi \(A_7 = \mathit{zbir\_prefiksa(D_A, 7)} = -1\), zatim \(X_7 = \mathit{zbir\_prefiksa(D_X, 7)} = -4\) i na kraju se izračuna \(7A_7-X_7=7\cdot(-1)-(-4) = -3\).

\(P_2\) se određuje tako što se pomoću Fenvikovog drveta efikasno odredi \(A_2 = \mathit{zbir\_prefiksa(D_A, 2)} = -1\), zatim \(X_2 = \mathit{zbir\_prefiksa(D_X, 2)} = 0\) i na kraju se izračuna \(2A_2-X_2=2\cdot(-1)-0 = -2\). Traženi zbir segmenta je onda \(-3 - (-2) = -1\). Jasno je da je to tačan rezultat, jer se u tom segmentu nalaze elementi \([-1, -1, 3, -1, -1]\).

Lenjo ažuriranje segmentnog drveta

Opišimo sada još jedno rešenje prethodnog problema, tj. još jednu strukturu podataka koja omogućava efikasno izvršavanje sve četiri vrste upita. Za razliku od prethodne, ova struktura omogućava i izračunavanje nekih drugih statistika (ne samo zbira elemenata).

Podsetimo se, segmentna drveta omogućavaju efikasno ažuriranje pojedinačnih elemenata i izračunavanje zbirova segmenata (samim tim i određivanje vrednosti pojedinačnih elemenata), međutim, ne omogućavaju efikasno ažuriranje svih elemenata datog segmenta \([a, b]\) (tj. njihovo uvećanje za datu vrednost ili njihovu zamenu datom vrednošću). Ovo se može postići u vremenu \(O(\log n)\) ako se na segmentno drvo primeni tehnika lenje propagacije (engl. lazy propagation). Ona je primer strategije lenjog izvršavanja kojim se izračunavanja odlažu sve dok odgovarajuće vrednosti nisu neophodne. Tehnika lenje propagacije se dosta oslanja na rad sa segmentnim drvetom odozgo naniže. Jednostavnosti radi, tehniku ćemo predstaviti kroz primer upita uvećavanja segmenata za datu vrednost.

Svaki čvor u segmentnom drvetu čuva zbir nekog segmenta orignalnog niza. Ako se taj segment u celosti sadrži unutar segmenta koji se ažurira, možemo unapred izračunati za koliko se povećava vrednost u tom čvoru. Naime, ako se svaka vrednost u segmentu povećava za \(v\), tada se vrednost zbira tog segmenta povećava za \(k \cdot v\), gde je \(k\) broj elemenata u tom segmentu. Vrednost zbira u tom čvoru time biva ažurirana u konstantnom vremenu, ali vrednosti zbirova unutar poddrveta kojima je taj čvor koren (uključujući i vrednosti u listovima koje odgovaraju vrednostima polaznog niza) i dalje ostaju neažurne. Njihovo ažuriranje zahtevalo bi linearno vreme, što je nedopustivo skupo. Ključna ideja je da se ažuriranje tih vrednosti odloži i da se one ne ažuriraju odmah, već samo kada zatrebaju tokom nekog naknadnog upita, tj. tokom neke kasnije posete tim čvorovima (koja se inače vrši u sklopu tog kasnijeg upita, a ne posebno u cilju ažuriranja vrednosti). Naime, ne želimo da te čvorove posećujemo samo zbog ovog ažuriranja, već ćemo ažuriranje uraditi usput, tokom neke druge posete tim čvorovima koja bi se svakako morala desiti.

Postavlja se pitanje kako da signaliziramo da vrednosti zbirova u nekom poddrvetu nisu ažurne i dodatno ostavimo uputstvo na koji način ih treba ažurirati. U tom cilju u svakom od čvorova pored vrednosti zbira segmenta čuvamo i dodatni koeficijent lenje propagacije. Ako drvo u svom korenu ima koeficijent lenje propagacije \(c\) koji je različit od nule, to znači da vrednosti zbirova u celom tom drvetu nisu ažurne i da je svaki od listova tog drveta potrebno povećati za \(c\) i u odnosu na to ažurirati i vrednosti zbirova u svim unutrašnjim čvorovima tog drveta (uključujući i koren). Ažuriranje se može odlagati sve dok vrednost zbira u nekom čvoru ne postane zaista neophodna, a to je tek prilikom upita izračunavanja vrednosti zbira nekog segmenta. Ipak, vrednosti zbirova u čvorovima ćemo ažurirati i češće i to zapravo prilikom svake posete čvoru – bilo u sklopu operacije uvećanja vrednosti iz nekog segmenta pozicija polaznog niza, bilo u sklopu upita izračunavanja zbira nekog segmenta. Naime, na početku obe rekurzivne funkcije možemo proveriti da li je vrednost koeficijenta lenje propagacije tekućeg čvora različita od nule i ako jeste, ažurirati vrednost zbira u tom čvoru tako što ćemo ga uvećati za proizvod tog koeficijenta i broja elemenata koji taj čvor pokriva, zatim koeficijente lenje propagacije oba njegova deteta uvećati za taj koeficijent, a koeficijent lenje propagacije tog čvora postaviti na nulu (time koren drveta koji trenutno posećujemo postaje ažuran, a njegovim poddrvetima se daje uputstvo kako ih u budućnosti ažurirati). Primetimo da se na ovaj način izbegava ažuriranje celog drveta odjednom, već se ažurira samo koren, što je operacija složenosti \(O(1)\).

Imajući ovo u vidu, razmotrimo kako se može implementirati funkcija koja vrši uvećanje svih elemenata nekog segmenta. Njena invarijanta će biti da svi čvorovi u drvetu ili sadrže ažurne vrednosti zbirova ili će biti ispravno obeleženi za kasnija ažuriranja (preko koeficijenta lenje propagacije), a da će nakon njenog izvršavanja koren drveta na kom je funkcija pozvana sadržati aktuelnu vrednost zbira. Nakon početnog obezbeđivanja da vrednost u tekućem čvoru postane ažurna, moguća su tri sledeća slučaja.

Lenjo segmentno drvo čuvaćemo korišćenjem dva niza: drvo i lenjo: u prvom ćemo čuvati elemente segmentnog drveta, a u drugom koeficijente lenje propagacije svakog od čvorova.

// ažurira elemente lenjog segmentnog drveta koje je smešteno
// u nizove drvo i lenjo od pozicije k u kome se čuvaju zbirovi
// elemenata originalnog niza sa pozicija iz segmenta [x, y]
// nakon što su u originalnom nizu svi elementi sa pozicija iz 
// segmenta [a, b] uvećani za vrednost v
void promeni(vector<int>& drvo, vector<int>& lenjo, int k, int x, int y,
             int a, int b, int v) {
   // ažuriramo vrednost u korenu, ako nije ažurna
   if (lenjo[k] != 0) {
      drvo[k] += (y - x + 1) * lenjo[k];
      // ako nije u pitanju list, propagaciju prenosimo na decu
      if (x != y) {
         lenjo[2*k] += lenjo[k];
         lenjo[2*k+1] += lenjo[k];
      }
      // vrednost u korenu je sad ažurna
      lenjo[k] = 0;
   }
   // ako su intervali disjunktni, ništa nije potrebno raditi
   if (b < x || y < a) return;
   // ako je interval [x, y] ceo sadržan u intervalu [a, b]
   // vrsimo uvecanje cvora za k * v
   if (a <= x && y <= b) {
     drvo[k] += (y - x + 1) * v;
     // ako nije u pitanju list, propagaciju prenosimo na decu
     if (x != y) {
        lenjo[2*k] += v;
        lenjo[2*k+1] += v;
     }
   } else {
     // u suprotnom se intervali seku, 
     // pa rekurzivno obilazimo poddrveta
     int s = (x + y) / 2;
     promeni(drvo, lenjo, 2*k, x, s, a, b, v); 
     promeni(drvo, lenjo, 2*k+1, s+1, y, a, b, v); 
     // azurnu vrednost u korenu dobijamo kao zbir vrednosti dece
     drvo[k] = drvo[2*k] + drvo[2*k+1];
   }
}

// ažurira elemente lenjog segmentnog drveta koje je smešteno
// u nizove drvo i lenjo od pozicije 1 u kome se čuvaju zbirovi
// elemenata originalnog niza sa pozicija iz segmenta [0, n-1]
// nakon što su u originalnom nizu svi elementi sa pozicija iz 
// segmenta [a, b] uvećani za vrednost v
void promeni(vector<int>& drvo, vector<int>& lenjo, int n, 
             int a, int b, int v) {
   promeni(drvo, lenjo, 1, 0, n-1, a, b, v);
}

Složenost funkcije ažuriranja svih elemenata datog segmenta u lenjom segmentnom drvetu iznosi \(O(\log n)\). Naime, kao i u slučaju operacije računanja sume segmenta u segmentnom drvetu, na svakom nivou se razmatra najviše \(4\) čvora, te je maksimalni broj čvorova koji se posećuje jednak \(4\log n\).

Naredni aplet prikazuje korake u izvršavanju svih primera iz ovog poglavlja (prvo se u nizu svi elementi iz segmenta pozicija \([2, 7]\) uvećavaju za \(3\), zatim se svi elementi iz segmenta pozicija \([0, 5]\) uvećavaju za \(2\) i onda se izračunava zbir elemenata na pozicijama iz segmenta \([3, 5]\)).

Primer 1.4.21. Prikažimo rad ove funkcije na primeru lenjog segmentnog drveta sa slike .

Slika 1: Lenjo segmentno drvo: u svakom čvoru čuvamo vrednosti zbira odgovarajućeg segmenta i koeficijenta lenje propagacije. Inicijalno su vrednosti svih koeficijenata lenje propagacije jednaki nula.

Prikažimo kako bismo sve elemente iz segmenta pozicija \([2, 7]\) uvećali za \(3\).

Nakon izvršavanja ovih operacija dobija se drvo prikazano na slici 2. Ovo lenjo segmentno drvo odgovara nizu \(3,4,4,5,9,8,4,7\).

Slika 2: Lenjo segmentno drvo nakon ažuriranja svih elemenata između pozicija 2 i 7 za vrednost 3.

Pretpostavimo da je u dobijenom segmentnom drvetu potrebno elemente iz segmenta \([0, 5]\) uvećavati za vrednost \(2\).

Nakon ovih operacija dobija se drvo prikazano na slici 3. Ovo lenjo segmentno drvo odgovara nizu \(5,6,6,7,11,10,4,7\).

Slika 3: Lenjo segmentno drvo nakon ažuriranja svih elemenata na pozicijama između 0 i 5 za 2.

Funkcija izračunavanja vrednosti zbira segmenta ostaje praktično nepromenjena, osim što se pri ulasku u svaki čvor vrši njegovo ažuriranje, ako je potrebno. Stoga i njena složenost ostaje nepromenjena i iznosi \(O(\log n)\).

// na osnovu lenjog segmentnog drveta koje je smešteno
// u nizove drvo i lenjo od pozicije k u kome se čuvaju zbirovi
// elemenata polaznog niza sa pozicija iz segmenta [x, y]
// izračunava se zbir elemenata polaznog niza
// sa pozicija iz segmenta [a, b]
int saberi(vector<int>& drvo, vector<int>& lenjo, int k, int x, int y, 
           int a, int b) {
  // ažuriramo vrednost u korenu, ako nije ažurna
  if (lenjo[k] != 0) {
     drvo[k] += (y - x + 1) * lenjo[k];
     if (x != y) {
       lenjo[2*k] += lenjo[k];
       lenjo[2*k+1] += lenjo[k];
     }
     lenjo[k] = 0;
  }
  
  // intervali [x, y] i [a, b] su disjunktni
  if (b < x || a > y) return 0;
  // interval [x, y] je potpuno sadržan unutar intervala [a, b]
  if (a <= x && y <= b)
      return drvo[k];
  // intervali [x, y] i [a, b] se seku
  int s = (x + y) / 2;
  return saberi(drvo, lenjo, 2*k, x, s, a, b) + 
         saberi(drvo, lenjo, 2*k+1, s+1, y, a, b);
}

// na osnovu lenjog segmentnog drveta koje je smešteno
// u nizove drvo i lenjo od pozicije 1 u kome se čuvaju zbirovi
// elemenata polaznog niza sa pozicija iz segmenta [0, n-1]
// izračunava se zbir elemenata polaznog niza sa pozicija
// iz segmenta [a, b]
int saberi(vector<int>& drvo, vector<int>& lenjo, int n, int a, int b) {
  // računamo doprinos celog niza, 
  // tj. elemenata iz intervala [0, n-1]
  return saberi(drvo, lenjo, 1, 0, n-1, a, b);
}

Primetimo da se tokom izvršavanja ove funkcije drvo može menjati, pa argumenti funkcije ne smeju biti konstantni (iako se suštinski vrednosti u drvetu ne menjaju, njegova interna reprezentacija, tj. raspodela podataka između nizova drvo i lenjo se može menjati).

Primer 1.4.22. Prikažimo rad prethodne funkcije na tekućem primeru. Razmotrimo kako se za drvo prikazano na slici 3 izračunava zbir elemenata iz segmenta \([3, 5]\).

Dakle, zbir segmenta \([3,5]\) u tekućem drvetu je \(28\). Stanje drveta nakon izvršavanja upita je prikazano na slici 4.

Slika 4: Lenjo segmentno drvo nakon računanja zbira elemenata između pozicija 3 i 5

Dakle, u lenjom segmentnom drvetu ne možemo analizom pojedinačnog čvora (npr. lista) zaključiti koja je tačna vrednost tog čvora, međutim, kada nam bude bila potrebna vrednost nekog čvora u drvetu, mi ćemo se od korena spustiti do tog čvora i, idući tom putanjom, sve vrednosti na toj putanji ažurirati. Na taj način, kada budemo stigli do željenog čvora, imaćemo njegovu ažurnu vrednost, što je jedino i važno.

point query range query point update range update
Lenjo segmentno drvo dobro / \(O(\log{n})\) dobro / \(O(\log{n})\) dobro / \(O(\log{n})\) dobro / \(O(\log{n})\)

  1. Pod ažuriranjem se podrazumeva ili postavljanje vrednosti elementa na datu vrednost ili uvećanje trenutne vrednosti elementa za datu vrednost. U nastavku teksta ćemo razmatrati samo uvećanje trenutne za datu vrednost.↩︎