cphb/luku14.tex

556 lines
17 KiB
TeX

\chapter{Puiden käsittely}
\index{puu@puu}
\key{Puu} on yhtenäinen, syklitön verkko,
jossa on $n$ solmua ja $n-1$ kaarta.
Jos puusta poistaa yhden kaaren, se ei ole enää yhtenäinen,
ja jos puuhun lisää yhden kaaren, se ei ole enää syklitön.
Puussa pätee myös aina, että
jokaisen kahden puun solmun välillä on yksikäsitteinen polku.
Esimerkiksi seuraavassa puussa on 7 solmua ja 6 kaarta:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
\node[draw, circle] (2) at (2,3) {$2$};
\node[draw, circle] (3) at (0,1) {$4$};
\node[draw, circle] (4) at (2,1) {$5$};
\node[draw, circle] (5) at (4,1) {$6$};
\node[draw, circle] (6) at (-2,3) {$7$};
\node[draw, circle] (7) at (-2,1) {$3$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (1) -- (4);
\path[draw,thick,-] (2) -- (5);
\path[draw,thick,-] (3) -- (6);
\path[draw,thick,-] (3) -- (7);
\end{tikzpicture}
\end{center}
\index{lehti@lehti}
Puun \key{lehdet} ovat solmut,
joiden aste on 1 eli joista lähtee vain yksi kaari.
Esimerkiksi yllä olevan puun lehdet ovat
solmut 3, 5, 6 ja 7.
\index{juuri@juuri}
\index{juurellinen puu@juurellinen puu}
Jos puu on \key{juurellinen}, yksi solmuista
on puun \key{juuri},
jonka alapuolelle muut solmut asettuvat.
Esimerkiksi jos yllä olevassa puussa valitaan
juureksi solmu 1, solmut asettuvat seuraavaan järjestykseen:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
\node[draw, circle] (2) at (2,1) {$2$};
\node[draw, circle] (3) at (-2,1) {$4$};
\node[draw, circle] (4) at (0,1) {$5$};
\node[draw, circle] (5) at (2,-1) {$6$};
\node[draw, circle] (6) at (-3,-1) {$3$};
\node[draw, circle] (7) at (-1,-1) {$7$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (1) -- (4);
\path[draw,thick,-] (2) -- (5);
\path[draw,thick,-] (3) -- (6);
\path[draw,thick,-] (3) -- (7);
\end{tikzpicture}
\end{center}
\index{lapsi@lapsi}
\index{vanhempi@vanhempi}
Juurellisessa puussa solmun \key{lapset}
ovat sen alemman tason naapurit
ja solmun \key{vanhempi}
on sen ylemmän tason naapuri.
Jokaisella solmulla on tasan yksi vanhempi,
paitsi juurella ei ole vanhempaa.
Esimerkiksi yllä olevassa puussa solmun 4
lapset ovat solmut 3 ja 7 ja solmun 4 vanhempi on solmu 1.
\index{alipuu@alipuu}
Juurellisen puun rakenne on \emph{rekursiivinen}:
jokaisesta puun solmusta alkaa \key{alipuu},
jonka juurena on solmu itse ja johon kuuluvat
kaikki solmut, joihin solmusta pääsee kulkemalla alaspäin puussa.
Esimerkiksi solmun 4 alipuussa
ovat solmut 4, 3 ja 7.
\section{Puun läpikäynti}
Puun läpikäyntiin voi käyttää syvyyshakua ja
leveyshakua samaan
tapaan kuin yleisen verkon läpikäyntiin.
Erona on kuitenkin, että puussa ei ole silmukoita,
minkä ansiosta ei tarvitse huolehtia siitä,
että läpikäynti päätyisi tiettyyn
solmuun monesta eri suunnasta.
Tavallisin menetelmä puun läpikäyntiin on
valita tietty solmu juureksi ja aloittaa
siitä syvyyshaku.
Seuraava rekursiivinen funktio toteuttaa sen:
\begin{lstlisting}
void haku(int s, int e) {
// solmun s käsittely tähän
for (auto u : v[s]) {
if (u != e) haku(u, s);
}
}
\end{lstlisting}
Funktion parametrit ovat käsiteltävä solmu $s$
sekä edellinen käsitelty solmu $e$.
Parametrin $e$ ideana on varmistaa, että
läpikäynti etenee vain alaspäin puussa
sellaisiin solmuihin, joita ei ole vielä käsitelty.
Seuraava kutsu käy läpi puun aloittaen juuresta $x$:
\begin{lstlisting}
haku(x, 0);
\end{lstlisting}
Ensimmäisessä kutsussa $e=0$, koska läpikäynti
saa edetä juuresta kaikkiin suuntiin alaspäin.
\subsubsection{Dynaaminen ohjelmointi}
Puun läpikäyntiin voi myös yhdistää dynaamista
ohjelmointia ja laskea sen avulla jotakin tietoa puusta.
Dynaamisen ohjelmoinnin avulla voi esimerkiksi
laskea ajassa $O(n)$ jokaiselle solmulle,
montako solmua sen alipuussa
on tai kuinka pitkä on pisin solmusta
alaspäin jatkuva polku puussa.
Lasketaan esimerkiksi jokaiselle solmulle $s$
sen alipuun solmujen määrä $\texttt{c}[s]$.
Solmun alipuuhun kuuluvat solmu itse
sekä kaikki sen lasten alipuut.
Niinpä solmun alipuun solmujen määrä on
yhden suurempi kuin summa lasten
alipuiden solmujen määristä.
Laskennan voi toteuttaa seuraavasti:
\begin{lstlisting}
void haku(int s, int e) {
c[s] = 1;
for (auto u : v[s]) {
if (u == e) continue;
haku(u, s);
c[s] += c[u];
}
}
\end{lstlisting}
\section{Läpimitta}
\index{lzpimitta@läpimitta}
Puun \key{läpimitta} on pisin polku
kahden puussa olevan solmun välillä.
Esimerkiksi puussa
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
\node[draw, circle] (2) at (2,3) {$2$};
\node[draw, circle] (3) at (0,1) {$4$};
\node[draw, circle] (4) at (2,1) {$5$};
\node[draw, circle] (5) at (4,1) {$6$};
\node[draw, circle] (6) at (-2,3) {$7$};
\node[draw, circle] (7) at (-2,1) {$3$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (1) -- (4);
\path[draw,thick,-] (2) -- (5);
\path[draw,thick,-] (3) -- (6);
\path[draw,thick,-] (3) -- (7);
\end{tikzpicture}
\end{center}
läpimitta on 4, jota vastaa kaksi polkua:
solmujen 3 ja 6 välinen polku sekä
solmujen 7 ja 6 välinen polku.
Käymme seuraavaksi läpi kaksi tehokasta
algoritmia puun läpimitan laskeminen.
Molemmat algoritmit laskevat läpimitan ajassa
$O(n)$.
Ensimmäinen algoritmi perustuu dynaamiseen
ohjelmointiin, ja toinen algoritmi
laskee läpimitan kahden syvyyshaun avulla.
\subsubsection{Algoritmi 1}
Algoritmin alussa
yksi solmuista valitaan puun juureksi.
Tämän jälkeen algoritmi laskee
jokaiseen solmuun,
kuinka pitkä on pisin polku,
joka alkaa jostakin lehdestä,
nousee kyseiseen solmuun asti
ja laskeutuu toiseen lehteen.
Pisin tällainen polku vastaa puun läpimittaa.
Esimerkissä pisin polku alkaa lehdestä 7,
nousee solmuun 1 asti ja laskeutuu
sitten alas lehteen 6:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
\node[draw, circle] (2) at (2,1) {$2$};
\node[draw, circle] (3) at (-2,1) {$4$};
\node[draw, circle] (4) at (0,1) {$5$};
\node[draw, circle] (5) at (2,-1) {$6$};
\node[draw, circle] (6) at (-3,-1) {$3$};
\node[draw, circle] (7) at (-1,-1) {$7$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (1) -- (4);
\path[draw,thick,-] (2) -- (5);
\path[draw,thick,-] (3) -- (6);
\path[draw,thick,-] (3) -- (7);
\path[draw,thick,-,color=red,line width=2pt] (7) -- (3);
\path[draw,thick,-,color=red,line width=2pt] (3) -- (1);
\path[draw,thick,-,color=red,line width=2pt] (1) -- (2);
\path[draw,thick,-,color=red,line width=2pt] (2) -- (5);
\end{tikzpicture}
\end{center}
Algoritmi laskee ensin dynaamisella ohjelmoinnilla
jokaiselle solmulle, kuinka pitkä on pisin polku,
joka lähtee solmusta alaspäin.
Esimerkiksi yllä olevassa puussa pisin polku
solmusta 1 alaspäin on pituudeltaan 2
(vaihtoehdot $1 \rightarrow 4 \rightarrow 3$,
$1 \rightarrow 4 \rightarrow 7$ ja $1 \rightarrow 2 \rightarrow 6$).
Tämän jälkeen algoritmi laskee kullekin solmulle,
kuinka pitkä on pisin polku, jossa solmu on käännekohtana.
Pisin tällainen polku syntyy valitsemalla kaksi lasta,
joista lähtee alaspäin mahdollisimman pitkä polku.
Esimerkiksi yllä olevassa puussa solmun 1 lapsista valitaan solmut 2 ja 4.
\subsubsection{Algoritmi 2}
Toinen tehokas tapa laskea puun läpimitta
perustuu kahteen syvyyshakuun.
Ensin valitaan mikä tahansa solmu $a$ puusta
ja etsitään siitä kaukaisin solmu $b$
syvyyshaulla.
Tämän jälkeen etsitään $b$:stä kaukaisin
solmu $c$ syvyyshaulla.
Puun läpimitta on etäisyys $b$:n ja $c$:n välillä.
Esimerkissä $a$, $b$ ja $c$ voisivat olla:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
\node[draw, circle] (2) at (2,3) {$2$};
\node[draw, circle] (3) at (0,1) {$4$};
\node[draw, circle] (4) at (2,1) {$5$};
\node[draw, circle] (5) at (4,1) {$6$};
\node[draw, circle] (6) at (-2,3) {$7$};
\node[draw, circle] (7) at (-2,1) {$3$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (1) -- (4);
\path[draw,thick,-] (2) -- (5);
\path[draw,thick,-] (3) -- (6);
\path[draw,thick,-] (3) -- (7);
\node[color=red] at (2,1.6) {$a$};
\node[color=red] at (-1.4,3) {$b$};
\node[color=red] at (4,1.6) {$c$};
\path[draw,thick,-,color=red,line width=2pt] (6) -- (3);
\path[draw,thick,-,color=red,line width=2pt] (3) -- (1);
\path[draw,thick,-,color=red,line width=2pt] (1) -- (2);
\path[draw,thick,-,color=red,line width=2pt] (2) -- (5);
\end{tikzpicture}
\end{center}
Menetelmä on tyylikäs, mutta miksi se toimii?
Tässä auttaa tarkastella puuta niin,
että puun läpimittaa vastaava polku on
levitetty vaakatasoon ja muut puun osat
riippuvat siitä alaspäin:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (2,1) {$1$};
\node[draw, circle] (2) at (4,1) {$2$};
\node[draw, circle] (3) at (0,1) {$4$};
\node[draw, circle] (4) at (2,-1) {$5$};
\node[draw, circle] (5) at (6,1) {$6$};
\node[draw, circle] (6) at (0,-1) {$3$};
\node[draw, circle] (7) at (-2,1) {$7$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (1) -- (4);
\path[draw,thick,-] (2) -- (5);
\path[draw,thick,-] (3) -- (6);
\path[draw,thick,-] (3) -- (7);
\node[color=red] at (2,-1.6) {$a$};
\node[color=red] at (-2,1.6) {$b$};
\node[color=red] at (6,1.6) {$c$};
\node[color=red] at (2,1.6) {$x$};
\path[draw,thick,-,color=red,line width=2pt] (7) -- (3);
\path[draw,thick,-,color=red,line width=2pt] (3) -- (1);
\path[draw,thick,-,color=red,line width=2pt] (1) -- (2);
\path[draw,thick,-,color=red,line width=2pt] (2) -- (5);
\end{tikzpicture}
\end{center}
Solmu $x$ on kohta,
jossa polku solmusta $a$ liittyy
läpimittaa vastaavaan polkuun.
Kaukaisin solmu $a$:sta
on solmu $b$, solmu $c$
tai jokin muu solmu, joka
on ainakin yhtä kaukana solmusta $x$.
Niinpä tämä solmu on aina sopiva
valinta läpimittaa vastaavan polun
toiseksi päätesolmuksi.
\section{Solmujen etäisyydet}
Vaikeampi tehtävä on laskea
jokaiselle puun solmulle
jokaiseen suuntaan, mikä on suurin
etäisyys johonkin kyseisessä suunnassa
olevaan solmuun.
Osoittautuu, että tämäkin tehtävä ratkeaa
ajassa $O(n)$ dynaamisella ohjelmoinnilla.
\begin{samepage}
Esimerkkipuussa etäisyydet ovat:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
\node[draw, circle] (2) at (2,3) {$2$};
\node[draw, circle] (3) at (0,1) {$4$};
\node[draw, circle] (4) at (2,1) {$5$};
\node[draw, circle] (5) at (4,1) {$6$};
\node[draw, circle] (6) at (-2,3) {$7$};
\node[draw, circle] (7) at (-2,1) {$3$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (1) -- (4);
\path[draw,thick,-] (2) -- (5);
\path[draw,thick,-] (3) -- (6);
\path[draw,thick,-] (3) -- (7);
\node[color=red] at (0.5,3.2) {$2$};
\node[color=red] at (0.3,2.4) {$1$};
\node[color=red] at (-0.2,2.4) {$2$};
\node[color=red] at (-0.2,1.5) {$3$};
\node[color=red] at (-0.5,1.2) {$1$};
\node[color=red] at (-1.7,2.4) {$4$};
\node[color=red] at (-0.5,0.8) {$1$};
\node[color=red] at (-1.5,0.8) {$4$};
\node[color=red] at (1.5,3.2) {$3$};
\node[color=red] at (1.5,1.2) {$3$};
\node[color=red] at (3.5,1.2) {$4$};
\node[color=red] at (2.2,2.4) {$1$};
\end{tikzpicture}
\end{center}
\end{samepage}
Esimerkiksi solmussa 4
kaukaisin solmu ylöspäin mentäessä
on solmu 6, johon etäisyys on 3 käyttäen
polkua $4 \rightarrow 1 \rightarrow 2 \rightarrow 6$.
\begin{samepage}
Tässäkin tehtävässä hyvä lähtökohta on
valita jokin solmu puun juureksi,
jolloin kaikki etäisyydet alaspäin
saa laskettua dynaamisella ohjelmoinnilla:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
\node[draw, circle] (2) at (2,1) {$2$};
\node[draw, circle] (3) at (-2,1) {$4$};
\node[draw, circle] (4) at (0,1) {$5$};
\node[draw, circle] (5) at (2,-1) {$6$};
\node[draw, circle] (6) at (-3,-1) {$3$};
\node[draw, circle] (7) at (-1,-1) {$7$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (1) -- (4);
\path[draw,thick,-] (2) -- (5);
\path[draw,thick,-] (3) -- (6);
\path[draw,thick,-] (3) -- (7);
\node[color=red] at (-2.5,0.7) {$1$};
\node[color=red] at (-1.5,0.7) {$1$};
\node[color=red] at (2.2,0.5) {$1$};
\node[color=red] at (-0.5,2.8) {$2$};
\node[color=red] at (0.2,2.5) {$1$};
\node[color=red] at (0.5,2.8) {$2$};
\end{tikzpicture}
\end{center}
\end{samepage}
Jäljelle jäävä tehtävä on laskea etäisyydet ylöspäin.
Tämä onnistuu tekemällä puuhun toinen läpikäynti,
joka pitää mukana tietoa,
mikä on suurin etäisyys solmun vanhemmasta
johonkin toisessa suunnassa olevaan solmuun.
Esimerkiksi solmun 2
suurin etäisyys ylöspäin on yhtä suurempi
kuin solmun 1 suurin etäisyys
johonkin muuhun suuntaan kuin solmuun 2:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
\node[draw, circle] (2) at (2,1) {$2$};
\node[draw, circle] (3) at (-2,1) {$4$};
\node[draw, circle] (4) at (0,1) {$5$};
\node[draw, circle] (5) at (2,-1) {$6$};
\node[draw, circle] (6) at (-3,-1) {$3$};
\node[draw, circle] (7) at (-1,-1) {$7$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (1) -- (4);
\path[draw,thick,-] (2) -- (5);
\path[draw,thick,-] (3) -- (6);
\path[draw,thick,-] (3) -- (7);
\path[draw,thick,-,color=red,line width=2pt] (1) -- (2);
\path[draw,thick,-,color=red,line width=2pt] (1) -- (3);
\path[draw,thick,-,color=red,line width=2pt] (3) -- (7);
\end{tikzpicture}
\end{center}
Lopputuloksena on etäisyydet kaikista solmuista
kaikkiin suuntiin:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
\node[draw, circle] (2) at (2,1) {$2$};
\node[draw, circle] (3) at (-2,1) {$4$};
\node[draw, circle] (4) at (0,1) {$5$};
\node[draw, circle] (5) at (2,-1) {$6$};
\node[draw, circle] (6) at (-3,-1) {$3$};
\node[draw, circle] (7) at (-1,-1) {$7$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (1) -- (4);
\path[draw,thick,-] (2) -- (5);
\path[draw,thick,-] (3) -- (6);
\path[draw,thick,-] (3) -- (7);
\node[color=red] at (-2.5,0.7) {$1$};
\node[color=red] at (-1.5,0.7) {$1$};
\node[color=red] at (2.2,0.5) {$1$};
\node[color=red] at (-0.5,2.8) {$2$};
\node[color=red] at (0.2,2.5) {$1$};
\node[color=red] at (0.5,2.8) {$2$};
\node[color=red] at (-3,-0.4) {$4$};
\node[color=red] at (-1,-0.4) {$4$};
\node[color=red] at (-2,1.6) {$3$};
\node[color=red] at (2,1.6) {$3$};
\node[color=red] at (2.2,-0.4) {$4$};
\node[color=red] at (0.2,1.6) {$3$};
\end{tikzpicture}
\end{center}
%
% Kummankin läpikäynnin aikavaativuus on $O(n)$,
% joten algoritmin kokonais\-aikavaativuus on $O(n)$.
\section{Binääripuut}
\index{binxxripuu@binääripuu}
\begin{samepage}
\key{Binääripuu} on juurellinen puu,
jonka jokaisella solmulla on vasen ja oikea alipuu.
On mahdollista, että alipuu on tyhjä,
jolloin puu ei jatku siitä pidemmälle alaspäin.
Binääripuun jokaisella solmulla on 0, 1 tai 2 lasta.
Esimerkiksi seuraava puu on binääripuu:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,0) {$1$};
\node[draw, circle] (2) at (-1.5,-1.5) {$2$};
\node[draw, circle] (3) at (1.5,-1.5) {$3$};
\node[draw, circle] (4) at (-3,-3) {$4$};
\node[draw, circle] (5) at (0,-3) {$5$};
\node[draw, circle] (6) at (-1.5,-4.5) {$6$};
\node[draw, circle] (7) at (3,-3) {$7$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (2) -- (4);
\path[draw,thick,-] (2) -- (5);
\path[draw,thick,-] (5) -- (6);
\path[draw,thick,-] (3) -- (7);
\end{tikzpicture}
\end{center}
\end{samepage}
Binääripuun solmuilla on kolme luontevaa järjestystä,
jotka syntyvät rekursiivisesta läpikäynnistä:
\index{esijxrjestys@esijärjestys}
\index{sisxjxrjestys@sisäjärjestys}
\index{jxlkijxrjestys@jälkijärjestys}
\begin{itemize}
\item \key{esijärjestys}: juuri, vasen alipuu, oikea alipuu
\item \key{sisäjärjestys}: vasen alipuu, juuri, oikea alipuu
\item \key{jälkijärjestys}: vasen alipuu, oikea alipuu, juuri
\end{itemize}
Esimerkissä kuvatun puun esijärjestys on
$[1,2,4,5,6,3,7]$,
sisäjärjestys on $[4,2,6,5,1,3,7]$
ja jälkijärjestys on $[4,6,5,2,7,3,1]$.
Osoittautuu, että tietämällä puun esijärjestyksen
ja sisäjärjestyksen voi päätellä puun koko rakenteen.
Esimerkiksi yllä oleva puu on ainoa mahdollinen
puu, jossa esijärjestys on
$[1,2,4,5,6,3,7]$ ja sisäjärjestys on $[4,2,6,5,1,3,7]$.
Vastaavasti myös jälkijärjestys ja sisäjärjestys
määrittävät puun rakenteen.
Tilanne on toinen, jos tiedossa on vain
esijärjestys ja jälkijärjestys.
Nämä järjestykset eivät
kuvaa välttämättä puuta yksikäsitteisesti.
Esimerkiksi molemmissa puissa
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,0) {$1$};
\node[draw, circle] (2) at (-1.5,-1.5) {$2$};
\path[draw,thick,-] (1) -- (2);
\node[draw, circle] (1b) at (0+4,0) {$1$};
\node[draw, circle] (2b) at (1.5+4,-1.5) {$2$};
\path[draw,thick,-] (1b) -- (2b);
\end{tikzpicture}
\end{center}
esijärjestys on $(1,2)$ ja jälkijärjestys on $(2,1)$,
mutta siitä huolimatta puiden rakenteet eivät ole samat.