Chapter 14 first version

This commit is contained in:
Antti H S Laaksonen 2017-01-07 23:31:41 +02:00
parent a0f2d60b2b
commit f1011ab0f6
1 changed files with 199 additions and 209 deletions

View File

@ -1,16 +1,16 @@
\chapter{Puiden käsittely}
\chapter{Tree algorithms}
\index{puu@puu}
\index{tree}
\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:
A \key{tree} is a connected, acyclic graph
that contains $n$ nodes and $n-1$ edges.
Removing any edge from a tree divides it
into two components,
and adding any edge to a tree creates a cycle.
Moreover, there is always a unique path between any
two nodes in a tree.
For example, the following tree contains 7 nodes and 6 edges:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
@ -29,21 +29,21 @@ Esimerkiksi seuraavassa puussa on 7 solmua ja 6 kaarta:
\end{tikzpicture}
\end{center}
\index{lehti@lehti}
\index{leaf}
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.
The \key{leaves} of a tree are nodes
with degree 1, i.e., with only one neighbor.
For example, the leaves in the above tree
are nodes 3, 5, 6 and 7.
\index{juuri@juuri}
\index{juurellinen puu@juurellinen puu}
\index{root}
\index{rooted tree}
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:
In a \key{rooted} tree, one of the nodes
is chosen to be a \key{root}, and all other nodes are
placed underneath the root.
For example, in the following tree,
node 1 is the root of the tree.
\begin{center}
\begin{tikzpicture}[scale=0.9]
@ -63,84 +63,82 @@ juureksi solmu 1, solmut asettuvat seuraavaan järjestykseen:
\end{tikzpicture}
\end{center}
\index{lapsi@lapsi}
\index{vanhempi@vanhempi}
\index{child}
\index{parent}
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.
In a rooted tree, the \key{childern} of a node
are its lower neighbors, and the \key{parent} of a node
is its upper neighbor.
Each node has exactly one parent,
except that the root doesn't have a parent.
For example, in the above tree,
the childern of node 4 are nodes 3 and 7,
and the parent is node 1.
\index{alipuu@alipuu}
\index{subtree}
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.
The structure of a rooted tree is \emph{recursive}:
each node in the tree is the root of a \key{subtree}
that contains the node itself and all other nodes
that can be reached by travelling downwards in the tree.
For example, in the above tree, the subtree of node 4
contains nodes 4, 3 and 7.
\section{Puun läpikäynti}
\section{Tree search}
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.
Depth-first search and breadth-first search
can be used for going through the nodes in a tree.
However, the search is easier to implement than
for a general graph, because
there are no cycles in the tree, and it is not
possible that the search would visit a node several times.
Tavallisin menetelmä puun läpikäyntiin on
valita tietty solmu juureksi ja aloittaa
siitä syvyyshaku.
Seuraava rekursiivinen funktio toteuttaa sen:
Often, we start a depth-first search from a chosen
root node.
The following recursive function implements it:
\begin{lstlisting}
void haku(int s, int e) {
// solmun s käsittely tähän
void dfs(int s, int e) {
// process node s
for (auto u : v[s]) {
if (u != e) haku(u, s);
if (u != e) dfs(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.
The function parameters are the current node $s$
and the previous node $e$.
The idea of the parameter $e$ is to ensure
that the search only proceeds downwards in the tree
towards nodes that have not been visited yet.
Seuraava kutsu käy läpi puun aloittaen juuresta $x$:
The following function call starts the search
at node $x$:
\begin{lstlisting}
haku(x, 0);
dfs(x, 0);
\end{lstlisting}
Ensimmäisessä kutsussa $e=0$, koska läpikäynti
saa edetä juuresta kaikkiin suuntiin alaspäin.
In the first call $e=0$ because there is no
previous node, and it is allowed
to proceed to any direction in the tree.
\subsubsection{Dynaaminen ohjelmointi}
\subsubsection{Dynamic programming}
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.
We can also use dynamic programming to calculate
some information from the tree during the search.
Using dynamic programming, we can, for example,
calculate in $O(n)$ time for each node the
number of nodes in its subtree,
or the length of the longest path downwards
that begins at the node.
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:
As an example, let's calculate for each node $s$
a value $\texttt{c}[s]$: the number of nodes in its subtree.
The subtree contains the node itself and
all nodes in the subtrees of its children.
Thus, we can calculate the number of nodes
recursively using the following code:
\begin{lstlisting}
void haku(int s, int e) {
@ -153,13 +151,14 @@ void haku(int s, int e) {
}
\end{lstlisting}
\section{Läpimitta}
\section{Diameter}
\index{lzpimitta@läpimitta}
\index{diameter}
Puun \key{läpimitta} on pisin polku
kahden puussa olevan solmun välillä.
Esimerkiksi puussa
The \key{diameter} of a tree
is the length of the longest path
between two nodes in the tree.
For example, in the tree
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
@ -177,33 +176,28 @@ Esimerkiksi puussa
\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.
the diameter is 4, and it corresponds to two paths:
the path between nodes 3 and 6,
and the path between nodes 7 and 6.
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.
Next we will learn two efficient algorithms
for calculating the diameter of a tree.
Both algorithms calculate the diameter in $O(n)$ time.
The first algorithm is based on dynamic programming,
and the second algorithm uses two depth-first searches
to calculate the diameter.
\subsubsection{Algoritmi 1}
\subsubsection{Algorithm 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.
First, one of the nodes is chosen to be the root.
After this, the algorithm calculates for each node
the length of the longest path that begins at some leaf,
ascends to the node and then descends to another leaf.
The length of the
longest such path equals the diameter of the tree.
Esimerkissä pisin polku alkaa lehdestä 7,
nousee solmuun 1 asti ja laskeutuu
sitten alas lehteen 6:
In the example case, the longest path begins at node 7,
ascends to node 1, and then descends to node 6:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
@ -227,32 +221,32 @@ sitten alas lehteen 6:
\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$).
The algorithm first calculates using dynamic programming
for each node the length of the longest path
that goes downwards from the node.
For example, in the above tree,
the longest path from node 1 downwards has length 2
(the path can be $1 \rightarrow 4 \rightarrow 3$,
$1 \rightarrow 4 \rightarrow 7$ or $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.
After this, the algorithm calculates for each node
the length of the longest path where the node
is the turning point of the path.
The longest such path can be found by selecting
two children with longest paths downwards.
For example, in the above graph,
nodes 2 and 4 are chosen for node 1.
\subsubsection{Algoritmi 2}
\subsubsection{Algorithm 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ä.
Another efficient way to calculate the diameter
of a tree is based on two depth-first searches.
First, we choose an arbitrary node $a$ in the tree
and find a node $b$ with maximum distance to $a$.
Then, we find a node $c$ with maximum distance to $b$.
The diameter is the distance between nodes $b$ and $c$.
Esimerkissä $a$, $b$ ja $c$ voisivat olla:
In the example case, $a$, $b$ and $c$ could be:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
@ -279,12 +273,12 @@ Esimerkissä $a$, $b$ ja $c$ voisivat olla:
\end{tikzpicture}
\end{center}
Menetelmä on tyylikäs, mutta miksi se toimii?
This is an elegant method, but why does it work?
Tässä auttaa tarkastella puuta niin,
että puun läpimittaa vastaava polku on
levitetty vaakatasoon ja muut puun osat
riippuvat siitä alaspäin:
It helps to draw the tree differently so that
the path that corresponds to the diameter
is horizontal, and all other
nodes hang from it:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (2,1) {$1$};
@ -312,29 +306,25 @@ riippuvat siitä alaspäin:
\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.
Node $x$ indicates the place where the path
from node $a$ joins the path that corresponds
to the diameter.
The farthest node from $a$
is node $b$, node $c$ or some other node
that is at least as far from node $x$.
Thus, this node can always be chosen for
a starting node of a path that corresponds to the diameter.
\section{Solmujen etäisyydet}
\section{Distances between nodes}
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.
A more difficult problem is to calculate
for each node in the tree and for each direction,
the maximum distance to a node in that direction.
It turns out that this can be calculated in
$O(n)$ time as well using dynamic programming.
\begin{samepage}
Esimerkkipuussa etäisyydet ovat:
In the example case, the distances are as follows:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
@ -365,16 +355,16 @@ Esimerkkipuussa etäisyydet ovat:
\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$.
For example, the furthest node from node 4
upwards is node 6, and the distance to this
node is 3 using the path
$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:
Also in this problem, a good starting point
is to root the tree.
After this, all distances downwards can
be calculated using dynamic programming:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
@ -403,16 +393,14 @@ saa laskettua dynaamisella ohjelmoinnilla:
\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.
The remaining task is to calculate the distances upwards.
This can be done by going through the nodes once again
and keeping track of the largest distance from the parent
of the current node to some other node in another direction.
Esimerkiksi solmun 2
suurin etäisyys ylöspäin on yhtä suurempi
kuin solmun 1 suurin etäisyys
johonkin muuhun suuntaan kuin solmuun 2:
For example, the distance from node 2 upwards
is one larger than the distance from node 1
downwards in some other direction than node 2:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
@ -435,8 +423,8 @@ johonkin muuhun suuntaan kuin solmuun 2:
\end{tikzpicture}
\end{center}
Lopputuloksena on etäisyydet kaikista solmuista
kaikkiin suuntiin:
Finally, we can calculate the distances for all nodes
and all directions:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,3) {$1$};
@ -471,23 +459,20 @@ kaikkiin suuntiin:
\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}
\section{Binary trees}
\index{binxxripuu@binääripuu}
\index{binary tree}
\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:
A \key{binary tree} is a rooted tree
where each node has a left subtree
and a right subtree.
It is possible that a subtree of a node is empty.
Thus, every node in a binary tree has
0, 1 or 2 children.
For example, the following tree is a binary tree:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,0) {$1$};
@ -508,37 +493,42 @@ Esimerkiksi seuraava puu on binääripuu:
\end{center}
\end{samepage}
Binääripuun solmuilla on kolme luontevaa järjestystä,
jotka syntyvät rekursiivisesta läpikäynnistä:
\index{pre-order}
\index{in-order}
\index{post-order}
\index{esijxrjestys@esijärjestys}
\index{sisxjxrjestys@sisäjärjestys}
\index{jxlkijxrjestys@jälkijärjestys}
The nodes in a binary tree have three natural
orders that correspond to different ways to
recursively traverse the nodes:
\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
\item \key{pre-order}: first process the root,
then traverse the left subtree, then traverse the right subtree
\item \key{in-order}: first traverse the left subtree,
then process the root, then traverse the right subtree
\item \key{post-order}: first traverse the left subtree,
then traverse the right subtree, then process the root
\end{itemize}
Esimerkissä kuvatun puun esijärjestys on
For the above tree, the nodes in
pre-order are
$[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]$.
in in-order $[4,2,6,5,1,3,7]$
and in post-order $[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.
If we know the pre-order and the in-order
of a tree, we can find out the exact structure of the tree.
For example, the tree above is the only possible tree
with pre-order $[1,2,4,5,6,3,7]$ and
in-order $[4,2,6,5,1,3,7]$.
Correspondingly, the post-order and the in-order
also determine the structure of a tree.
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
However, the situation is different if we only know
the pre-order and the post-order of a tree.
In this case, there may be more than one tree
that match the orders.
For example, in both of the trees
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle] (1) at (0,0) {$1$};
@ -550,6 +540,6 @@ Esimerkiksi molemmissa puissa
\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.
the pre-order is $[1,2]$ and the post-order is $[2,1]$
but the trees have different structures.