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