Backtracking and pruning

This commit is contained in:
Antti H S Laaksonen 2017-01-01 20:47:18 +02:00
parent 9de7221c10
commit d3d26a99dc
1 changed files with 164 additions and 159 deletions

View File

@ -228,26 +228,28 @@ do {
} while (next_permutation(v.begin(),v.end())); } while (next_permutation(v.begin(),v.end()));
\end{lstlisting} \end{lstlisting}
\section{Peruuttava haku} \section{Backtracking}
\index{peruuttava haku@peruuttava haku} \index{backtracking}
\key{Peruuttava haku} A \key{backtracking} algorithm
aloittaa ratkaisun etsimisen tyhjästä begins from an empty solution
ja laajentaa ratkaisua askel kerrallaan. and extends the solution step by step.
Joka askeleella haku haarautuu kaikkiin At each step, the search branches
mahdollisiin suuntiin, joihin ratkaisua voi laajentaa. to all possible directions how the solution
Haaran tutkimisen jälkeen haku peruuttaa takaisin can be extended.
ja jatkaa muihin mahdollisiin suuntiin. After processing one branch, the search
continues to other possible directions.
\index{kuningatarongelma} \index{queen problem}
Tarkastellaan esimerkkinä \key{kuningatarongelmaa}, As an example, consider the \key{queen problem}
jossa laskettavana on, where our task is to calculate the number
monellako tavalla $n \times n$ -shakkilaudalle of ways we can place $n$ queens to
voidaan asettaa $n$ kuningatarta niin, an $n \times n$ chessboard so that
että mitkään kaksi kuningatarta eivät uhkaa toisiaan. no two queens attack each other.
Esimerkiksi kun $n=4$, mahdolliset ratkaisut ovat seuraavat: For example, when $n=4$,
there are two possible solutions for the problem:
\begin{center} \begin{center}
\begin{tikzpicture}[scale=.65] \begin{tikzpicture}[scale=.65]
@ -268,16 +270,16 @@ Esimerkiksi kun $n=4$, mahdolliset ratkaisut ovat seuraavat:
\end{tikzpicture} \end{tikzpicture}
\end{center} \end{center}
Tehtävän voi ratkaista peruuttavalla haulla The problem can be solved using backtracking
muodostamalla ratkaisua rivi kerrallaan. by placing queens to the board row by row.
Jokaisella rivillä täytyy valita yksi ruuduista, More precisely, we should place exactly one queen
johon sijoitetaan kuningatar niin, to each row so that no queen attacks
ettei se uhkaa mitään aiemmin lisättyä kuningatarta. any of the queens placed before.
Ratkaisu on valmis, kun viimeisellekin A solution is ready when we have placed all
riville on lisätty kuningatar. $n$ queens to the board.
Esimerkiksi kun $n=4$, osa peruuttavan haun muodostamasta For example, when $n=4$, the tree produced by
puusta näyttää seuraavalta: the backtracking algorithm begins like this:
\begin{center} \begin{center}
\begin{tikzpicture}[scale=.55] \begin{tikzpicture}[scale=.55]
@ -327,19 +329,16 @@ puusta näyttää seuraavalta:
\end{tikzpicture} \end{tikzpicture}
\end{center} \end{center}
Kuvan alimmalla tasolla kolme ensimmäistä osaratkaisua At the bottom level, the three first subsolutions
eivät kelpaa, koska niissä kuningattaret uhkaavat are not valid because the queens attack each other.
toisiaan. However, the fourth subsolution is valid
Sen sijaan neljäs osaratkaisu kelpaa, and it can be extended to a full solution by
ja sitä on mahdollista laajentaa loppuun asti placing two more queens to the board.
kokonaiseksi ratkaisuksi
asettamalla vielä kaksi kuningatarta laudalle.
\begin{samepage} \begin{samepage}
Seuraava koodi toteuttaa peruuttavan haun: The following code implements the search:
\begin{lstlisting} \begin{lstlisting}
void haku(int y) { void search(int y) {
if (y == n) { if (y == n) {
c++; c++;
return; return;
@ -347,32 +346,32 @@ void haku(int y) {
for (int x = 0; x < n; x++) { for (int x = 0; x < n; x++) {
if (r1[x] || r2[x+y] || r3[x-y+n-1]) continue; if (r1[x] || r2[x+y] || r3[x-y+n-1]) continue;
r1[x] = r2[x+y] = r3[x-y+n-1] = 1; r1[x] = r2[x+y] = r3[x-y+n-1] = 1;
haku(y+1); search(y+1);
r1[x] = r2[x+y] = r3[x-y+n-1] = 0; r1[x] = r2[x+y] = r3[x-y+n-1] = 0;
} }
} }
\end{lstlisting} \end{lstlisting}
\end{samepage} \end{samepage}
Haku alkaa kutsumalla funktiota \texttt{haku(0)}. The search begins by calling \texttt{search(0)}.
Laudan koko on muuttujassa $n$, The size of the board is in the variable $n$,
ja koodi laskee ratkaisuiden määrän and the code calculates the number of solutions
muuttujaan $c$. to the variable $c$.
Koodi olettaa, että laudan vaaka- ja pystyrivit The code assumes that the rows and columns
on numeroitu 0:sta alkaen. of the board are numbered from 0.
Funktio asettaa kuningattaren vaakariville $y$, The function places a queen to row $y$
kun $0 \le y < n$. when $0 \le y < n$.
Jos taas $y=n$, yksi ratkaisu on valmis Finally, if $y=n$, one solution has been found
ja funktio kasvattaa muuttujaa $c$. and the variable $c$ is increased by one.
Taulukko \texttt{r1} pitää kirjaa, The array \texttt{r1} keeps track of the columns
millä laudan pystyriveillä on jo kuningatar. that already contain a queen.
Vastaavasti taulukot \texttt{r2} ja \texttt{r3} Similarly, the arrays \texttt{r2} and \texttt{r3}
pitävät kirjaa vinoriveistä. keep track of the diagonals.
Tällaisille riveille ei voi laittaa enää toista It is not allowed to add another queen to a
kuningatarta. column or to a diagonal.
Esimerkiksi $4 \times 4$ -laudan tapauksessa For example, the rows and the diagonals of
rivit on numeroitu seuraavasti: the $4 \times 4$ board are numbered as follows:
\begin{center} \begin{center}
\begin{tikzpicture}[scale=.65] \begin{tikzpicture}[scale=.65]
@ -439,36 +438,38 @@ rivit on numeroitu seuraavasti:
\end{tikzpicture} \end{tikzpicture}
\end{center} \end{center}
Koodin avulla selviää esimerkiksi, Using the presented backtracking
että tapauksessa $n=8$ on 92 tapaa sijoittaa 8 algorithm, we can calculate that,
kuningatarta $8 \times 8$ -laudalle. for example, there are 92 ways to place 8
Kun $n$ kasvaa, koodi hidastuu nopeasti, queens to an $8 \times 8$ chessboard.
koska ratkaisujen määrä kasvaa räjähdysmäisesti. When $n$ increases, the search quickly becomes slow
Tapauksen $n=16$ laskeminen vie jo noin minuutin because the number of the solutions increases
nykyaikaisella tietokoneella (14772512 ratkaisua). exponentially.
For example, calculating the ways to
place 16 queens to the $16 \times 16$
chessboard already takes about a minute
(there are 14772512 solutions).
\section{Haun optimointi} \section{Pruning the search}
Peruuttavaa hakua on usein mahdollista tehostaa A backtracking algorithm can often be optimized
erilaisten optimointien avulla. by pruning the search tree.
Tavoitteena on lisätä hakuun ''älykkyyttä'' The idea is to add ''intelligence'' to the algorithm
niin, että haku pystyy havaitsemaan so that it will notice as soon as possible
mahdollisimman aikaisin, if is not possible to extend a subsolution into
jos muodosteilla oleva ratkaisu ei voi a full solution.
johtaa kokonaiseen ratkaisuun. This kind of optimization can have a tremendous
Tällaiset optimoinnit karsivat haaroja effect on the efficiency of the search.
hakupuusta, millä voi olla suuri vaikutus
peruuttavan haun tehokkuuteen.
Tarkastellaan esimerkkinä tehtävää, Let us consider a problem where
jossa laskettavana on reittien määrä our task is to calculate the number of paths
$n \times n$ -ruudukon in an $n \times n$ grid from the upper-left corner
vasemmasta yläkulmasta oikeaan alakulmaan, to the lower-right corner so that each square
kun reitin aikana tulee käydä tarkalleen kerran will be visited exactly once.
jokaisessa ruudussa. For example, in the $7 \times 7$ grid,
Esimerkiksi $7 \times 7$ -ruudukossa on there are 111712 possible paths from the
111712 mahdollista reittiä vasemmasta yläkulmasta lower-right corner to the upper-right corner.
oikeaan alakulmaan, joista yksi on seuraava: One of the paths is as follows:
\begin{center} \begin{center}
\begin{tikzpicture}[scale=.55] \begin{tikzpicture}[scale=.55]
@ -486,38 +487,39 @@ oikeaan alakulmaan, joista yksi on seuraava:
\end{tikzpicture} \end{tikzpicture}
\end{center} \end{center}
Keskitymme seuraavaksi nimenomaan tapaukseen $7 \times 7$, We will concentrate on the $7 \times 7$ case
koska se on laskennallisesti sopivan haastava. because it is computationally suitable difficult.
Lähdemme liikkeelle suoraviivaisesta peruuttavaa hakua We begin with a straightforward backtracking algorithm,
käyttävästä algoritmista and then optimize it step by step using observations
ja teemme siihen pikkuhiljaa optimointeja, how the search tree can be pruned.
jotka nopeuttavat hakua eri tavoin. After each optimization, we measure the running time
Mittaamme jokaisen optimoinnin jälkeen of the algorithm and the number of recursive calls,
algoritmin suoritusajan sekä rekursiokutsujen yhteismäärän, so that we will clearly see the effect of each
jotta näemme selvästi, mikä vaikutus kullakin optimization on the efficiency of the search.
optimoinnilla on haun tehokkuuteen.
\subsubsection{Perusalgoritmi} \subsubsection{Basic algorithm}
Algoritmin ensimmäisessä versiossa ei ole mitään optimointeja, The first version of the algorithm doesn't contain
vaan peruuttava haku käy läpi kaikki mahdolliset tavat any optimizations. We simply use backtracking to generate
muodostaa reitti ruudukon vasemmasta yläkulmasta all possible paths from the upper-left corner to
oikeaan alakulmaan. the lower-right corner.
\begin{itemize} \begin{itemize}
\item \item
suoritusaika: 483 sekuntia running time: 483 seconds
\item \item
rekursiokutsuja: 76 miljardia recursive calls: 76 billions
\end{itemize} \end{itemize}
\subsubsection{Optimointi 1} \subsubsection{Optimization 1}
Reitin ensimmäinen askel on joko alaspäin The first step in a solution is either
tai oikealle. Tästä valinnasta seuraavat tilanteet downward or to the right.
ovat symmetrisiä ruudukon lävistäjän suhteen. There are always two paths that
Esimerkiksi seuraavat ratkaisut ovat are symmetric
symmetrisiä keskenään: about the diagonal of the grid
after the first step.
For example, the following paths are symmetric:
\begin{center} \begin{center}
\begin{tabular}{ccc} \begin{tabular}{ccc}
@ -552,23 +554,24 @@ symmetrisiä keskenään:
\end{tabular} \end{tabular}
\end{center} \end{center}
Tämän ansiosta voimme tehdä päätöksen, Thus, we can decide that the first step
että reitin ensimmäinen askel on alaspäin, in the solution is always downward,
ja kertoa lopuksi reittien määrän 2:lla. and finally multiply the number of the solutions by two.
\begin{itemize} \begin{itemize}
\item \item
suoritusaika: 244 sekuntia running time: 244 seconds
\item \item
rekursiokutsuja: 38 miljardia recursive calls: 38 billions
\end{itemize} \end{itemize}
\subsubsection{Optimointi 2} \subsubsection{Optimization 2}
Jos reitti menee oikean alakulman ruutuun ennen kuin If the path reaches the lower-right square
se on käynyt kaikissa muissa ruuduissa, before it has visited all other squares of the grid,
siitä ei voi mitenkään enää saada kelvollista ratkaisua. it is clear that
Näin on esimerkiksi seuraavassa tilanteessa: it will not be possible to complete the solution.
An example of this is the following case:
\begin{center} \begin{center}
\begin{tikzpicture}[scale=.55] \begin{tikzpicture}[scale=.55]
@ -582,23 +585,23 @@ Näin on esimerkiksi seuraavassa tilanteessa:
\end{scope} \end{scope}
\end{tikzpicture} \end{tikzpicture}
\end{center} \end{center}
Niinpä voimme keskeyttää hakuhaaran heti, Using this observation, we can terminate the search branch
jos tulemme oikean alakulman ruutuun liian aikaisin. immediately if we reach the lower-right square too early.
\begin{itemize} \begin{itemize}
\item \item
suoritusaika: 119 sekuntia running time: 119 seconds
\item \item
rekursiokutsuja: 20 miljardia recursive calls: 20 billions
\end{itemize} \end{itemize}
\subsubsection{Optimointi 3} \subsubsection{Optimization 3}
Jos reitti osuu seinään niin, että kummallakin puolella If the path touches the wall so that there is
on ruutu, jossa reitti ei ole vielä käynyt, an unvisited square at both sides,
ruudukko jakautuu kahteen osaan. the grid splits into two parts.
Esimerkiksi seuraavassa tilanteessa For example, in the following case
sekä vasemmalla että both the left and the right squares
oikealla puolella on tyhjä ruutu: are unvisited:
\begin{center} \begin{center}
\begin{tikzpicture}[scale=.55] \begin{tikzpicture}[scale=.55]
@ -612,30 +615,31 @@ oikealla puolella on tyhjä ruutu:
\end{scope} \end{scope}
\end{tikzpicture} \end{tikzpicture}
\end{center} \end{center}
Nyt ei ole enää mahdollista käydä kaikissa ruuduissa, Now it will not be possible to visit every square,
joten voimme keskeyttää hakuhaaran. so we can terminate the search branch.
Tämä optimointi on hyvin hyödyllinen: This optimization is very useful:
\begin{itemize} \begin{itemize}
\item \item
suoritusaika: 1{,}8 sekuntia running time: 1.8 seconds
\item \item
rekursiokutsuja: 221 miljoonaa recursive calls: 221 millions
\end{itemize} \end{itemize}
\subsubsection{Optimointi 4} \subsubsection{Optimization 4}
Äskeisen optimoinnin ideaa voi yleistää: The idea of the previous optimization
ruudukko jakaantuu kahteen osaan, can be generalized:
jos nykyisen ruudun ylä- ja alapuolella on the grid splits into two parts
tyhjä ruutu sekä vasemmalla ja oikealla if the top and bottom neighbors
puolella on seinä tai aiemmin käyty ruutu of the current square are unvisited and
(tai päinvastoin). the left and right neighbors are
wall or visited (or vice versa).
Esimerkiksi seuraavassa tilanteessa For example, in the following case
nykyisen ruudun ylä- ja alapuolella on the top and bottom neighbors are unvisited,
tyhjä ruutu eikä reitti voi enää edetä so the path cannot visit all squares
molempiin ruutuihin: in the grid anymore:
\begin{center} \begin{center}
\begin{tikzpicture}[scale=.55] \begin{tikzpicture}[scale=.55]
\begin{scope} \begin{scope}
@ -648,32 +652,33 @@ molempiin ruutuihin:
\end{scope} \end{scope}
\end{tikzpicture} \end{tikzpicture}
\end{center} \end{center}
Haku tehostuu entisestään, kun keskeytämme The search becomes even faster when we terminate
hakuhaaran kaikissa tällaisissa tapauksissa: the search branch in all such cases:
\begin{itemize} \begin{itemize}
\item \item
suoritusaika: 0{,}6 sekuntia running time: 0.6 seconds
\item \item
rekursiokutsuja: 69 miljoonaa recursive calls: 69 millions
\end{itemize} \end{itemize}
~\\ ~\\
Nyt on hyvä hetki lopettaa optimointi ja muistella, Now it's a good moment to stop optimization
mistä lähdimme liikkeelle. and remember our starting point.
Alkuperäinen algoritmi vei aikaa 483 sekuntia, The running time of the original algorithm
ja nyt optimointien jälkeen algoritmi vie aikaa was 483 seconds,
vain 0{,}6 sekuntia. and now after the optimizations,
Optimointien ansiosta algoritmi nopeutui the running time is only 0.6 seconds.
siis lähes 1000-kertaisesti. Thus, the algorithm became nearly 1000 times
faster after the optimizations.
Tämä on yleinen ilmiö peruuttavassa haussa, This is a usual phenomenon in backtracking
koska hakupuu on yleensä valtava ja because the search tree is usually large
yksinkertainenkin optimointi voi karsia suuren and even simple optimizations can prune
määrän haaroja hakupuusta. a lot of branches in the tree.
Erityisen hyödyllisiä ovat optimoinnit, Especially useful are optimizations that
jotka kohdistuvat hakupuun yläosaan, occur at the top of the search tree because
koska ne karsivat eniten haaroja. they can prune the search very efficiently.
\section{Puolivälihaku} \section{Puolivälihaku}