cphb/luku27.tex

406 lines
12 KiB
TeX

\chapter{Square root algorithms}
\index{nelizjuurialgoritmi@neliöjuurialgoritmi}
\key{Neliöjuurialgoritmi} on algoritmi,
jonka aikavaativuudessa esiintyy neliöjuuri.
Neliöjuurta voi ajatella ''köyhän miehen logaritmina'':
aikavaativuus $O(\sqrt n)$ on parempi kuin $O(n)$
mutta huonompi kuin $O(\log n)$.
Toisaalta neliöjuurialgoritmit toimivat
käytännössä hyvin ja niiden vakiokertoimet ovat pieniä.
Tarkastellaan esimerkkinä tuttua ongelmaa,
jossa toteutettavana on summakysely taulukkoon.
Halutut operaatiot ovat:
\begin{itemize}
\item muuta kohdassa $x$ olevaa lukua
\item laske välin $[a,b]$ lukujen summa
\end{itemize}
Olemme aiemmin ratkaisseet tehtävän
binääri-indeksipuun ja segmenttipuun avulla,
jolloin kummankin operaation aikavaativuus on $O(\log n)$.
Nyt ratkaisemme tehtävän toisella
tavalla neliöjuurirakennetta käyttäen,
jolloin summan laskenta vie aikaa $O(\sqrt n)$
ja luvun muuttaminen vie aikaa $O(1)$.
Ideana on jakaa taulukko $\sqrt n$-kokoisiin
väleihin niin, että jokaiseen väliin
tallennetaan lukujen summa välillä.
Seuraavassa on esimerkki taulukosta ja
sitä vastaavista $\sqrt n$-väleistä:
\begin{center}
\begin{tikzpicture}[scale=0.7]
\draw (0,0) grid (16,1);
\draw (0,1) rectangle (4,2);
\draw (4,1) rectangle (8,2);
\draw (8,1) rectangle (12,2);
\draw (12,1) rectangle (16,2);
\node at (0.5, 0.5) {5};
\node at (1.5, 0.5) {8};
\node at (2.5, 0.5) {6};
\node at (3.5, 0.5) {3};
\node at (4.5, 0.5) {2};
\node at (5.5, 0.5) {7};
\node at (6.5, 0.5) {2};
\node at (7.5, 0.5) {6};
\node at (8.5, 0.5) {7};
\node at (9.5, 0.5) {1};
\node at (10.5, 0.5) {7};
\node at (11.5, 0.5) {5};
\node at (12.5, 0.5) {6};
\node at (13.5, 0.5) {2};
\node at (14.5, 0.5) {3};
\node at (15.5, 0.5) {2};
\node at (2, 1.5) {21};
\node at (6, 1.5) {17};
\node at (10, 1.5) {20};
\node at (14, 1.5) {13};
\end{tikzpicture}
\end{center}
Kun taulukon luku muuttuu,
tämän yhteydessä täytyy laskea uusi summa
vastaavalle $\sqrt n$-välille:
\begin{center}
\begin{tikzpicture}[scale=0.7]
\fill[color=lightgray] (5,0) rectangle (6,1);
\draw (0,0) grid (16,1);
\fill[color=lightgray] (4,1) rectangle (8,2);
\draw (0,1) rectangle (4,2);
\draw (4,1) rectangle (8,2);
\draw (8,1) rectangle (12,2);
\draw (12,1) rectangle (16,2);
\node at (0.5, 0.5) {5};
\node at (1.5, 0.5) {8};
\node at (2.5, 0.5) {6};
\node at (3.5, 0.5) {3};
\node at (4.5, 0.5) {2};
\node at (5.5, 0.5) {5};
\node at (6.5, 0.5) {2};
\node at (7.5, 0.5) {6};
\node at (8.5, 0.5) {7};
\node at (9.5, 0.5) {1};
\node at (10.5, 0.5) {7};
\node at (11.5, 0.5) {5};
\node at (12.5, 0.5) {6};
\node at (13.5, 0.5) {2};
\node at (14.5, 0.5) {3};
\node at (15.5, 0.5) {2};
\node at (2, 1.5) {21};
\node at (6, 1.5) {15};
\node at (10, 1.5) {20};
\node at (14, 1.5) {13};
\end{tikzpicture}
\end{center}
Välin summan laskeminen taas tapahtuu muodostamalla
summa reunoissa olevista yksittäisistä luvuista
sekä keskellä olevista $\sqrt n$-väleistä:
\begin{center}
\begin{tikzpicture}[scale=0.7]
\fill[color=lightgray] (3,0) rectangle (4,1);
\fill[color=lightgray] (12,0) rectangle (13,1);
\fill[color=lightgray] (13,0) rectangle (14,1);
\draw (0,0) grid (16,1);
\fill[color=lightgray] (4,1) rectangle (8,2);
\fill[color=lightgray] (8,1) rectangle (12,2);
\draw (0,1) rectangle (4,2);
\draw (4,1) rectangle (8,2);
\draw (8,1) rectangle (12,2);
\draw (12,1) rectangle (16,2);
\node at (0.5, 0.5) {5};
\node at (1.5, 0.5) {8};
\node at (2.5, 0.5) {6};
\node at (3.5, 0.5) {3};
\node at (4.5, 0.5) {2};
\node at (5.5, 0.5) {5};
\node at (6.5, 0.5) {2};
\node at (7.5, 0.5) {6};
\node at (8.5, 0.5) {7};
\node at (9.5, 0.5) {1};
\node at (10.5, 0.5) {7};
\node at (11.5, 0.5) {5};
\node at (12.5, 0.5) {6};
\node at (13.5, 0.5) {2};
\node at (14.5, 0.5) {3};
\node at (15.5, 0.5) {2};
\node at (2, 1.5) {21};
\node at (6, 1.5) {15};
\node at (10, 1.5) {20};
\node at (14, 1.5) {13};
\draw [decoration={brace}, decorate, line width=0.5mm] (14,-0.25) -- (3,-0.25);
\end{tikzpicture}
\end{center}
Luvun muuttamisen aikavaativuus on
$O(1)$, koska riittää muuttaa yhden $\sqrt n$-välin summaa.
Välin summa taas lasketaan kolmessa osassa:
\begin{itemize}
\item vasemmassa reunassa on $O(\sqrt n)$ yksittäistä lukua
\item keskellä on $O(\sqrt n)$ peräkkäistä $\sqrt n$-väliä
\item oikeassa reunassa on $O(\sqrt n)$ yksittäistä lukua
\end{itemize}
Jokaisen osan summan laskeminen vie aikaa $O(\sqrt n)$,
joten summan laskemisen aikavaativuus on yhteensä $O(\sqrt n)$.
Neliöjuurialgoritmeissa parametri $\sqrt n$
johtuu siitä, että se saattaa kaksi asiaa tasapainoon:
esimerkiksi $n$ alkion taulukko jakautuu
$\sqrt n$ osaan, joista jokaisessa on $\sqrt n$ alkiota.
Käytännössä algoritmeissa
ei ole kuitenkaan pakko käyttää
tarkalleen parametria $\sqrt n$,
vaan voi olla parempi valita toiseksi
parametriksi $k$ ja toiseksi $n/k$,
missä $k$ on pienempi tai suurempi kuin $\sqrt n$.
Paras parametri selviää usein kokeilemalla
ja riippuu tehtävästä ja syötteestä.
Esimerkiksi jos taulukkoa käsittelevä algoritmi
käy usein läpi välit mutta harvoin välin sisällä
olevia alkioita, taulukko voi olla järkevää
jakaa $k < \sqrt n$ väliin,
joista jokaisella on $n/k > \sqrt n$ alkiota.
\section{Eräkäsittely}
\index{erxkxsittely@eräkäsittely}
\key{Eräkäsittelyssä} algoritmin suorittamat
operaatiot jaetaan eriin,
jotka käsitellään omina kokonaisuuksina.
Erien välissä tehdään yksittäinen työläs toimenpide,
joka auttaa tulevien operaatioiden käsittelyä.
Neliöjuurialgoritmi syntyy, kun $n$ operaatiota
jaetaan $O(\sqrt n)$-kokoisiin eriin,
jolloin sekä eriä että operaatioita kunkin erän
sisällä on $O(\sqrt n)$.
Tämä tasapainottaa sitä, miten usein erien välinen
työläs toimenpide tapahtuu sekä miten paljon työtä
erän sisällä täytyy tehdä.
Tarkastellaan esimerkkinä tehtävää, jossa
ruudukossa on $k \times k$ ruutua,
jotka ovat aluksi valkoisia.
Tehtävänä on suorittaa ruudukkoon
$n$ operaatiota,
joista jokainen on jompikumpi seuraavista:
\begin{itemize}
\item
väritä ruutu $(y,x)$ mustaksi
\item
etsi ruudusta $(y,x)$ lähin
musta ruutu, kun
ruutujen $(y_1,x_1)$ ja $(y_2,x_2)$
etäisyys on $|y_1-y_2|+|x_1-x_2|$
\end{itemize}
Ratkaisuna on jakaa operaatiot $O(\sqrt n)$ erään,
joista jokaisessa on $O(\sqrt n)$ operaatiota.
Kunkin erän alussa jokaiseen ruudukon ruutuun
lasketaan pienin etäisyys mustaan ruutuun.
Tämä onnistuu ajassa $O(k^2)$ leveyshaun avulla.
Kunkin erän käsittelyssä pidetään yllä listaa ruuduista,
jotka on muutettu mustaksi tässä erässä.
Nyt etäisyys ruudusta lähimpään mustaan ruutuun
on joko erän alussa laskettu etäisyys tai sitten
etäisyys johonkin listassa olevaan tämän erän aikana mustaksi
muutettuun ruutuun.
Algoritmi vie aikaa $O((k^2+n) \sqrt n)$,
koska erien välissä tehdään $O(\sqrt n)$ kertaa
$O(k^2)$-aikainen läpikäynti, ja
erissä käsitellään yhteensä $O(n)$ solmua,
joista jokaisen kohdalla käydään läpi
$O(\sqrt n)$ solmua listasta.
Jos algoritmi tekisi leveyshaun jokaiselle operaatiolle,
aikavaativuus olisi $O(k^2 n)$.
Jos taas algoritmi kävisi kaikki muutetut ruudut läpi
jokaisen operaation kohdalla,
aikavaativuus olisi $O(n^2)$.
Neliöjuurialgoritmi yhdistää nämä aikavaativuudet
ja muuttaa kertoimen $n$ kertoimeksi $\sqrt n$.
\section{Tapauskäsittely}
\index{tapauskxsittely@tapauskäsittely}
\key{Tapauskäsittelyssä} algoritmissa on useita
toimintatapoja, jotka aktivoituvat syötteen
ominaisuuksista riippuen.
Tyypillisesti yksi algoritmin osa on tehokas
pienellä parametrilla
ja toinen osa on tehokas suurella parametrilla,
ja sopiva jakokohta kulkee suunnilleen arvon $\sqrt n$ kohdalla.
Tarkastellaan esimerkkinä tehtävää, jossa
puussa on $n$ solmua, joista jokaisella on tietty väri.
Tavoitteena on etsiä puusta kaksi solmua,
jotka ovat samanvärisiä ja mahdollisimman
kaukana toisistaan.
Tehtävän voi ratkaista
käymällä läpi värit yksi kerrallaan ja
etsimällä kullekin värille kaksi solmua, jotka ovat
mahdollisimman kaukana toisistaan.
Tietyllä värillä algoritmin toiminta riippuu siitä,
montako kyseisen väristä solmua puussa on.
Oletetaan nyt, että käsittelyssä on väri $x$
ja puussa on $c$ solmua, joiden väri on $x$.
Tapaukset ovat seuraavat:
\subsubsection*{Tapaus 1: $c \le \sqrt n$}
Jos $x$-värisiä solmuja on vähän,
käydään läpi kaikki $x$-väristen solmujen parit
ja valitaan pari, jonka etäisyys on suurin.
Jokaisesta solmusta täytyy
laskea etäisyys $O(\sqrt n)$ muuhun solmuun (ks. luku 18.3),
joten kaikkien tapaukseen 1 osuvien solmujen
käsittely vie aikaa yhteensä $O(n \sqrt n)$.
\subsubsection*{Tapaus 2: $c > \sqrt n$}
Jos $x$-värisiä solmuja on paljon,
käydään koko puu läpi ja
lasketaan suurin etäisyys kahden
$x$-värisen solmun välillä.
Läpikäynnin aikavaativuus on $O(n)$,
ja tapaus 2 aktivoituu korkeintaan $O(\sqrt n)$
värille, joten tapauksen 2 solmut
tuottavat aikavaativuuden $O(n \sqrt n)$.\\\\
\noindent
Algoritmin kokonaisaikavaativuus on $O(n \sqrt n)$,
koska sekä tapaus 1 että tapaus 2 vievät aikaa
yhteensä $O(n \sqrt n)$.
\section{Mo'n algoritmi}
\index{Mo'n algoritmi}
\key{Mo'n algoritmi} soveltuu tehtäviin,
joissa taulukkoon tehdään välikyselyitä ja
taulukon sisältö kaikissa kyselyissä on sama.
Algoritmi järjestää
kyselyt uudestaan niin,
että niiden käsittely on tehokasta.
Algoritmi pitää yllä taulukon väliä,
jolle on laskettu kyselyn vastaus.
Kyselystä toiseen siirryttäessä algoritmi
muuttaa väliä askel kerrallaan niin,
että vastaus uuteen kyselyyn saadaan laskettua.
Algoritmin aikavaativuus on $O(n \sqrt n f(n))$,
kun kyselyitä on $n$ ja
yksi välin muutosaskel vie aikaa $f(n)$.
Algoritmin toiminta perustuu järjestykseen,
jossa kyselyt käsitellään.
Kun kyselyjen välit ovat muotoa $[a,b]$,
algoritmi järjestää ne ensisijaisesti arvon
$\lfloor a/\sqrt n \rfloor$ mukaan ja toissijaisesti arvon $b$ mukaan.
Algoritmi suorittaa siis peräkkäin kaikki kyselyt,
joiden alkukohta on tietyllä $\sqrt n$-välillä.
Osoittautuu, että tämän järjestyksen ansiosta
algoritmi tekee yhteensä vain $O(n \sqrt n)$ muutosaskelta.
Tämä johtuu siitä, että välin vasen reuna liikkuu
$n$ kertaa $O(\sqrt n)$ askelta,
kun taas välin oikea reuna liikkuu $\sqrt n$
kertaa $O(n)$ askelta. Molemmat reunat liikkuvat
siis yhteensä $O(n \sqrt n)$ askelta.
\subsubsection*{Esimerkki}
Tarkastellaan esimerkkinä tehtävää,
jossa annettuna on joukko välejä taulukossa
ja tehtävänä on selvittää kullekin välille,
montako eri lukua taulukossa on kyseisellä välillä.
Mo'n algoritmissa kyselyt järjestetään aina samalla
tavalla, ja tehtävästä riippuva osa on,
miten kyselyn vastausta pidetään yllä.
Tässä tehtävässä luonteva tapa on
pitää muistissa kyselyn vastausta sekä
taulukkoa \texttt{c}, jossa $\texttt{c}[x]$
on alkion $x$ lukumäärä aktiivisella välillä.
Kyselystä toiseen siirryttäessä taulukon aktiivinen
väli muuttuu. Esimerkiksi jos nykyinen kysely koskee väliä
\begin{center}
\begin{tikzpicture}[scale=0.7]
\fill[color=lightgray] (1,0) rectangle (5,1);
\draw (0,0) grid (9,1);
\node at (0.5, 0.5) {4};
\node at (1.5, 0.5) {2};
\node at (2.5, 0.5) {5};
\node at (3.5, 0.5) {4};
\node at (4.5, 0.5) {2};
\node at (5.5, 0.5) {4};
\node at (6.5, 0.5) {3};
\node at (7.5, 0.5) {3};
\node at (8.5, 0.5) {4};
\end{tikzpicture}
\end{center}
ja seuraava kysely koskee väliä
\begin{center}
\begin{tikzpicture}[scale=0.7]
\fill[color=lightgray] (2,0) rectangle (7,1);
\draw (0,0) grid (9,1);
\node at (0.5, 0.5) {4};
\node at (1.5, 0.5) {2};
\node at (2.5, 0.5) {5};
\node at (3.5, 0.5) {4};
\node at (4.5, 0.5) {2};
\node at (5.5, 0.5) {4};
\node at (6.5, 0.5) {3};
\node at (7.5, 0.5) {3};
\node at (8.5, 0.5) {4};
\end{tikzpicture}
\end{center}
niin tapahtuu kolme muutosaskelta:
välin vasen reuna siirtyy askeleen oikealle
ja välin oikea reuna siirtyy kaksi askelta oikealle.
Jokaisen muutosaskeleen jälkeen täytyy
päivittää taulukkoa \texttt{c}.
Jos väliin tulee alkio $x$,
arvo $\texttt{c}[x]$ kasvaa 1:llä,
ja jos välistä poistuu alkio $x$,
arvo $\texttt{c}[x]$ vähenee 1:llä.
Jos lisäyksen jälkeen $\texttt{c}[x]=1$,
kyselyn vastaus kasvaa 1:llä,
ja jos poiston jälkeen $\texttt{c}[x]=0$,
kyselyn vastaus vähenee 1:llä.
Tässä tapauksessa muutosaskeleen aikavaativuus on $O(1)$,
joten algoritmin kokonaisaikavaativuus on $O(n \sqrt n)$.