Lazy segment tree

This commit is contained in:
Antti H S Laaksonen 2017-01-27 00:24:29 +02:00
parent 454dea9d95
commit 4994a68057
1 changed files with 156 additions and 161 deletions

View File

@ -1,29 +1,22 @@
\chapter{Segment trees revisited} \chapter{Segment trees revisited}
\index{segmenttipuu@segmenttipuu} \index{segment tree}
Segmenttipuu on tehokas tietorakenne, A segment tree is a versatile data structure
joka mahdollistaa monenlaisten that can be used in many different situations.
kyselyiden toteuttamisen tehokkaasti. However, there are many topics related to segment trees
Tähän mennessä olemme käyttäneet that we haven't touched yet.
kuitenkin segmenttipuuta melko rajoittuneesti. Now it's time to learn some more advanced variations
Nyt on aika tutustua pintaa syvemmältä of segment trees and see their full potential.
segmenttipuun mahdollisuuksiin.
Tähän mennessä olemme kulkeneet segmenttipuuta So far, we have implemented the operations
\textit{alhaalta ylöspäin} lehdistä juureen. of a segment tree by walking \emph{from the bottom to the top},
Vaihtoehtoinen tapa toteuttaa puun käsittely from the leaves to the root.
on kulkea \textit{ylhäältä alaspäin} juuresta lehtiin. For example, we have calculated the sum of a range $[a,b]$
Tämä kulkusuunta on usein kätevä silloin, as follows (Chapter 9.3):
kun kyseessä on perustilannetta
monimutkaisempi segmenttipuu.
Esimerkiksi välin $[a,b]$ summan laskeminen
segmenttipuussa tapahtuu alhaalta ylöspäin
tuttuun tapaan näin (luku 9.3):
\begin{lstlisting} \begin{lstlisting}
int summa(int a, int b) { int sum(int a, int b) {
a += N; b += N; a += N; b += N;
int s = 0; int s = 0;
while (a <= b) { while (a <= b) {
@ -34,51 +27,48 @@ int summa(int a, int b) {
return s; return s;
} }
\end{lstlisting} \end{lstlisting}
Ylhäältä alaspäin toteutettuna funktiosta tulee: However, in more advanced segment trees,
it's beneficial to implement the operations
in another way, \emph{from the top to the bottom},
from the root to the leaves.
Using this approach, the function becomes as follows:
\begin{lstlisting} \begin{lstlisting}
int summa(int a, int b, int k, int x, int y) { int sum(int a, int b, int k, int x, int y) {
if (b < x || a > y) return 0; if (b < x || a > y) return 0;
if (a == x && b == y) return p[k]; if (a == x && b == y) return p[k];
int d = (y-x+1)/2; int d = (y-x+1)/2;
return summa(a, min(x+d-1,b), 2*k, x, x+d-1) + return sum(a, min(x+d-1,b), 2*k, x, x+d-1) +
summa(max(x+d,a), b, 2*k+1, x+d, y); sum(max(x+d,a), b, 2*k+1, x+d, y);
} }
\end{lstlisting} \end{lstlisting}
Nyt välin $[a,b]$ summan saa laskettua Now we can calulate the sum of the range $[a,b]$
kutsumalla funktiota näin: as follows:
\begin{lstlisting} \begin{lstlisting}
int s = summa(a, b, 1, 0, N-1); int s = sum(a, b, 1, 0, N-1);
\end{lstlisting} \end{lstlisting}
The parameter $k$ is the current position
in array \texttt{p}.
Initially $k$ equals 1, because we begin
at the root of the segment tree.
The range $[x,y]$ corresponds to $k$,
and is initially $[0,N-1]$.
If $[a,b]$ is outside $[x,y]$,
the sum of the range is 0,
and if $[a,b]$ equals $[x,y]$,
the sum can be found in array \texttt{p}.
If $[a,b]$ is completely or partially inside $[x,y]$,
the search continues recursively to the
left and right half of $[x,y]$.
The size of both halves is $d=\frac{1}{2}(y-x+1)$;
the left half is $[x,x+d-1]$
and the right half is $[x+d,y]$.
Parametri $k$ ilmaisee kohdan The following picture shows how the search proceeds
taulukossa \texttt{p}. when calculating the sum of the marked elements.
Aluksi $k$:n arvona on 1, The gray nodes indicate nodes where the recursion
koska summan laskeminen alkaa stops and the sum of the range can be found in array \texttt{p}.
segmenttipuun juuresta.
Väli $[x,y]$ on parametria $k$ vastaava väli,
aluksi koko kyselyalue eli $[0,N-1]$.
Jos väli $[a,b]$ on välin $[x,y]$
ulkopuolella, välin summa on 0.
Jos taas välit $[a,b]$ ja $[x,y]$
ovat samat, summan saa taulukosta \texttt{p}.
Jos väli $[a,b]$ on kokonaan tai osittain välin $[x,y]$
sisällä, haku jatkuu rekursiivisesti
välin $[x,y]$ vasemmasta ja oikeasta puoliskosta.
Kummankin puoliskon koko on $d=\frac{1}{2}(y-x+1)$,
joten vasen puolisko kattaa välin $[x,x+d-1]$
ja oikea puolisko kattaa välin $[x+d,y]$.
Seuraava kuva näyttää,
kuinka haku etenee puussa,
kun lasketaan puun alle
merkityn välin summa.
Harmaat solmut ovat kohtia,
joissa rekursio päättyy ja välin
summan saa taulukosta \texttt{p}.
\\ \\
\begin{center} \begin{center}
\begin{tikzpicture}[scale=0.7] \begin{tikzpicture}[scale=0.7]
@ -169,59 +159,61 @@ summan saa taulukosta \texttt{p}.
\draw [decoration={brace}, decorate, line width=0.5mm] (14,-0.25) -- (5,-0.25); \draw [decoration={brace}, decorate, line width=0.5mm] (14,-0.25) -- (5,-0.25);
\end{tikzpicture} \end{tikzpicture}
\end{center} \end{center}
Myös tässä toteutuksessa kyselyn aikavaativuus on $O(\log n)$, Also in this implementation,
koska haun aikana käsiteltävien solmujen määrä on $O(\log n)$. the time complexity of a range query is $O(\log n)$,
because the total number of processed nodes is $O(\log n)$.
\section{Laiska eteneminen} \section{Lazy propagation}
\index{laiska eteneminen@laiska eteneminen} \index{lazy propagation}
\index{laiska segmenttipuu@laiska segmenttipuu} \index{lazy segment tree}
\key{Laiska eteneminen} Using \key{lazy propagation}, we can construct
mahdollistaa segmenttipuun, a segment tree that supports both range updates
jossa voi sekä muuttaa väliä että kysyä tietoa väliltä and range queries in $O(\log n)$ time.
ajassa $O(\log n)$. The idea is to perform the updates and queries
Ideana on suorittaa muutokset ja kyselyt ylhäältä from the top to the bottom, and process the updates
alaspäin ja toteuttaa muutokset laiskasti niin, \emph{lazily} so that they are propagated
että ne välitetään puussa alaspäin vain silloin, down the tree only when it is necessary.
kun se on välttämätöntä.
Laiskassa segmenttipuussa solmuihin liittyy In a lazy segment tree, nodes contain two types of
kahdenlaista tietoa. information.
Kuten tavallisessa segmenttipuussa, Like in a normal segment tree,
jokaisessa solmussa on sitä vastaavan välin each node contains the sum or some other value
summa tai muu haluttu tieto. of the corresponding subarray.
Tämän lisäksi solmussa voi olla laiskaan etenemiseen In addition, the node may contain information
liittyvää tietoa, jota ei ole vielä välitetty related to lazy updates, which has not been
solmusta alaspäin. propagated yet to its children.
Välin muutostapa voi olla joko There are two possible types for range updates:
\textit{lisäys} tai \textit{asetus}. \emph{addition} and \emph{insertion}.
Lisäyksessä välin jokaiseen alkioon lisätään In addition, each element in the range is
tietty arvo, ja asetuksessa välin increased by some value,
jokainen alkio saa tietyn arvon. and in insertion, each element in the range
Kummankin operaation toteutus on melko samanlainen, is assigned some value.
ja puu voi myös sallia samaan aikaan Both operations can be implemented using
molemmat muutostavat. similar ideas, and it's possible to construct
a tree that supports both the operations
simultaneously.
\subsubsection{Laiska segmenttipuu} \subsubsection{Lazy segment tree}
Let's consider an example where our goal is to
construct a segment tree that supports the following operations:
Tarkastellaan esimerkkinä tilannetta,
jossa segmenttipuun
tulee toteuttaa seuraavat operaatiot:
\begin{itemize} \begin{itemize}
\item lisää jokaisen välin $[a,b]$ alkioon arvo $u$ \item increase each element in $[a,b]$ by $u$
\item laske välin $[a,b]$ alkioiden summa \item calculate the sum of elements in $[a,b]$
\end{itemize} \end{itemize}
Toteutamme puun, jonka jokaisessa We will construct a tree where each node
solmussa on kaksi arvoa $s/z$: contains two values $s/z$:
välin lukujen summa $s$, $s$ denotes the sum of elements in the range,
kuten tavallisessa segmenttipuussa, sekä like in a standard segment tree,
laiska muutos $z$, and $z$ denotes a lazy update,
joka tarkoittaa, which means that all elements in the range
että kaikkiin välin lukuihin tulee lisätä $z$. should be increased by $z$.
Seuraavassa puussa jokaisessa solmussa $z=0$ In the following tree, $z=0$ for all nodes,
eli mitään muutoksia ei ole kesken. so there are no lazy updates.
\begin{center} \begin{center}
\begin{tikzpicture}[scale=0.7] \begin{tikzpicture}[scale=0.7]
\draw (0,0) grid (16,1); \draw (0,0) grid (16,1);
@ -294,19 +286,19 @@ eli mitään muutoksia ei ole kesken.
\end{tikzpicture} \end{tikzpicture}
\end{center} \end{center}
Kun välin $[a,b]$ solmuja kasvatetaan $u$:lla, When a range $[a,b]$ is increased by $u$,
alkaa kulku puun juuresta lehtiä kohti. we walk from the root towards the leaves
Kulun aikana tapahtuu kahdenlaisia muutoksia puun solmuihin: and modify the nodes in the tree as follows:
If the range $[x,y]$ of a node is
completely inside the range $[a,b]$,
we increase the $z$ value of the node by $u$ and stop.
However, if $[x,y]$ only partially belongs to $[a,b]$,
we increase the $s$ value of the node by $hu$,
where $h$ is the size of the intersection of $[a,b]$
and $[x,y]$, and continue our walk recursively in the tree.
Jos solmun väli $[x,y]$ kuuluu kokonaan For example, the following picture shows the tree after
muutettavalle välille $[a,b]$, increasing the elements in the range marked at the bottom by 2:
solmun $z$-arvo kasvaa $u$:llä ja kulku pysähtyy.
Jos taas väli $[x,y]$ kuuluu osittain välille $[a,b]$,
solmun $s$-arvo kasvaa $hu$:llä,
missä $h$ on välien $[a,b]$ ja $[x,y]$ yhteisen osan pituus,
ja kulku jatkuu rekursiivisesti alaspäin.
Kasvatetaan esimerkiksi puun alle merkittyä väliä 2:lla:
\begin{center} \begin{center}
\begin{tikzpicture}[scale=0.7] \begin{tikzpicture}[scale=0.7]
\fill[color=gray!50] (5,0) rectangle (6,1); \fill[color=gray!50] (5,0) rectangle (6,1);
@ -395,23 +387,24 @@ Kasvatetaan esimerkiksi puun alle merkittyä väliä 2:lla:
\end{tikzpicture} \end{tikzpicture}
\end{center} \end{center}
Välin $[a,b]$ summan laskenta tapahtuu myös We also calculate the sum in a range $[a,b]$
kulkuna puun juuresta lehtiä kohti. by walking in the tree from the root towards the leaves.
Jos solmun väli $[x,y]$ kuuluu kokonaan väliin $[a,b]$, If the range $[x,y]$ of a node completely belongs
kyselyn summaan lisätään solmun $s$-arvo to $[a,b]$, we add the $s$ value of the node to the sum.
sekä mahdollinen $z$-arvon tuottama lisäys. Otherwise, we continue the search recursively
Muussa tapauksessa kulku jatkuu rekursiivisesti alaspäin solmun lapsiin. downwards in the tree.
Aina ennen solmun käsittelyä siinä mahdollisesti Always before processing a node,
oleva laiska muutos välitetään tasoa alemmas. the value of the lazy update is propagated
Tämä tapahtuu sekä välin muutoskyselyssä to the children of the node.
että summakyselyssä. This happens both in a range update
Ideana on, että laiska muutos etenee alaspäin and a range query.
vain silloin, kun tämä on välttämätöntä, The idea is that the lazy update will be propagated
jotta puun käsittely on tehokasta. downwards only when it is necessary,
so that the operations are always efficient.
Seuraava kuva näyttää, kuinka äskeinen puu muuttuu, The following picture shows how the tree changes
kun siitä lasketaan puun alle merkityn välin summa: when we calculate the sum in the marked range:
\begin{center} \begin{center}
\begin{tikzpicture}[scale=0.7] \begin{tikzpicture}[scale=0.7]
\draw (0,0) grid (16,1); \draw (0,0) grid (16,1);
@ -495,52 +488,54 @@ kun siitä lasketaan puun alle merkityn välin summa:
\draw[color=blue,thick] (8,1.5) rectangle (12,5.5); \draw[color=blue,thick] (8,1.5) rectangle (12,5.5);
\end{tikzpicture} \end{tikzpicture}
\end{center} \end{center}
Tämän kyselyn seurauksena laiska muutos eteni alaspäin The result of this query was that a lazy update was
laatikolla ympäröidyssä puun osassa. propagated downwards in the nodes that are inside the rectangle.
Laiskaa muutosta täytyi viedä alaspäin, koska kyselyn It was necessary to propagate the lazy update,
kohteena oleva väli osui osittain laiskan muutoksen välille. because some of the updated elements were inside the range.
Huomaa, että joskus puussa olevia laiskoja muutoksia täytyy yhdistää. Note that sometimes it's necessary to combine lazy updates.
Näin tapahtuu silloin, kun solmussa on valmiina laiska muutos This happens when a node already has a lazy update,
ja siihen tulee ylhäältä toinen laiska muutos. and another lazy update will be added to it.
Tässä tapauksessa yhdistäminen on helppoa, In the above tree, it's easy to combine lazy updates
koska muutokset $z_1$ ja $z_2$ aiheuttavat yhdessä muutoksen $z_1+z_2$. because updates $z_1$ and $z_2$ combined equal to update $z_1+z_2$.
\subsubsection{Polynomimuutos} \subsubsection{Polynomial update}
Laiskaa segmenttipuuta voi yleistää niin, A lazy update can be generalized so that it's
että väliä muuttaa polynomi allowed to update a range by a polynomial
\[p(u) = t_k u^k + t_{k-1} u^{k-1} + \cdots + t_0.\] \[p(u) = t_k u^k + t_{k-1} u^{k-1} + \cdots + t_0.\]
Ideana on, että välin ensimmäisen kohdan Here, the update for the first element in the range is $p(0)$,
muutos on $p(0)$, toisen kohdan muutos on $p(1)$ jne., for the second element $p(1)$, etc., so the update
eli välin $[a,b]$ kohdan $i$ muutos on $p(i-a)$. at index $i$ in range $[a,b]$ is $p(i-a)$.
Esimerkiksi polynomin $p(u)=u+1$ lisäys välille For example, adding a polynomial $p(u)=u+1$
$[a,b]$ tarkoittaa, että kohta $a$ kasvaa 1:llä, to range $[a,b]$ means that the element at index $a$
kohta $a+1$ kasvaa 2:lla, kohta $a+2$ kasvaa 3:lla jne. increases by 1, the element at index $a+1$
increases by 2, etc.
Polynomimuutoksen voi toteuttaa niin, A polynomial update can be supported by
että jokaisessa solmussa on $k+2$ arvoa, storing $k+2$ values to each node where $k$
missä $k$ on polynomin asteluku. equals the degree of the polynomial.
Arvo $s$ kertoo solmua vastaavan välin summan kuten ennenkin, The value $s$ is the sum of the elements in the range,
ja arvot $z_0,z_1,\ldots,z_k$ ovat polynomin kertoimet, and values $z_0,z_1,\ldots,z_k$ are the coefficients
joka ilmaisee väliin kohdistuvan laiskan muutoksen. of a polynomial that corresponds to a lazy update.
Nyt välin $[x,y]$ summa on Now, the sum of $[x,y]$ is
\[s+\sum_{u=0}^{y-x} z_k u^k + z_{k-1} u^{k-1} + \cdots + z_0,\] \[s+\sum_{u=0}^{y-x} z_k u^k + z_{k-1} u^{k-1} + \cdots + z_0,\]
jonka saa laskettua tehokkaasti osissa summakaavoilla. that can be efficiently calculated using sum formulas
Esimerkiksi termin $z_0$ summaksi tulee For example, the value $z_0$ corresponds to the sum
$(y-x+1)z_0$ ja termin $z_1 u$ summaksi tulee $(y-x+1)z_0$, and the value $z_1 u$ corresponds to the sum
\[z_1(0+1+\cdots+y-x) = z_1 \frac{(y-x)(y-x+1)}{2} .\] \[z_1(0+1+\cdots+y-x) = z_1 \frac{(y-x)(y-x+1)}{2} .\]
Kun muutos etenee alaspäin puussa, When propagating an update in the tree,
polynomin $p(u)$ indeksointi muuttuu, the indices of the polynomial $p(u)$ change,
koska jokaisella välillä $[x,y]$ because in each range $[x,y]$,
polynomin arvot tulevat kohdista $x=0,1,\ldots,y-x$. the values are
Tämä ei kuitenkaan tuota ongelmia, calculated for $x=0,1,\ldots,y-x$.
koska $p'(u)=p(u+h)$ on aina However, this is not a problem, because
samanasteinen polynomi kuin $p(u)$. $p'(u)=p(u+h)$ is a polynomial
Esimerkiksi jos $p(u)=t_2 u^2+t_1 u-t_0$, niin of equal degree as $p(u)$.
For example, if $p(u)=t_2 u^2+t_1 u-t_0$, then
\[p'(u)=t_2(u+h)^2+t_1(u+h)-t_0=t_2 u^2 + (2ht_2+t_1)u+t_2h^2+t_1h-t_0.\] \[p'(u)=t_2(u+h)^2+t_1(u+h)-t_0=t_2 u^2 + (2ht_2+t_1)u+t_2h^2+t_1h-t_0.\]
\section{Dynaaminen toteutus} \section{Dynaaminen toteutus}