commit c210d9497b8b8777a07928847edc39fb0ec6b635 Author: Antti H S Laaksonen Date: Thu Dec 29 00:54:51 2016 +0200 First commit diff --git a/johdanto.tex b/johdanto.tex new file mode 100644 index 0000000..6ceed11 --- /dev/null +++ b/johdanto.tex @@ -0,0 +1,26 @@ +\chapter*{Preface} +\markboth{\MakeUppercase{Preface}}{} +\addcontentsline{toc}{chapter}{Preface} + +The purpose of this book is to give you +a thorough introduction to competitive programming. +The book assumes that you already +know the basics of programming, but previous +background on competitive programming is not needed. + +The book is especially intended for +high school students who want to learn +algorithms and possibly participate in +the International Olympiad in Informatics (IOI). +The book is also suitable for university students +and anybody else interested in competitive programming. + +It takes a long time to become a good competitive +programmer, but it is also an opportunity to learn a lot. +You can be sure that you will learn a great deal +about algorithms if you spend time reading the book +and solving exercises. + +The book is under continuous development. +You can always send feedback about the book to +\texttt{ahslaaks@cs.helsinki.fi}. diff --git a/kkkk.tex b/kkkk.tex new file mode 100644 index 0000000..7f0fe0b --- /dev/null +++ b/kkkk.tex @@ -0,0 +1,113 @@ +\documentclass[twoside,12pt,a4paper,english]{book} + +\includeonly{luku01} + +\usepackage[english]{babel} +\usepackage[utf8]{inputenc} +\usepackage{listings} +\usepackage[table]{xcolor} +\usepackage{tikz} +\usepackage{multicol} +\usepackage{hyperref} +\usepackage{array} +\usepackage{microtype} + +\usepackage{fouriernc} +\usepackage[T1]{fontenc} + +\usepackage{graphicx} +\usepackage{framed} +\usepackage{amssymb} +\usepackage{amsmath} + +\usepackage{pifont} +\usepackage{ifthen} +\usepackage{makeidx} +\usepackage{enumitem} + +\usepackage{titlesec} + +\usetikzlibrary{patterns,snakes} +\pagestyle{plain} + +\lstset{language=C++,frame=single,basicstyle=\ttfamily \small,showstringspaces=false,columns=flexible} +\lstset{ + literate={ö}{{\"o}}1 + {ä}{{\"a}}1 + {ü}{{\"u}}1 +} +\lstset{xleftmargin=20pt,xrightmargin=5pt} +\lstset{aboveskip=12pt,belowskip=8pt} + + +\date{\Large \today} + +\usepackage[a4paper,vmargin=30mm,hmargin=33mm,footskip=15mm]{geometry} + +\title{\Huge Handbook of Competitive Programming} +\author{\Large Antti Laaksonen} + +\makeindex + +\titleformat{\subsubsection} +{\normalfont\large\bfseries\sffamily}{\thesubsection}{1em}{} + +\begin{document} + +%\selectlanguage{finnish} + +%\setcounter{page}{1} +%\pagenumbering{roman} + +\frontmatter +\maketitle +\setcounter{tocdepth}{1} +\tableofcontents + +\include{johdanto} + +\mainmatter +\pagenumbering{arabic} +\setcounter{page}{1} + +\newcommand{\key}[1] {\textbf{#1}} + +\part{Perusasiat} +\include{luku01} +\include{luku02} +\include{luku03} +\include{luku04} +\include{luku05} +\include{luku06} +\include{luku07} +\include{luku08} +\include{luku09} +\include{luku10} +\part{Graph algorithms} +\include{luku11} +\include{luku12} +\include{luku13} +\include{luku14} +\include{luku15} +\include{luku16} +\include{luku17} +\include{luku18} +\include{luku19} +\include{luku20} +\part{Advanced topics} +\include{luku21} +\include{luku22} +\include{luku23} +\include{luku24} +\include{luku25} +\include{luku26} +\include{luku27} +\include{luku28} +\include{luku29} +\include{luku30} +\include{kirj} + +\cleardoublepage +\printindex + +\end{document} \ No newline at end of file diff --git a/luku01.tex b/luku01.tex new file mode 100644 index 0000000..e5c12ff --- /dev/null +++ b/luku01.tex @@ -0,0 +1,835 @@ +\chapter{Introduction} + +Competitive programming combines two topics: +(1) design of algorithms and (2) implementation of algorithms. + +The \key{design of algorithms} consists of problem solving +and mathematical thinking. +Skills for analyzing problems and solving them +using creativity is needed. +An algorithm for solving a problem +has to be both correct and efficient, +and the core of the problem is often +how to invent an efficient algorithm. + +Theoretical knowledge of algorithms +is very important to competitive programmers. +Typically, a solution for a problem is +a combination of well-known techniques and +new insights. +The techniques that appear in competitive programming +also form the basis for the scientific research +of algorithms. + +The \key{implementation of algorithms} requires good +programming skills. +In competitive programming, the solutions +are graded by testing an implemented algorithm +using a set of test cases. +Thus, it is not enough that the idea of the +algorithm is correct, but the implementation has +to be correct as well. + +Good coding style in contests is +straightforward and concise. +The solutions should be written quickly, +because there is not much time available. +Unlike in traditional software engineering, +the solutions are short (usually at most some +hundreds of lines) and it is not needed to +maintain them after the contest. + +\section{Programming languages} + +\index{programming language} + +At the moment, the most popular programming +languages in contests are C++, Python and Java. +For example, in Google Code Jam 2016, +among the best 3,000 participants, +73 \% used C++, +15 \% used Python and +10 \% used Java\footnote{\url{https://www.go-hero.net/jam/16}}. +Some participants also used several languages. + +Many people think that C++ is the best choice +for a competitive programmer, +and C++ is nearly always available in +contest systems. +The benefits in using C++ are that +it is a very efficient language and +its standard library contains a +large collection +of data structures and algorithms. + +On the other hand, it is good to +master several languages and know the +benefits of them. +For example, if big integers are needed +in the problem, +Python can be a good choice because it +contains a built-in library for handling +big integers. +Still, usually the goal is to write the problems so that +the use of a specific programming language +is not an unfair advantage in the contest. + +All examples in this book are written in C++, +and the data structures and algorithms in +the standard library are often used. +The standard is C++11 that can be used in most +contests. +If you can't program in C++ yet, +now it is a good time to start learning. + +\subsubsection{C++ template} + +A typical C++ template for competitive programming +looks like this: + +\begin{lstlisting} +#include + +using namespace std; + +int main() { + // koodi tulee tähän +} +\end{lstlisting} + +The \texttt{\#include} line at the beginning +is a feature in the \texttt{g++} compiler +that allows to include the whole standard library. +Thus, it is not needed to separately include +libraries such as \texttt{iostream}, +\texttt{vector} and \texttt{algorithm}, +but they are available automatically. + +The following \texttt{using} line determines +that the standard library can be used directly +in the code. +Without the \texttt{using} line we should write, +for example, \texttt{std::cout}, +but now it is enough to write \texttt{cout}. + +The code can be compiled using the following command: + +\begin{lstlisting} +g++ -std=c++11 -O2 -Wall code.cpp -o code +\end{lstlisting} + +This command produces a binary file \texttt{code} +from the source code \texttt{code.cpp}. +The compiler obeys the C++11 standard +(\texttt{-std=c++11}), +optimizes the code (\texttt{-O2}) +and shows warnings about possible errors (\texttt{-Wall}). + +\section{Input and output} + +\index{input and output} + +In most contests, standard streams are used for +reading input and writing output. +In C++, the standard streams are +\texttt{cin} for input and \texttt{cout} for output. +In addition, the C functions +\texttt{scanf} and \texttt{printf} can be used. + +The input for the program usually consists of +numbers and strings that are separated with +spaces and newlines. +They can be read from the \texttt{cin} stream +as follows: + +\begin{lstlisting} +int a, b; +string x; +cin >> a >> b >> x; +\end{lstlisting} + +This kind of code always works, +assuming that there is at least one space +or one newline between each element in the input. +For example, the above code accepts +both the following inputs: +\begin{lstlisting} +123 456 apina +\end{lstlisting} +\begin{lstlisting} +123 456 +apina +\end{lstlisting} +The \texttt{cout} stream is used for output +as follows: +\begin{lstlisting} +int a = 123, b = 456; +string x = "apina"; +cout << a << " " << b << " " << x << "\n"; +\end{lstlisting} + +Handling input and output is sometimes +a bottleneck in the program. +The following lines at the beginning of the code +make input and output more efficient: + +\begin{lstlisting} +ios_base::sync_with_stdio(0); +cin.tie(0); +\end{lstlisting} + +Note that the newline \texttt{"\textbackslash n"} +works faster than \texttt{endl}, +becauses \texttt{endl} always causes +a flush operation. + +The C functions \texttt{scanf} +and \texttt{printf} are an alternative +to the C++ standard streams. +They are usually a bit faster, +but they are also more difficult to use. +The following code reads two integers from the input: +\begin{lstlisting} +int a, b; +scanf("%d %d", &a, &b); +\end{lstlisting} +The following code prints two integers: +\begin{lstlisting} +int a = 123, b = 456; +printf("%d %d\n", a, b); +\end{lstlisting} + +Sometimes the program should read a whole line +from the input, possibly with spaces. +This can be accomplished using the +\texttt{getline} function: + +\begin{lstlisting} +string s; +getline(cin, s); +\end{lstlisting} + +If the amount of data is unknown, the following +loop can be handy: +\begin{lstlisting} +while (cin >> x) { + // koodia +} +\end{lstlisting} +This loop reads elements from the input +one after another, until there is no +more data available in the input. + +In some contest systems, files are used for +input and output. +An easy solution for this is to write +the code as usual using standard streams, +but add the following lines to the beginning of the code: +\begin{lstlisting} +freopen("input.txt", "r", stdin); +freopen("output.txt", "w", stdout); +\end{lstlisting} +After this, the code reads the input from the file +''input.txt'' and writes the output to the file +''output.txt''. + +\section{Handling numbers} + +\index{integer} + +\subsubsection{Integers} + +The most popular integer type in competitive programming +is \texttt{int}. This is a 32-bit type with +value range $-2^{31} \ldots 2^{31}-1$, +i.e., about $-2 \cdot 10^9 \ldots 2 \cdot 10^9$. +If the type \texttt{int} is not enough, +the 64-bit type \texttt{long long} can be used, +with value range $-2^{63} \ldots 2^{63}-1$, +i.e., about $-9 \cdot 10^{18} \ldots 9 \cdot 10^{18}$. + +The following code defines a +\texttt{long long} variable: +\begin{lstlisting} +long long x = 123456789123456789LL; +\end{lstlisting} +The suffix \texttt{LL} means that the +type of the number is \texttt{long long}. + +A typical error when using the type \texttt{long long} +is that the type \texttt{int} is still used somewhere +in the code. +For example, the following code contains +a subtle error: + +\begin{lstlisting} +int a = 123456789; +long long b = a*a; +cout << b << "\n"; // -1757895751 +\end{lstlisting} + +Even though the variable \texttt{b} is of type \texttt{long long}, +both numbers in the expression \texttt{a*a} +are of type \texttt{int} and the result is +also of type \texttt{int}. +Because of this, the variable \texttt{b} will +contain a wrong result. +The problem can be solved by changing the type +of \texttt{a} to \texttt{long long} or +by changing the expression to \texttt{(long long)a*a}. + +Usually, the problems are written so that the +type \texttt{long long} is enough. +Still, it is good to know that +the \texttt{g++} compiler also features +an 128-bit type \texttt{\_\_int128\_t} +with value range +$-2^{127} \ldots 2^{127}-1$, i.e., $-10^{38} \ldots 10^{38}$. +However, this type is not available in all contest systems. + +\subsubsection{Modular arithmetic} + +\index{remainder} +\index{modular arithmetic} + +We denote by $x \bmod m$ the remainder +when $x$ is divided by $m$. +For example, $17 \bmod 5 = 2$, +because $17 = 3 \cdot 5 + 2$. + +Sometimes, the answer for a problem is a +very big integer but it is enough to +print it ''modulo $m$'', i.e., +the remainder when the answer is divided by $m$ +(for example, ''modulo $10^9+7$''). +The idea is that even if the actual answer +may be very big, +it is enough to use the types +\texttt{int} and \texttt{long long}. + +An important property of the remainder is that +in addition, subtraction and multiplication, +the remainder can be calculated before the operation: + +\[ +\begin{array}{rcr} +(a+b) \bmod m & = & (a \bmod m + b \bmod m) \bmod m \\ +(a-b) \bmod m & = & (a \bmod m - b \bmod m) \bmod m \\ +(a \cdot b) \bmod m & = & (a \bmod m \cdot b \bmod m) \bmod m +\end{array} +\] + +Thus, we can calculate the remainder after every operation +and the numbers will never become too large. + +For example, the following code calculates $n!$, +the factorial of $n$, modulo $m$: +\begin{lstlisting} +long long x = 1; +for (int i = 2; i <= n i++) { + x = (x*i)%m; +} +cout << x << "\n"; +\end{lstlisting} + +Usually, the answer should be always given so +that the remainder is between $0\ldots m-1$. +However, in C++ and other languages, +the remainder of a negative number can +be negative. +An easy way to make sure that this will +not happen is to first calculate +the remainder as usual and then add $m$ +if the result is negative: +\begin{lstlisting} +x = x%m; +if (x < 0) x += m; +\end{lstlisting} +However, this is only needed when there +are subtractions in the code and the +remainder may become negative. + +\subsubsection{Floating point numbers} + +\index{floating point number} + +The usual floating point types in +competitive programming are +the 64-bit \texttt{double} +and, as an extension in the \texttt{g++} compiler, +the 80-bit \texttt{long double}. +In most cases, \texttt{double} is enough, +but \texttt{long double} is more accurate. + +The required precision of the answer +is usually given. +The easiest way is to use +the \texttt{printf} function +that can be given the number of decimal places. +For example, the following code prints +the value of $x$ with 9 decimal places: + +\begin{lstlisting} +printf("%.9f\n", x); +\end{lstlisting} + +A difficulty when using floating point numbers +is that some numbers cannot be represented +accurately, but there will be rounding errors. +For example, the result of the following code +is surprising: + +\begin{lstlisting} +double x = 0.3*3+0.1; +printf("%.20f\n", x); // 0.99999999999999988898 +\end{lstlisting} + +Because of a rounding error, +the value of \texttt{x} is a bit less than 1, +while the correct value would be 1. + +It is risky to compare floating point numbers +with the \texttt{==} operator, +because it is possible that the values should +be equal but they are not due to rounding errors. +A better way to compare floating point numbers +is to assume that two numbers are equal +if the difference between them is $\varepsilon$, +where $\varepsilon$ is a small number. + +In practice, the numbers can be compared +as follows ($\varepsilon=10^{-9}$): + +\begin{lstlisting} +if (abs(a-b) < 1e-9) { + // a ja b ovat yhtä suuret +} +\end{lstlisting} + +Note that while floating point numbers are inaccurate, +integers up to a certain limit can be still +represented accurately. +For example, using \texttt{double}, +it is possible to accurately represent all +integers having absolute value at most $2^{53}$. + +\section{Koodin lyhentäminen} + +Kisakoodauksessa ihanteena on lyhyt koodi, +koska algoritmi täytyy pystyä toteuttamaan +mahdollisimman nopeasti. +Monet kisakoodarit käyttävätkin lyhennysmerkintöjä +tietotyypeille ja muille koodin osille. + +\subsubsection{Tyyppinimet} +\index{tuppdef@\texttt{typedef}} +Komennolla \texttt{typedef} voi antaa lyhyemmän +nimen tietotyypille. +Esimerkiksi nimi \texttt{long long} on pitkä, +joten tyypille voi antaa lyhyemmän nimen \texttt{ll}: +\begin{lstlisting} +typedef long long ll; +\end{lstlisting} +Tämän jälkeen koodin +\begin{lstlisting} +long long a = 123456789; +long long b = 987654321; +cout << a*b << "\n"; +\end{lstlisting} +voi lyhentää seuraavasti: +\begin{lstlisting} +ll a = 123456789; +ll b = 987654321; +cout << a*b << "\n"; +\end{lstlisting} + +Komentoa \texttt{typedef} voi käyttää myös +monimutkaisempien tyyppien kanssa. +Esimerkiksi seuraava koodi antaa nimen \texttt{vi} +kokonaisluvuista muodostuvalle vektorille +sekä nimen \texttt{pi} kaksi +kokonaislukua sisältävälle parille. +\begin{lstlisting} +typedef vector vi; +typedef pair pi; +\end{lstlisting} + +\subsubsection{Makrot} +\index{makro} +Toinen tapa lyhentää koodia on määritellä \key{makroja}. +Makro ilmaisee, että tietyt koodissa olevat +merkkijonot korvataan toisilla ennen koodin +kääntämistä. +C++:ssa makro määritellään +esikääntäjän komennolla \texttt{\#define}. + +Määritellään esimerkiksi seuraavat makrot: +\begin{lstlisting} +#define F first +#define S second +#define PB push_back +#define MP make_pair +\end{lstlisting} +Tämän jälkeen koodin +\begin{lstlisting} +v.push_back(make_pair(y1,x1)); +v.push_back(make_pair(y2,x2)); +int d = v[i].first+v[i].second; +\end{lstlisting} +voi kirjoittaa lyhyemmin seuraavasti: +\begin{lstlisting} +v.PB(MP(y1,x1)); +v.PB(MP(y2,x2)); +int d = v[i].F+v[i].S; +\end{lstlisting} + +Makro on mahdollista määritellä myös niin, +että sille voi antaa parametreja. +Tämän ansiosta makrolla voi lyhentää esimerkiksi +komentorakenteita. +Määritellään esimerkiksi seuraava makro: +\begin{lstlisting} +#define REP(i,a,b) for (int i = a; i <= b; i++) +\end{lstlisting} +Tämän jälkeen koodin +\begin{lstlisting} +for (int i = 1; i <= n; i++) { + haku(i); +} +\end{lstlisting} +voi lyhentää seuraavasti: +\begin{lstlisting} +REP(i,1,n) { + haku(i); +} +\end{lstlisting} + +\section{Matematiikka} + +Matematiikka on tärkeässä asemassa kisakoodauksessa, +ja menestyvän kisakoodarin täytyy osata myös +hyvin matematiikkaa. +Käymme seuraavaksi läpi joukon keskeisiä +matematiikan käsitteitä ja kaavoja. +Palaamme moniin aiheisiin myöhemmin tarkemmin kirjan aikana. + +\subsubsection{Summakaavat} + +Jokaiselle summalle muotoa +\[\sum_{x=1}^n x^k = 1^k+2^k+3^k+\ldots+n^k\] +on olemassa laskukaava, +kun $k$ on jokin positiivinen kokonaisluku. +Tällainen laskukaava on aina astetta $k+1$ +oleva polynomi. Esimerkiksi +\[\sum_{x=1}^n x = 1+2+3+\ldots+n = \frac{n(n+1)}{2}\] +ja +\[\sum_{x=1}^n x^2 = 1^2+2^2+3^2+\ldots+n^2 = \frac{n(n+1)(2n+1)}{6}.\] + +\key{Aritmeettinen summa} on summa, \index{aritmeettinen summa@aritmeettinen summa} +jossa jokaisen vierekkäisen luvun erotus on vakio. +Esimerkiksi +\[3+7+11+15\] +on aritmeettinen summa, +jossa vakio on 4. +Aritmeettinen summa voidaan laskea kaavalla +\[\frac{n(a+b)}{2},\] +jossa summan ensimmäinen luku on $a$, +viimeinen luku on $b$ ja lukujen määrä on $n$. +Esimerkiksi +\[3+7+11+15=\frac{4 \cdot (3+15)}{2} = 36.\] +Kaava perustuu siihen, että summa muodostuu $n$ luvusta +ja luvun suuruus on keskimäärin $(a+b)/2$. + +\index{geometrinen summa@geometrinen summa} +\key{Geometrinen summa} on summa, +jossa jokaisen vierekkäisen luvun suhde on vakio. +Esimerkiksi +\[3+6+12+24\] +on geometrinen summa, +jossa vakio on 2. +Geometrinen summa voidaan laskea kaavalla +\[\frac{bx-a}{x-1},\] +jossa summan ensimmäinen luku on $a$, +viimeinen luku on $b$ ja vierekkäisten lukujen suhde on $x$. +Esimerkiksi +\[3+6+12+24=\frac{24 \cdot 2 - 3}{2-1} = 45.\] + +Geometrisen summan kaavan voi johtaa merkitsemällä +\[ S = a + ax + ax^2 + \cdots + b .\] +Kertomalla molemmat puolet $x$:llä saadaan +\[ xS = ax + ax^2 + ax^3 + \cdots + bx,\] +josta kaava seuraa ratkaisemalla yhtälön +\[ xS-S = bx-a.\] + +Geometrisen summan erikoistapaus on usein kätevä kaava +\[1+2+4+8+\ldots+2^{n-1}=2^n-1.\] + +% Geometrisen summan sukulainen on +% \[x+2x^2+3x^3+\cdots+k x^k = \frac{kx^{k+2}-(k+1)x^{k+1}+x}{(x-1)^2}. \] + +\index{harmoninen summa@harmoninen summa} + +\key{Harmoninen summa} on summa muotoa +\[ \sum_{x=1}^n \frac{1}{x} = 1+\frac{1}{2}+\frac{1}{3}+\ldots+\frac{1}{n}.\] + +Yläraja harmonisen summan suuruudelle on $\log_2(n)+1$. +Summaa voi näet arvioida ylöspäin +muuttamalla jokaista termiä $1/k$ niin, +että $k$:ksi tulee alempi 2:n potenssi. +Esimerkiksi tapauksessa $n=6$ arvioksi tulee +\[ 1+\frac{1}{2}+\frac{1}{3}+\frac{1}{4}+\frac{1}{5}+\frac{1}{6} \le +1+\frac{1}{2}+\frac{1}{2}+\frac{1}{4}+\frac{1}{4}+\frac{1}{4}.\] +Tämän seurauksena summa jakaantuu $\log_2(n)+1$ osaan +($1$, $2 \cdot 1/2$, $4 \cdot 1/4$, jne.), +joista jokaisen summa on enintään 1. + +\subsubsection{Joukko-oppi} + +\index{joukko-oppi} +\index{joukko@joukko} +\index{leikkaus@leikkaus} +\index{yhdiste@yhdiste} +\index{erotus@erotus} +\index{osajoukko@osajoukko} +\index{perusjoukko} + +\key{Joukko} on kokoelma alkioita. +Esimerkiksi joukko +\[X=\{2,4,7\}\] +sisältää alkiot 2, 4 ja 7. +Merkintä $\emptyset$ tarkoittaa tyhjää joukkoa. +Joukon $S$ koko eli alkoiden määrä on $|S|$. +Esimerkiksi äskeisessä joukossa $|X|=3$. + +Merkintä $x \in S$ tarkoittaa, +että alkio $x$ on joukossa $S$, +ja merkintä $x \notin S$ tarkoittaa, +että alkio $x$ ei ole joukossa $S$. +Esimerkiksi äskeisessä joukossa +\[4 \in X \hspace{10px}\textrm{ja}\hspace{10px} 5 \notin X.\] + +\begin{samepage} +Uusia joukkoja voidaan muodostaa joukko-operaatioilla +seuraavasti: +\begin{itemize} +\item \key{Leikkaus} $A \cap B$ sisältää alkiot, +jotka ovat molemmissa joukoista $A$ ja $B$. +Esimerkiksi jos $A=\{1,2,5\}$ ja $B=\{2,4\}$, +niin $A \cap B = \{2\}$. +\item \key{Yhdiste} $A \cup B$ sisältää alkiot, +jotka ovat ainakin toisessa joukoista $A$ ja $B$. +Esimerkiksi jos $A=\{3,7\}$ ja $B=\{2,3,8\}$, +niin $A \cup B = \{2,3,7,8\}$. +\item \key{Komplementti} $\bar A$ sisältää alkiot, +jotka eivät ole joukossa $A$. +Komplementin tulkinta riippuu siitä, mikä on +\key{perusjoukko} eli joukko, jossa on kaikki +mahdolliset alkiot. Esimerkiksi jos +$A=\{1,2,5,7\}$ ja perusjoukko on $P=\{1,2,\ldots,10\}$, +niin $\bar A = \{3,4,6,8,9,10\}$. +\item \key{Erotus} $A \setminus B = A \cap \bar B$ sisältää alkiot, +jotka ovat joukossa $A$ mutta eivät joukossa $B$. +Huomaa, että $B$:ssä voi olla alkioita, +joita ei ole $A$:ssa. +Esimerkiksi jos $A=\{2,3,7,8\}$ ja $B=\{3,5,8\}$, +niin $A \setminus B = \{2,7\}$. +\end{itemize} +\end{samepage} + + +Merkintä $A \subset S$ tarkoittaa, +että $A$ on $S$:n \key{osajoukko} +eli jokainen $A$:n alkio esiintyy $S$:ssä. +Joukon $S$ osajoukkojen yhteismäärä on $2^{|S|}$. +Esimerkiksi joukon $\{2,4,7\}$ +osajoukot ovat +\begin{center} +$\emptyset$, +$\{2\}$, $\{4\}$, $\{7\}$, $\{2,4\}$, $\{2,7\}$, $\{4,7\}$ ja $\{2,4,7\}$. +\end{center} + +Usein esiintyviä joukkoja ovat + +\begin{itemize}[noitemsep] +\item $\mathbb{N}$ (luonnolliset luvut), +\item $\mathbb{Z}$ (kokonaisluvut), +\item $\mathbb{Q}$ (rationaaliluvut) ja +\item $\mathbb{R}$ (reaaliluvut). +\end{itemize} + +Luonnollisten lukujen joukko $\mathbb{N}$ voidaan määritellä +tilanteesta riippuen kahdella tavalla: +joko $\mathbb{N}=\{0,1,2,\ldots\}$ +tai $\mathbb{N}=\{1,2,3,...\}$. + +Joukon voi muodostaa myös säännöllä muotoa +\[\{f(n) : n \in S\},\] +missä $f(n)$ on jokin funktio. +Tällainen joukko sisältää kaikki alkiot +$f(n)$, jossa $n$ on valittu joukosta $S$. +Esimerkiksi joukko +\[X=\{2n : n \in \mathbb{Z}\}\] +sisältää kaikki parilliset kokonaisluvut. + +\subsubsection{Logiikka} + +\index{logiikka@logiikka} +\index{negaatio@negaatio} +\index{konjunktio@konjunktio} +\index{disjunktio@disjunktio} +\index{implikaatio@implikaatio} +\index{ekvivalenssi@ekvivalenssi} + +Loogisen lausekkeen arvo on joko \key{tosi} (1) tai +\key{epätosi} (0). +Tärkeimmät loogiset operaatiot ovat +$\lnot$ (\key{negaatio}), +$\land$ (\key{konjunktio}), +$\lor$ (\key{disjunktio}), +$\Rightarrow$ (\key{implikaatio}) sekä +$\Leftrightarrow$ (\key{ekvivalenssi}). +Seuraava taulukko näyttää operaatioiden merkityksen: + +\begin{center} +\begin{tabular}{rr|rrrrrrr} +$A$ & $B$ & $\lnot A$ & $\lnot B$ & $A \land B$ & $A \lor B$ & $A \Rightarrow B$ & $A \Leftrightarrow B$ \\ +\hline +0 & 0 & 1 & 1 & 0 & 0 & 1 & 1 \\ +0 & 1 & 1 & 0 & 0 & 1 & 1 & 0 \\ +1 & 0 & 0 & 1 & 0 & 1 & 0 & 0 \\ +1 & 1 & 0 & 0 & 1 & 1 & 1 & 1 \\ +\end{tabular} +\end{center} + +Negaatio $\lnot A$ muuttaa lausekkeen käänteiseksi. +Lauseke $A \land B$ on tosi, jos molemmat $A$ ja $B$ ovat tosia, +ja lauseke $A \lor B$ on tosi, jos $A$ tai $B$ on tosi. +Lauseke $A \Rightarrow B$ on tosi, +jos $A$:n ollessa tosi myös $B$ on aina tosi. +Lauseke $A \Leftrightarrow B$ on tosi, +jos $A$:n ja $B$:n totuusarvo on sama. + +\index{predikaatti@predikaatti} + +\key{Predikaatti} on lauseke, jonka arvo on tosi tai epätosi +riippuen sen parametreista. +Yleensä predikaattia merkitään suurella kirjaimella. +Esimerkiksi voimme määritellä predikaatin $P(x)$, +joka on tosi tarkalleen silloin, kun $x$ on alkuluku. +Tällöin esimerkiksi $P(7)$ on tosi, kun taas $P(8)$ on epätosi. + +\index{kvanttori@kvanttori} + +\key{Kvanttori} ilmaisee, että looginen +lauseke liittyy jollakin tavalla joukon alkioihin. +Tavalliset kvanttorit +ovat $\forall$ (\key{kaikille}) ja $\exists$ (\key{on olemassa}). +Esimerkiksi +\[\forall x (\exists y (y < x))\] +tarkoittaa, että jokaiselle joukon +alkiolle $x$ on olemassa +jokin joukon alkio $y$ niin, että $y$ on $x$:ää pienempi. +Tämä pätee kokonaislukujen joukossa, +mutta ei päde luonnollisten lukujen joukossa. + +Yllä esitettyjen merkintöjä avulla on mahdollista esittää +monenlaisia loogisia väitteitä. +Esimerkiksi +\[\forall x ((x>2 \land \lnot P(x)) \Rightarrow (\exists a (\exists b (x = ab \land a > 1 \land b > 1))))\] +tarkoittaa, että jos luku $x$ on suurempi +kuin 2 eikä ole alkuluku, +niin on olemassa luvut $a$ ja $b$, +joiden tulo on $x$ ja jotka molemmat ovat suurempia kuin 1. +Tämä väite pitää paikkansa kokonaislukujen joukossa. + +\subsubsection{Funktioita} + +Funktio $\lfloor x \rfloor$ pyöristää luvun $x$ +alaspäin kokonaisluvuksi ja +funktio $\lceil x \rceil$ pyöristää luvun $x$ +ylöspäin kokonaisluvuksi. Esimerkiksi +\[ \lfloor 3/2 \rfloor = 1 \hspace{10px} \textrm{ja} \hspace{10px} \lceil 3/2 \rceil = 2.\] + +Funktiot $\min(x_1,x_2,\ldots,x_n)$ +ja $\max(x_1,x_2,\ldots,x_n)$ +palauttavat pienimmän ja suurimman +arvoista $x_1,x_2,\ldots,x_n$. +Esimerkiksi +\[ \min(1,2,3)=1 \hspace{10px} \textrm{ja} \hspace{10px} \max(1,2,3)=3.\] + +\index{kertoma@kertoma} + +\key{Kertoma} $n!$ määritellään +\[\prod_{x=1}^n x = 1 \cdot 2 \cdot 3 \cdot \ldots \cdot n\] +tai vaihtoehtoisesti rekursiivisesti +\[ +\begin{array}{lcl} +0! & = & 1 \\ +n! & = & n \cdot (n-1)! \\ +\end{array} +\] + +\index{Fibonaccin luku@Fibonaccin luku} + +\key{Fibonaccin luvut} esiintyvät monissa erilaisissa yhteyksissä. +Ne määritellään seuraavasti rekursiivisesti: +\[ +\begin{array}{lcl} +f(0) & = & 0 \\ +f(1) & = & 1 \\ +f(n) & = & f(n-1)+f(n-2) \\ +\end{array} +\] +Ensimmäiset Fibonaccin luvut ovat +\[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, \ldots\] +Fibonaccin lukujen laskemiseen on olemassa myös +suljetun muodon kaava +\[f(n)=\frac{(1 + \sqrt{5})^n - (1-\sqrt{5})^n}{2^n \sqrt{5}}.\] + +\subsubsection{Logaritmi} + +\index{logaritmi@logaritmi} + +Luvun $x$ +\key{logaritmi} merkitään $\log_k(x)$, missä $k$ on logaritmin kantaluku. +Logaritmin määritelmän mukaan +$\log_k(x)=a$ tarkalleen silloin, kun $k^a=x$. + +Algoritmiikassa hyödyllinen tulkinta on, +että logaritmi $\log_k(x)$ ilmaisee, montako kertaa lukua $x$ +täytyy jakaa $k$:lla, ennen kuin tulos on 1. +Esimerkiksi $\log_2(32)=5$, +koska lukua 32 täytyy jakaa 2:lla 5 kertaa: + +\[32 \rightarrow 16 \rightarrow 8 \rightarrow 4 \rightarrow 2 \rightarrow 1 \] + +Logaritmi tulee usein vastaan algoritmien analyysissa, +koska monessa tehokkaassa algoritmissa jokin asia puolittuu +joka askeleella. +Niinpä logaritmin avulla voi arvioida algoritmin tehokkuutta. + +Logaritmille pätee kaava +\[\log_k(ab) = \log_k(a)+\log_k(b),\] +josta seuraa edelleen +\[\log_k(x^n) = n \cdot \log_k(x).\] +Samoin logaritmille pätee +\[\log_k\Big(\frac{a}{b}\Big) = \log_k(a)-\log_k(b).\] +Lisäksi on voimassa kaava +\[\log_u(x) = \frac{\log_k(x)}{\log_k(u)},\] +minkä ansiosta logaritmeja voi laskea mille tahansa kantaluvulle, +jos on keino laskea logaritmeja jollekin kantaluvulle. + +\index{luonnollinen logaritmi@luonnollinen logaritmi} +\index{Neperin luku@Neperin luku} + +Luvun $x$ \key{luonnollinen logaritmi} $\ln(x)$ on logaritmi, jonka kantaluku on +\key{Neperin luku} $e \approx 2{,}71828$. + +Vielä yksi logaritmin ominaisuus on, että +luvun $x$ numeroiden määrä $b$-kantaisessa +lukujärjestelmässä +on $\lfloor \log_b(x)+1 \rfloor$. +Esimerkiksi luvun $123$ esitys +2-järjestelmässä on 1111011 ja +$\lfloor \log_2(123)+1 \rfloor = 7$. + diff --git a/luku02.tex b/luku02.tex new file mode 100644 index 0000000..8d5888c --- /dev/null +++ b/luku02.tex @@ -0,0 +1,551 @@ +\chapter{Time complexity} + +\index{aikavaativuus@aikavaativuus} + +Kisakoodauksessa oleellinen asia on algoritmien tehokkuus. +Yleensä on helppoa suunnitella algoritmi, +joka ratkaisee tehtävän hitaasti, +mutta todellinen vaikeus piilee siinä, +kuinka keksiä nopeasti toimiva algoritmi. +Jos algoritmi on liian hidas, se tuottaa vain +osan pisteistä tai ei pisteitä lainkaan. + +\key{Aikavaativuus} on kätevä tapa arvioida, +kuinka nopeasti algoritmi toimii. +Se esittää algoritmin tehokkuuden funktiona, +jonka parametrina on syötteen koko. +Aikavaativuuden avulla algoritmista voi päätellä ennen koodaamista, +onko se riittävän tehokas tehtävän ratkaisuun. + +\section{Laskusäännöt} + +Algoritmin aikavaativuus merkitään $O(\cdots)$, +jossa kolmen pisteen tilalla +on kaava, joka kuvaa algoritmin ajankäyttöä. +Yleensä muuttuja $n$ esittää syötteen kokoa. +Esimerkiksi jos algoritmin syötteenä on taulukko lukuja, +$n$ on lukujen määrä, +ja jos syötteenä on merkkijono, +$n$ on merkkijonon pituus. + +\subsubsection*{Silmukat} + +Algoritmin ajankäyttö johtuu usein +pohjimmiltaan silmukoista, +jotka käyvät syötettä läpi. +Mitä enemmän sisäkkäisiä silmukoita +algoritmissa on, sitä hitaampi se on. +Jos sisäkkäisiä silmukoita on $k$, +aikavaativuus on $O(n^k)$. + +Esimerkiksi seuraavan koodin aikavaativuus on $O(n)$: +\begin{lstlisting} +for (int i = 1; i <= n; i++) { + // koodia +} +\end{lstlisting} + +Vastaavasti seuraavan koodin aikavaativuus on $O(n^2)$: +\begin{lstlisting} +for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + // koodia + } +} +\end{lstlisting} + +\subsubsection*{Suuruusluokka} + +Aikavaativuus ei kerro tarkasti, +montako kertaa silmukan sisällä oleva koodi suoritetaan, +vaan se kertoo vain suuruusluokan. +Esimerkiksi seuraavissa esimerkeissä silmukat +suoritetaan $3n$, $n+5$ ja $\lceil n/2 \rceil$ kertaa, +mutta kunkin koodin aikavaativuus on sama $O(n)$. + +\begin{lstlisting} +for (int i = 1; i <= 3*n; i++) { + // koodia +} +\end{lstlisting} + +\begin{lstlisting} +for (int i = 1; i <= n+5; i++) { + // koodia +} +\end{lstlisting} + +\begin{lstlisting} +for (int i = 1; i <= n; i += 2) { + // koodia +} +\end{lstlisting} + +Seuraavan koodin aikavaativuus on puolestaan $O(n^2)$: + +\begin{lstlisting} +for (int i = 1; i <= n; i++) { + for (int j = i+1; j <= n; j++) { + // koodia + } +} +\end{lstlisting} + +\subsubsection*{Peräkkäisyys} + +Jos koodissa on peräkkäisiä osia, +kokonaisaikavaativuus on suurin yksittäisen +osan aikavaativuus. +Tämä johtuu siitä, että koodin hitain +vaihe on yleensä koodin pullonkaula +ja muiden vaiheiden merkitys on pieni. + +Esimerkiksi seuraava koodi muodostuu +kolmesta osasta, +joiden aikavaativuudet ovat $O(n)$, $O(n^2)$ ja $O(n)$. +Niinpä koodin aikavaativuus on $O(n^2)$. + +\begin{lstlisting} +for (int i = 1; i <= n; i++) { + // koodia +} +for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + // koodia + } +} +for (int i = 1; i <= n; i++) { + // koodia +} +\end{lstlisting} + +\subsubsection*{Monta muuttujaa} + +Joskus syötteessä on monta muuttujaa, +jotka vaikuttavat aikavaativuuteen. +Tällöin myös aikavaativuuden kaavassa esiintyy +monta muuttujaa. + +Esimerkiksi seuraavan koodin +aikavaativuus on $O(nm)$: + +\begin{lstlisting} +for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + // koodia + } +} +\end{lstlisting} + +\subsubsection*{Rekursio} + +Rekursiivisen funktion aikavaativuuden +määrittää, montako kertaa funktiota kutsutaan yhteensä +ja mikä on yksittäisen kutsun aikavaativuus. +Kokonais\-aikavaativuus saadaan kertomalla +nämä arvot toisillaan. + +Tarkastellaan esimerkiksi seuraavaa funktiota: +\begin{lstlisting} +void f(int n) { + if (n == 1) return; + f(n-1); +} +\end{lstlisting} +Kutsu $\texttt{f}(n)$ aiheuttaa yhteensä $n$ funktiokutsua, +ja jokainen funktiokutsu vie aikaa $O(1)$, +joten aikavaativuus on $O(n)$. + +Tarkastellaan sitten seuraavaa funktiota: +\begin{lstlisting} +void g(int n) { + if (n == 1) return; + g(n-1); + g(n-1); +} +\end{lstlisting} +Tässä tapauksessa funktio haarautuu kahteen osaan, +joten kutsu $\texttt{g}(n)$ aiheuttaa kaikkiaan seuraavat kutsut: +\begin{center} +\begin{tabular}{rr} +kutsu & kerrat \\ +\hline +$\texttt{g}(n)$ & 1 \\ +$\texttt{g}(n-1)$ & 2 \\ +$\cdots$ & $\cdots$ \\ +$\texttt{g}(1)$ & $2^{n-1}$ \\ +\end{tabular} +\end{center} +Tämän perusteella kutsun $\texttt{g}(n)$ aikavaativuus on +\[1+2+4+\cdots+2^{n-1} = 2^n-1 = O(2^n).\] + +\section{Vaativuusluokkia} + +\index{vaativuusluokka@vaativuusluokka} + +Usein esiintyviä vaativuusluokkia ovat seuraavat: + +\begin{description} +\item[$O(1)$] +\index{vakioaikainen algoritmi@vakioaikainen algoritmi} +\key{Vakioaikainen} algoritmi +käyttää saman verran aikaa minkä tahansa +syötteen käsittelyyn, +eli algoritmin nopeus ei riipu syötteen koosta. +Tyypillinen vakioaikainen algoritmi on suora kaava +vastauksen laskemiseen. + +\item[$O(\log n)$] +\index{logaritminen algoritmi@logaritminen algoritmi} +\key{Logaritminen} aikavaativuus +syntyy usein siitä, että algoritmi +puolittaa syötteen koon joka askeleella. +Logaritmi $\log_2 n$ näet ilmaisee, montako +kertaa luku $n$ täytyy puolittaa, +ennen kuin tuloksena on 1. + +\item[$O(\sqrt n)$] + +Tällainen algoritmi sijoittuu +aikavaativuuksien $O(\log n)$ ja $O(n)$ välimaastoon. +Neliöjuuren erityinen ominaisuus on, +että $\sqrt n = n/\sqrt n$, joten neliöjuuri +osuu tietyllä tavalla syötteen puoliväliin. + +\item[$O(n)$] +\index{lineaarinen algoritmi@lineaarinen algoritmi} +\key{Lineaarinen} algoritmi käy syötteen läpi +kiinteän määrän kertoja. +Tämä on usein paras mahdollinen aikavaativuus, +koska yleensä syöte täytyy käydä +läpi ainakin kerran, +ennen kuin algoritmi voi ilmoittaa vastauksen. + +\item[$O(n \log n)$] + +Tämä aikavaativuus viittaa usein +syötteen järjestämiseen, +koska tehokkaat järjestämisalgoritmit toimivat +ajassa $O(n \log n)$. +Toinen mahdollisuus on, että algoritmi +käyttää tietorakennetta, +jonka operaatiot ovat $O(\log n)$-aikaisia. + +\item[$O(n^2)$] +\index{nelizllinen algoritmi@neliöllinen algoritmi} +\key{Neliöllinen} aikavaativuus voi syntyä +siitä, että algoritmissa on +kaksi sisäkkäistä silmukkaa. +Neliöllinen algoritmi voi käydä läpi kaikki +tavat valita joukosta kaksi alkiota. + +\item[$O(n^3)$] +\index{kuutiollinen algoritmi@kuutiollinen algoritmi} +\key{Kuutiollinen} aikavaativuus voi syntyä siitä, +että algoritmissa on +kolme sisäkkäistä silmukkaa. +Kuutiollinen algoritmi voi käydä läpi kaikki +tavat valita joukosta kolme alkiota. + +\item[$O(2^n)$] + +Tämä aikavaativuus tarkoittaa usein, +että algoritmi käy läpi kaikki syötteen osajoukot. +Esimerkiksi joukon $\{1,2,3\}$ osajoukot ovat +$\emptyset$, $\{1\}$, $\{2\}$, $\{3\}$, $\{1,2\}$, +$\{1,3\}$, $\{2,3\}$ sekä $\{1,2,3\}$. + +\item[$O(n!)$] + +Tämä aikavaativuus voi syntyä siitä, +että algoritmi käy läpi kaikki syötteen permutaatiot. +Esimerkiksi joukon $\{1,2,3\}$ permutaatiot ovat +$(1,2,3)$, $(1,3,2)$, $(2,1,3)$, $(2,3,1)$, +$(3,1,2)$ sekä $(3,2,1)$. + +\end{description} + +\index{polynominen algoritmi@polynominen algoritmi} +Algoritmi on \key{polynominen}, +jos sen aikavaativuus on korkeintaan $O(n^k)$, +kun $k$ on vakio. +Edellä mainituista aikavaativuuksista +kaikki paitsi $O(2^n)$ ja $O(n!)$ +ovat polynomisia. +Käytännössä vakio $k$ on yleensä pieni, +minkä ansiosta +polynomisuus kuvastaa sitä, +että algoritmi on \emph{tehokas}. +\index{NP-vaikea ongelma} + +Useimmat tässä kirjassa esitettävät algoritmit +ovat polynomisia. +Silti on paljon ongelmia, joihin ei tunneta +polynomista algoritmia eli ongelmaa ei osata +ratkaista tehokkaasti. +\key{NP-vaikeat} ongelmat ovat +tärkeä joukko ongelmia, +joihin ei tiedetä polynomista algoritmia. + +\section{Tehokkuuden arviointi} + +Aikavaativuuden hyötynä on, +että sen avulla voi arvioida ennen algoritmin +toteuttamista, onko algoritmi riittävän nopea +tehtävän ratkaisemiseen. +Lähtökohtana arviossa on, että nykyaikainen tietokone +pystyy suorittamaan sekunnissa joitakin +satoja miljoonia koodissa olevia komentoja. + +Oletetaan esimerkiksi, että tehtävän aikaraja on +yksi sekunti ja syötteen koko on $n=10^5$. +Jos algoritmin aikavaativuus on $O(n^2)$, +algoritmi suorittaa noin $(10^5)^2=10^{10}$ komentoa. +Tähän kuluu aikaa arviolta kymmeniä sekunteja, +joten algoritmi vaikuttaa liian hitaalta tehtävän ratkaisemiseen. + +Käänteisesti syötteen koosta voi päätellä, +kuinka tehokasta algoritmia tehtävän laatija odottaa +ratkaisijalta. +Seuraavassa taulukossa on joitakin hyödyllisiä arvioita, +jotka olettavat, että tehtävän aikaraja on yksi sekunti. + +\begin{center} +\begin{tabular}{ll} +syötteen koko ($n$) & haluttu aikavaativuus \\ +\hline +$n \le 10^{18}$ & $O(1)$ tai $O(\log n)$ \\ +$n \le 10^{12}$ & $O(\sqrt n)$ \\ +$n \le 10^6$ & $O(n)$ tai $O(n \log n)$ \\ +$n \le 5000$ & $O(n^2)$ \\ +$n \le 500$ & $O(n^3)$ \\ +$n \le 25$ & $O(2^n)$ \\ +$n \le 10$ & $O(n!)$ \\ +\end{tabular} +\end{center} + +Esimerkiksi jos syötteen koko on $n=10^5$, +tehtävän laatija odottaa luultavasti +algoritmia, jonka aikavaativuus on $O(n)$ tai $O(n \log n)$. +Tämä tieto helpottaa algoritmin suunnittelua, +koska se rajaa pois monia lähestymistapoja, +joiden tuloksena olisi hitaampi aikavaativuus. + +\index{vakiokerroin} + +Aikavaativuus ei kerro kuitenkaan kaikkea algoritmin +tehokkuudesta, koska se kätkee toteutuksessa olevat +\key{vakiokertoimet}. Esimerkiksi aikavaativuuden $O(n)$ +algoritmi voi tehdä käytännössä $n/2$ tai $5n$ operaatiota. +Tällä on merkittävä vaikutus algoritmin +todelliseen ajankäyttöön. + +\section{Suurin alitaulukon summa} + +\index{suurin alitaulukon summa@suurin alitaulukon summa} + +Usein ohjelmointitehtävän ratkaisuun on monta +luontevaa algoritmia, joiden aikavaativuudet eroavat. +Tutustumme seuraavaksi klassiseen ongelmaan, +jonka suoraviivaisen ratkaisun aikavaativuus on $O(n^3)$, +mutta algoritmia parantamalla aikavaativuudeksi +tulee ensin $O(n^2)$ ja lopulta $O(n)$. + +Annettuna on taulukko, jossa on $n$ kokonaislukua +$x_1,x_2,\ldots,x_n$, ja tehtävänä on etsiä +taulukon \key{suurin alitaulukon summa} +eli mahdollisimman suuri summa +taulukon yhtenäisellä välillä. +Tehtävän kiinnostavuus on siinä, että taulukossa +saattaa olla negatiivisia lukuja. +Esimerkiksi taulukossa +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$-1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$-3$}; +\node at (4.5,0.5) {$5$}; +\node at (5.5,0.5) {$2$}; +\node at (6.5,0.5) {$-5$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +\begin{samepage} +suurimman summan $10$ tuottaa seuraava alitaulukko: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (1,0) rectangle (6,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$-1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$-3$}; +\node at (4.5,0.5) {$5$}; +\node at (5.5,0.5) {$2$}; +\node at (6.5,0.5) {$-5$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +\end{samepage} + + +\subsubsection{Ratkaisu 1} + +Suoraviivainen ratkaisu tehtävään on käydä +läpi kaikki tavat valita alitaulukko taulukosta, +laskea jokaisesta vaihtoehdosta lukujen summa +ja pitää muistissa suurinta summaa. +Seuraava koodi toteuttaa tämän algoritmin: + +\begin{lstlisting} +int p = 0; +for (int a = 1; a <= n; a++) { + for (int b = a; b <= n; b++) { + int s = 0; + for (int c = a; c <= b; c++) { + s += x[c]; + } + p = max(p,s); + } +} +cout << p << "\n"; +\end{lstlisting} + +Koodi olettaa, että luvut on tallennettu taulukkoon \texttt{x}, +jota indeksoidaan $1 \ldots n$. +Muuttujat $a$ ja $b$ valitsevat alitaulukon ensimmäisen +ja viimeisen luvun, ja alitaulukon summa lasketaan muuttujaan $s$. +Muuttujassa $p$ on puolestaan paras haun aikana löydetty summa. + +Algoritmin aikavaativuus on $O(n^3)$, koska siinä on kolme +sisäkkäistä silmukkaa ja jokainen silmukka käy läpi $O(n)$ lukua. + +\subsubsection{Ratkaisu 2} + +Äskeistä ratkaisua on helppoa tehostaa hankkiutumalla +eroon sisimmästä silmukasta. +Tämä on mahdollista laskemalla summaa samalla, +kun alitaulukon oikea reuna liikkuu eteenpäin. +Tuloksena on seuraava koodi: + +\begin{lstlisting} +int p = 0; +for (int a = 1; a <= n; a++) { + int s = 0; + for (int b = a; b <= n; b++) { + s += x[b]; + p = max(p,s); + } +} +cout << p << "\n"; +\end{lstlisting} +Tämän muutoksen jälkeen koodin aikavaativuus on $O(n^2)$. + +\subsubsection{Ratkaisu 3} + +Yllättävää kyllä, tehtävään on olemassa myös +$O(n)$-aikainen ratkaisu eli koodista pystyy +karsimaan vielä yhden silmukan. +Ideana on laskea taulukon jokaiseen +kohtaan, mikä on suurin alitaulukon +summa, jos alitaulukko päättyy kyseiseen kohtaan. +Tämän jälkeen ratkaisu tehtävään on suurin +näistä summista. + +Tarkastellaan suurimman summan tuottavan +alitaulukon etsimistä, +kun valittuna on alitaulukon loppukohta $k$. +Vaihtoehtoja on kaksi: +\begin{enumerate} +\item Alitaulukossa on vain kohdassa $k$ oleva luku. +\item Alitaulukossa on ensin jokin kohtaan $k-1$ päättyvä alitaulukko +ja sen jälkeen kohdassa $k$ oleva luku. +\end{enumerate} + +Koska tavoitteena on löytää alitaulukko, +jonka lukujen summa on suurin, +tapauksessa 2 myös kohtaan $k-1$ päättyvän +alitaulukon tulee olla sellainen, +että sen summa on suurin. +Niinpä tehokas ratkaisu syntyy käymällä läpi +kaikki alitaulukon loppukohdat järjestyksessä +ja laskemalla jokaiseen kohtaan suurin +mahdollinen kyseiseen kohtaan päättyvän alitaulukon summa. + +Seuraava koodi toteuttaa ratkaisun: + +\begin{lstlisting} +int p = 0, s = 0; +for (int k = 1; k <= n; k++) { + s = max(x[k],s+x[k]); + p = max(p,s); +} +cout << p << "\n"; +\end{lstlisting} + +Algoritmissa on vain yksi silmukka, +joka käy läpi taulukon luvut, +joten sen aikavaativuus on $O(n)$. +Tämä on myös paras mahdollinen aikavaativuus, +koska minkä tahansa algoritmin täytyy käydä +läpi ainakin kerran taulukon sisältö. + +\subsubsection{Tehokkuusvertailu} + +On kiinnostavaa tutkia, kuinka tehokkaita algoritmit +ovat käytännössä. +Seuraava taulukko näyttää, kuinka nopeasti äskeiset +ratkaisut toimivat eri $n$:n arvoilla +nykyaikaisella tietokoneella. + +Jokaisessa testissä syöte on muodostettu satunnaisesti. +Ajankäyttöön ei ole laskettu syötteen lukemiseen +kuluvaa aikaa. + +\begin{center} +\begin{tabular}{rrrr} +taulukon koko $n$ & ratkaisu 1 & ratkaisu 2 & ratkaisu 3 \\ +\hline +$10^2$ & $0{,}0$ s & $0{,}0$ s & $0{,}0$ s \\ +$10^3$ & $0{,}1$ s & $0{,}0$ s & $0{,}0$ s \\ +$10^4$ & > $10,0$ s & $0{,}1$ s & $0{,}0$ s \\ +$10^5$ & > $10,0$ s & $5{,}3$ s & $0{,}0$ s \\ +$10^6$ & > $10,0$ s & > $10,0$ s & $0{,}0$ s \\ +$10^7$ & > $10,0$ s & > $10,0$ s & $0{,}0$ s \\ +\end{tabular} +\end{center} + +Vertailu osoittaa, +että pienillä syötteillä kaikki algoritmit +ovat tehokkaita, +mutta suuremmat syötteet tuovat esille +merkittäviä eroja algoritmien suoritusajassa. +$O(n^3)$-aikainen ratkaisu 1 alkaa hidastua, +kun $n=10^3$, ja $O(n^2)$-aikainen ratkaisu 2 +alkaa hidastua, kun $n=10^4$. +Vain $O(n)$-aikainen ratkaisu 3 selvittää +suurimmatkin syötteet salamannopeasti. diff --git a/luku03.tex b/luku03.tex new file mode 100644 index 0000000..b1392b8 --- /dev/null +++ b/luku03.tex @@ -0,0 +1,972 @@ +\chapter{Sorting} + +\index{jxrjestxminen@järjestäminen} + +\key{Järjestäminen} +on keskeinen algoritmiikan ongelma. +Moni tehokas algoritmi +perustuu järjestämiseen, +koska järjestetyn tiedon +käsittely on helpompaa +kuin sekalaisessa järjestyksessä olevan. + +Esimerkiksi kysymys ''onko taulukossa kahta samaa +alkiota?'' ratkeaa tehokkaasti järjestämisen avulla. +Jos taulukossa on kaksi samaa alkiota, +ne ovat järjestämisen jälkeen peräkkäin, +jolloin niiden löytäminen on helppoa. +Samaan tapaan ratkeaa myös kysymys +''mikä on yleisin alkio taulukossa?''. + +Järjestämiseen on kehitetty monia +algoritmeja, jotka tarjoavat hyviä +esimerkkejä algoritmien suunnittelun tekniikoista. +Tehokkaat yleiset järjestämis\-algoritmit +toimivat ajassa $O(n \log n)$, ja tämä aikavaativuus +on myös monella järjestämistä käyttävällä algoritmilla. + +\section{Järjestämisen teoriaa} + +Järjestämisen perusongelma on seuraava: +\begin{framed} +\noindent +Annettuna on taulukko, jossa on $n$ alkiota. +Tehtäväsi on järjestää alkiot pienimmästä +suurimpaan. +\end{framed} +\noindent +Esimerkiksi taulukko +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$8$}; +\node at (3.5,0.5) {$2$}; +\node at (4.5,0.5) {$9$}; +\node at (5.5,0.5) {$2$}; +\node at (6.5,0.5) {$5$}; +\node at (7.5,0.5) {$6$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +on järjestettynä seuraava: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$3$}; +\node at (4.5,0.5) {$5$}; +\node at (5.5,0.5) {$6$}; +\node at (6.5,0.5) {$8$}; +\node at (7.5,0.5) {$9$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +\subsubsection{$O(n^2)$-algoritmit} + +\index{kuplajxrjestxminen@kuplajärjestäminen} + +Yksinkertaiset algoritmit taulukon +järjestämiseen vievät aikaa $O(n^2)$. +Tällaiset algoritmit ovat lyhyitä ja +muodostuvat tyypillisesti +kahdesta sisäkkäisestä silmukasta. +Tunnettu $O(n^2)$-aikainen algoritmi on +\key{kuplajärjestäminen}, +jossa alkiot ''kuplivat'' eteenpäin taulukossa +niiden suuruuden perusteella. + +Kuplajärjestäminen muodostuu $n-1$ kierroksesta, +joista jokainen käy taulukon läpi vasemmalta oikealle. +Aina kun taulukosta löytyy kaksi vierekkäistä +alkiota, joiden järjestys on väärä, algoritmi +korjaa niiden järjestyksen. +Algoritmin voi toteuttaa seuraavasti +taulukolle +$\texttt{t}[1],\texttt{t}[2],\ldots,\texttt{t}[n]$: +\begin{lstlisting} +for (int i = 1; i <= n-1; i++) { + for (int j = 1; j <= n-i; j++) { + if (t[j] > t[j+1]) swap(t[j],t[j+1]); + } +} +\end{lstlisting} + +Algoritmin ensimmäisen kierroksen jälkeen suurin +alkio on paikallaan, toisen kierroksen jälkeen +kaksi suurinta alkiota on paikallaan, jne. +Niinpä $n-1$ kierroksen jälkeen koko taulukko +on järjestyksessä. + +Esimerkiksi taulukossa + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$8$}; +\node at (3.5,0.5) {$2$}; +\node at (4.5,0.5) {$9$}; +\node at (5.5,0.5) {$2$}; +\node at (6.5,0.5) {$5$}; +\node at (7.5,0.5) {$6$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +\noindent +kuplajärjestämisen ensimmäinen +läpikäynti tekee seuraavat vaihdot: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$8$}; +\node at (4.5,0.5) {$9$}; +\node at (5.5,0.5) {$2$}; +\node at (6.5,0.5) {$5$}; +\node at (7.5,0.5) {$6$}; + +\draw[thick,<->] (3.5,-0.25) .. controls (3.25,-1.00) and (2.75,-1.00) .. (2.5,-0.25); + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$8$}; +\node at (4.5,0.5) {$2$}; +\node at (5.5,0.5) {$9$}; +\node at (6.5,0.5) {$5$}; +\node at (7.5,0.5) {$6$}; + +\draw[thick,<->] (5.5,-0.25) .. controls (5.25,-1.00) and (4.75,-1.00) .. (4.5,-0.25); + + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$8$}; +\node at (4.5,0.5) {$2$}; +\node at (5.5,0.5) {$5$}; +\node at (6.5,0.5) {$9$}; +\node at (7.5,0.5) {$6$}; + +\draw[thick,<->] (6.5,-0.25) .. controls (6.25,-1.00) and (5.75,-1.00) .. (5.5,-0.25); + + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$8$}; +\node at (4.5,0.5) {$2$}; +\node at (5.5,0.5) {$5$}; +\node at (6.5,0.5) {$6$}; +\node at (7.5,0.5) {$9$}; + +\draw[thick,<->] (7.5,-0.25) .. controls (7.25,-1.00) and (6.75,-1.00) .. (6.5,-0.25); + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +\subsubsection{Inversiot} + +\index{inversio@inversio} + +Kuplajärjestäminen on esimerkki algoritmista, +joka perustuu taulukon vierekkäisten alkioiden +vaihtamiseen keskenään. +Osoittautuu, että tällaisen algoritmin +aikavaativuus on \emph{aina} vähintään $O(n^2)$, +koska pahimmassa tapauksessa taulukon +järjestäminen vaatii $O(n^2)$ alkioparin vaihtamista. + +Hyödyllinen käsite järjestämisalgoritmien +analyysissa on \key{inversio}. +Se on taulukossa oleva alkiopari +$(\texttt{t}[a],\texttt{t}[b])$, +missä $a\texttt{t}[b]$ +eli alkiot ovat väärässä järjestyksessä taulukossa. +Esimerkiksi taulukon +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$6$}; +\node at (4.5,0.5) {$3$}; +\node at (5.5,0.5) {$5$}; +\node at (6.5,0.5) {$9$}; +\node at (7.5,0.5) {$8$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +inversiot ovat $(6,3)$, $(6,5)$ ja $(9,8)$. +Inversioiden määrä kuvaa, miten lähellä +järjestystä taulukko on. +Taulukko on järjestyksessä tarkalleen +silloin, kun siinä ei ole yhtään inversiota. +Inversioiden määrä on puolestaan suurin, +kun taulukon järjestys on käänteinen, +jolloin inversioita on +\[1+2+\cdots+(n-1)=\frac{n(n-1)}{2} = O(n^2).\] + +Jos vierekkäiset taulukon alkiot +ovat väärässä järjestyksessä, +niiden järjestyksen korjaaminen +poistaa taulukosta tarkalleen yhden inversion. +Niinpä jos järjestämisalgoritmi pystyy +vaihtamaan keskenään vain +taulukon vierekkäisiä alkioita, +jokainen vaihto voi poistaa enintään yhden inversion +ja algoritmin aikavaativuus on varmasti ainakin $O(n^2)$. + +\subsubsection{$O(n \log n)$-algoritmit} + +\index{lomitusjxrjestxminen@lomitusjärjestäminen} + +Taulukon järjestäminen on mahdollista +tehokkaasti ajassa $O(n \log n)$ +algoritmilla, joka ei rajoitu vierekkäisten +alkoiden vaihtamiseen. +Yksi tällainen algoritmi on +\key{lomitusjärjestäminen}, +joka järjestää taulukon +rekursiivisesti jakamalla sen +pienemmiksi osataulukoiksi. + +Lomitusjärjestäminen järjestää taulukon välin +$[a,b]$ seuraavasti: + +\begin{enumerate} +\item Jos $a=b$, älä tee mitään, koska väli on valmiiksi järjestyksessä. +\item Valitse välin jakokohdaksi $k=\lfloor (a+b)/2 \rfloor$. +\item Järjestä rekursiivisesti välin $[a,k]$ alkiot. +\item Järjestä rekursiivisesti välin $[k+1,b]$ alkiot. +\item \emph{Lomita} järjestetyt välit $[a,k]$ ja $[k+1,b]$ +järjestetyksi väliksi $[a,b]$. +\end{enumerate} + +Lomitusjärjestämisen tehokkuus perustuu siihen, +että se puolittaa joka askeleella välin kahteen osaan. +Rekursiosta muodostuu yhteensä $O(\log n)$ tasoa +ja jokaisen tason käsittely vie aikaa $O(n)$. +Kohdan 5 lomittaminen on mahdollista ajassa $O(n)$, +koska välit $[a,k]$ ja $[k+1,b]$ on jo järjestetty. + +Tarkastellaan esimerkkinä seuraavan taulukon +järjestämistä: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$6$}; +\node at (3.5,0.5) {$2$}; +\node at (4.5,0.5) {$8$}; +\node at (5.5,0.5) {$2$}; +\node at (6.5,0.5) {$5$}; +\node at (7.5,0.5) {$9$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Taulukko jakautuu ensin kahdeksi +osataulukoksi seuraavasti: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (4,1); +\draw (5,0) grid (9,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$6$}; +\node at (3.5,0.5) {$2$}; + +\node at (5.5,0.5) {$8$}; +\node at (6.5,0.5) {$2$}; +\node at (7.5,0.5) {$5$}; +\node at (8.5,0.5) {$9$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (5.5,1.4) {$5$}; +\node at (6.5,1.4) {$6$}; +\node at (7.5,1.4) {$7$}; +\node at (8.5,1.4) {$8$}; + +\end{tikzpicture} +\end{center} + +Algoritmi järjestää osataulukot rekursiivisesti, +jolloin tuloksena on: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (4,1); +\draw (5,0) grid (9,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$3$}; +\node at (3.5,0.5) {$6$}; + +\node at (5.5,0.5) {$2$}; +\node at (6.5,0.5) {$5$}; +\node at (7.5,0.5) {$8$}; +\node at (8.5,0.5) {$9$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (5.5,1.4) {$5$}; +\node at (6.5,1.4) {$6$}; +\node at (7.5,1.4) {$7$}; +\node at (8.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Lopuksi algoritmi lomittaa järjestetyt osataulukot, +jolloin syntyy lopullinen järjestetty taulukko: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$3$}; +\node at (4.5,0.5) {$5$}; +\node at (5.5,0.5) {$6$}; +\node at (6.5,0.5) {$8$}; +\node at (7.5,0.5) {$9$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +\subsubsection{Järjestämisen alaraja} + +Onko sitten mahdollista järjestää taulukkoa +nopeammin kuin ajassa $O(n \log n)$? +Osoittautuu, että tämä \emph{ei} ole mahdollista, +kun rajoitumme +järjestämis\-algoritmeihin, +jotka perustuvat taulukon alkioiden +vertailemiseen. + +Aikavaativuuden alaraja on mahdollista todistaa +tarkastelemalla järjestämistä +prosessina, jossa jokainen kahden alkion vertailu +antaa lisää tietoa taulukon sisällöstä. +Prosessista muodostuu seuraavanlainen puu: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) rectangle (3,1); +\node at (1.5,0.5) {$x < y?$}; + +\draw[thick,->] (1.5,0) -- (-2.5,-1.5); +\draw[thick,->] (1.5,0) -- (5.5,-1.5); + +\draw (-4,-2.5) rectangle (-1,-1.5); +\draw (4,-2.5) rectangle (7,-1.5); +\node at (-2.5,-2) {$x < y?$}; +\node at (5.5,-2) {$x < y?$}; + +\draw[thick,->] (-2.5,-2.5) -- (-4.5,-4); +\draw[thick,->] (-2.5,-2.5) -- (-0.5,-4); +\draw[thick,->] (5.5,-2.5) -- (3.5,-4); +\draw[thick,->] (5.5,-2.5) -- (7.5,-4); + +\draw (-6,-5) rectangle (-3,-4); +\draw (-2,-5) rectangle (1,-4); +\draw (2,-5) rectangle (5,-4); +\draw (6,-5) rectangle (9,-4); +\node at (-4.5,-4.5) {$x < y?$}; +\node at (-0.5,-4.5) {$x < y?$}; +\node at (3.5,-4.5) {$x < y?$}; +\node at (7.5,-4.5) {$x < y?$}; + +\draw[thick,->] (-4.5,-5) -- (-5.5,-6); +\draw[thick,->] (-4.5,-5) -- (-3.5,-6); +\draw[thick,->] (-0.5,-5) -- (0.5,-6); +\draw[thick,->] (-0.5,-5) -- (-1.5,-6); +\draw[thick,->] (3.5,-5) -- (2.5,-6); +\draw[thick,->] (3.5,-5) -- (4.5,-6); +\draw[thick,->] (7.5,-5) -- (6.5,-6); +\draw[thick,->] (7.5,-5) -- (8.5,-6); +\end{tikzpicture} +\end{center} + +Merkintä ''$x v = {4,2,5,3,5,8,3}; +sort(v.begin(),v.end()); +\end{lstlisting} +Järjestämisen seurauksena +vektorin sisällöksi tulee +$\{2,3,3,4,5,5,8\}$. +Oletuksena \texttt{sort}-funktio järjestää +siis alkiot pienimmästä suurimpaan, +mutta järjestyksen saa käänteiseksi näin: +\begin{lstlisting} +sort(v.rbegin(),v.rend()); +\end{lstlisting} +Tavallisen taulukon voi järjestää seuraavasti: +\begin{lstlisting} +int n = 7; // taulukon koko +int t[] = {4,2,5,3,5,8,3}; +sort(t,t+n); +\end{lstlisting} +Seuraava koodi taas järjestää merkkijonon \texttt{s}: +\begin{lstlisting} +string s = "apina"; +sort(s.begin(), s.end()); +\end{lstlisting} +Merkkijonon järjestäminen tarkoittaa, +että sen merkit järjestetään aakkosjärjestykseen. +Esimerkiksi merkkijono ''apina'' +on järjestettynä ''aainp''. + +\subsubsection{Vertailuoperaattori} + +\index{vertailuoperaattori@vertailuoperaattori} + +Funktion \texttt{sort} käyttäminen vaatii, +että järjestettävien alkioiden +tietotyypille on määritelty \key{vertailuoperaattori} \texttt{<}, +jonka avulla voi selvittää, mikä on kahden alkion järjestys. +Järjestämisen aikana \texttt{sort}-funktio +käyttää operaattoria \texttt{<} aina, kun sen täytyy +vertailla järjestettäviä alkioita. + +Vertailuoperaattori on määritelty valmiiksi +useimmille C++:n tietotyypeille, +minkä ansiosta niitä pystyy järjestämään automaattisesti. +Jos järjestettävänä on lukuja, ne järjestyvät +suuruusjärjestykseen, +ja jos järjestettävänä on merkkijonoja, +ne järjestyvät aakkosjärjestykseen. + +\index{pair@\texttt{pair}} + +Parit (\texttt{pair}) järjestyvät ensisijaisesti +ensimmäisen kentän (\texttt{first}) mukaan. +Jos kuitenkin parien ensimmäiset kentät ovat samat, +järjestys määräytyy toisen kentän (\texttt{second}) mukaan: +\begin{lstlisting} +vector> v; +v.push_back({1,5}); +v.push_back({2,3}); +v.push_back({1,2}); +sort(v.begin(), v.end()); +\end{lstlisting} +Tämän seurauksena parien järjestys on +$(1,2)$, $(1,5)$ ja $(2,3)$. + +\index{tuple@\texttt{tuple}} +Vastaavasti \texttt{tuple}-rakenteet +järjestyvät ensisijaisesti ensimmäisen kentän, +toissijaisesti toisen kentän, jne., mukaan: +\begin{lstlisting} +vector> v; +v.push_back(make_tuple(2,1,4)); +v.push_back(make_tuple(1,5,3)); +v.push_back(make_tuple(2,1,3)); +sort(v.begin(), v.end()); +\end{lstlisting} +Tämän seurauksena järjestys on +$(1,5,3)$, $(2,1,3)$ ja $(2,1,4)$. + +\subsubsection{Omat tietueet} + +Jos järjestettävänä on omia tietueita, +niiden vertailuoperaattori täytyy toteuttaa itse. +Operaattori määritellään tietueen sisään +\texttt{operator<}-nimisenä funktiona, +jonka parametrina on toinen alkio. +Operaattorin tulee palauttaa \texttt{true}, +jos oma alkio on pienempi kuin parametrialkio, +ja muuten \texttt{false}. + +Esimerkiksi seuraava tietue \texttt{P} +sisältää pisteen x- ja y-koordinaatit. +Vertailuoperaattori on toteutettu niin, +että pisteet järjestyvät ensisijaisesti x-koor\-di\-naa\-tin +ja toissijaisesti y-koordinaatin mukaan. + +\begin{lstlisting} +struct P { + int x, y; + bool operator<(const P &p) { + if (x != p.x) return x < p.x; + else return y < p.y; + } +}; +\end{lstlisting} + +\subsubsection{Vertailufunktio} + +\index{vertailufunktio@vertailufunktio} + +On myös mahdollista antaa +\texttt{sort}-funktiolle ulkopuolinen \key{vertailufunktio}. +Esimerkiksi seuraava vertailufunktio +järjestää merkkijonot ensisijaisesti pituuden mukaan +ja toissijaisesti aakkosjärjestyksen mukaan: + +\begin{lstlisting} +bool cmp(string a, string b) { + if (a.size() != b.size()) return a.size() < b.size(); + return a < b; +} +\end{lstlisting} +Tämän jälkeen merkkijonovektorin voi järjestää näin: +\begin{lstlisting} +sort(v.begin(), v.end(), cmp); +\end{lstlisting} + +\section{Binäärihaku} + +\index{binxxrihaku@binäärihaku} + +Tavallinen tapa etsiä alkiota taulukosta +on käyttää \texttt{for}-silmukkaa, joka käy läpi +taulukon sisällön alusta loppuun. +Esimerkiksi seuraava koodi etsii alkiota +$x$ taulukosta \texttt{t}. + +\begin{lstlisting} +for (int i = 1; i <= n; i++) { + if (t[i] == x) // alkio x löytyi kohdasta i +} +\end{lstlisting} + +Tämän menetelmän aikavaativuus on $O(n)$, +koska pahimmassa tapauksessa koko taulukko täytyy +käydä läpi. +Jos taulukon sisältö voi olla mikä tahansa, +tämä on kuitenkin tehokkain mahdollinen menetelmä, +koska saatavilla ei ole lisätietoa siitä, +mistä päin taulukkoa alkiota $x$ kannattaa etsiä. + +Tilanne on toinen silloin, kun taulukko on +järjestyksessä. +Tässä tapauksessa haku on mahdollista toteuttaa +paljon nopeammin, koska taulukon järjestys +ohjaa etsimään alkiota oikeasta suunnasta. +Seuraavaksi käsiteltävä \key{binäärihaku} +löytää alkion järjestetystä taulukosta +tehokkaasti ajassa $O(\log n)$. + +\subsubsection{Toteutus 1} + +Perinteinen tapa toteuttaa binäärihaku muistuttaa sanan etsimistä +sanakirjasta. Haku puolittaa joka askeleella hakualueen taulukossa, +kunnes lopulta etsittävä alkio löytyy tai osoittautuu, +että sitä ei ole taulukossa. + +Haku tarkistaa ensin taulukon keskimmäisen alkion. +Jos keskimmäinen alkio on etsittävä alkio, haku päättyy. +Muuten haku jatkuu taulukon vasempaan tai oikeaan osaan sen mukaan, +onko keskimmäinen alkio suurempi vain pienempi kuin etsittävä alkio. + +Yllä olevan idean voi toteuttaa seuraavasti: +\begin{lstlisting} +int a = 1, b = n; +while (a <= b) { + int k = (a+b)/2; + if (t[k] == x) // alkio x löytyi kohdasta k + if (t[k] > x) b = k-1; + else a = k+1; +} +\end{lstlisting} + +Algoritmi pitää yllä väliä $a \ldots b$, joka on +jäljellä oleva hakualue taulukossa. +Aluksi väli on $1 \ldots n$ eli koko taulukko. +Välin koko puolittuu algoritmin joka vaiheessa, +joten aikavaativuus on $O(\log n)$. + +\subsubsection{Toteutus 2} + +Vaihtoehtoinen tapa toteuttaa binäärihaku +perustuu taulukon tehostettuun läpikäyntiin. +Ideana on käydä taulukkoa läpi hyppien +ja hidastaa vauhtia, kun etsittävä alkio lähestyy. + +Haku käy taulukkoa läpi vasemmalta oikealle aloittaen +hypyn pituudesta $n/2$. +Joka vaiheessa hypyn pituus puolittuu: +ensin $n/4$, sitten $n/8$, sitten $n/16$ jne., +kunnes lopulta hypyn pituus on 1. +Hyppyjen jälkeen joko haettava alkio on löytynyt +tai selviää, että sitä ei ole taulukossa. + +Seuraava koodi toteuttaa äskeisen idean: +\begin{lstlisting} +int k = 1; +for (int b = n/2; b >= 1; b /= 2) { + while (k+b <= n && t[k+b] <= x) k += b; +} +if (t[k] == x) // alkio x löytyi kohdasta k +\end{lstlisting} + +Muuttuja $k$ on läpikäynnin kohta taulukossa +ja muuttuja $b$ on hypyn pituus. +Jos alkio $x$ esiintyy taulukossa, +sen kohta on muuttujassa $k$ algoritmin päätteeksi. +Algoritmin aikavaativuus on $O(\log n)$, +koska \texttt{while}-silmukassa oleva koodi suoritetaan +aina enintään kahdesti. + +\subsubsection{Muutoskohdan etsiminen} + +Käytännössä binäärihakua tarvitsee toteuttaa +harvoin alkion etsimiseen taulukosta, +koska sen sijasta voi käyttää standardikirjastoa. +Esimerkiksi C++:n funktiot \texttt{lower\_bound} +ja \texttt{upper\_bound} toteuttavat binäärihaun +ja tietorakenne \texttt{set} ylläpitää joukkoa, +jonka operaatiot ovat $O(\log n)$-aikaisia. + +Sitäkin tärkeämpi binäärihaun käyttökohde on +funktion \key{muutoskohdan} etsiminen. +Oletetaan, että haluamme löytää pienimmän arvon $k$, +joka on kelvollinen ratkaisu ongelmaan. +Käytössämme on funktio $\texttt{ok}(x)$, +joka palauttaa \texttt{true}, jos $x$ on kelvollinen +ratkaisu, ja muuten \texttt{false}. +Lisäksi tiedämme, että $\texttt{ok}(x)$ on \texttt{false} +aina kun $x= 1; b /= 2) { + while (!ok(x+b)) x += b; +} +int k = x+1; +\end{lstlisting} + +Haku etsii suurimman $x$:n arvon, +jolla $\texttt{ok}(x)$ on \texttt{false}. +Niinpä tästä seuraava arvo $k=x+1$ +on pienin arvo, jolla $\texttt{ok}(k)$ on \texttt{true}. +Hypyn aloituspituus $z$ tulee olla +sopiva suuri luku, esimerkiksi sellainen, +jolla $\texttt{ok}(z)$ on varmasti \texttt{true}. + +Algoritmi kutsuu $O(\log z)$ kertaa funktiota +\texttt{ok}, joten kokonaisaikavaativuus +riippuu siitä, kauanko funktion \texttt{ok} +suoritus kestää. +Esimerkiksi jos ratkaisun tarkastus +vie aikaa $O(n)$, niin kokonaisaikavaativuus +on $O(n \log z)$. + +\subsubsection{Huippuarvon etsiminen} + +Binäärihaulla voi myös etsiä +suurimman arvon funktiolle, +joka on ensin kasvava ja sitten laskeva. +Toisin sanoen tehtävänä on etsiä arvo +$k$ niin, että + +\begin{itemize} +\item +$f(x)f(x+1)$, kun $x >= k$. +\end{itemize} + +Ideana on etsiä binäärihaulla +viimeinen kohta $x$, +jossa pätee $f(x)f(x+2)$. +Seuraava koodi toteuttaa haun: + +\begin{lstlisting} +int x = -1; +for (int b = z; b >= 1; b /= 2) { + while (f(x+b) < f(x+b+1)) x += b; +} +int k = x+1; +\end{lstlisting} + +Huomaa, että toisin kuin tavallisessa binäärihaussa, +tässä ei ole sallittua, +että peräkkäiset arvot olisivat yhtä suuria. +Silloin ei olisi mahdollista tietää, +mihin suuntaan hakua tulee jatkaa. + + + diff --git a/luku04.tex b/luku04.tex new file mode 100644 index 0000000..039a0b8 --- /dev/null +++ b/luku04.tex @@ -0,0 +1,774 @@ +\chapter{Data structures} + +\index{tietorakenne@tietorakenne} + +\key{Tietorakenne} +on tapa säilyttää tietoa tietokoneen muistissa. +Sopivan tietorakenteen valinta on tärkeää, +koska kullakin rakenteella on omat +vahvuutensa ja heikkoutensa. +Tietorakenteen valinnassa oleellinen kysymys on, +mitkä operaatiot rakenne toteuttaa tehokkaasti. + +Tämä luku esittelee keskeisimmät +C++:n standardikirjaston tietorakenteet. +Valmiita tietorakenteita kannattaa käyttää +aina kun mahdollista, +koska se säästää paljon aikaa toteutuksessa. +Myöhemmin kirjassa tutustumme erikoisempiin +rakenteisiin, joita ei ole valmiina C++:ssa. + +\section{Dynaaminen taulukko} + +\index{vektori@vektori} +\index{vector@\texttt{vector}} + +\key{Dynaaminen taulukko} on taulukko, +jonka kokoa voi muuttaa +ohjelman suorituksen aikana. +C++:n tavallisin dynaaminen taulukko +on \key{vektori} (\texttt{vector}). +Sitä voi käyttää hyvin samalla tavalla +kuin tavallista taulukkoa. + +Seuraava koodi luo tyhjän vektorin +ja lisää siihen kolme lukua: + +\begin{lstlisting} +vector v; +v.push_back(3); // [3] +v.push_back(2); // [3,2] +v.push_back(5); // [3,2,5] +\end{lstlisting} + +Tämän jälkeen vektorin sisältöä voi käsitellä taulukon tavoin: + +\begin{lstlisting} +cout << v[0] << "\n"; // 3 +cout << v[1] << "\n"; // 2 +cout << v[2] << "\n"; // 5 +\end{lstlisting} + +Funktio \texttt{size} kertoo, montako alkiota vektorissa on. +Seuraava koodi käy läpi ja tulostaa kaikki vektorin alkiot: + +\begin{lstlisting} +for (int i = 0; i < v.size(); i++) { + cout << v[i] << "\n"; +} +\end{lstlisting} + +\begin{samepage} +Vektorin voi käydä myös läpi lyhyemmin näin: + +\begin{lstlisting} +for (auto x : v) { + cout << x << "\n"; +} +\end{lstlisting} +\end{samepage} + +Funktio \texttt{back} hakee vektorin viimeisen alkion, +ja funktio \texttt{pop\_back} poistaa vektorin +viimeisen alkion: + +\begin{lstlisting} +vector v; +v.push_back(5); +v.push_back(2); +cout << v.back() << "\n"; // 2 +v.pop_back(); +cout << v.back() << "\n"; // 5 +\end{lstlisting} + +Vektorin sisällön voi antaa myös sen luonnissa: + +\begin{lstlisting} +vector v = {2,4,2,5,1}; +\end{lstlisting} + +Kolmas tapa luoda vektori on ilmoittaa +vektorin koko ja alkuarvo: + +\begin{lstlisting} +// koko 10, alkuarvo 0 +vector v(10); +\end{lstlisting} +\begin{lstlisting} +// koko 10, alkuarvo 5 +vector v(10, 5); +\end{lstlisting} + +Vektori on toteutettu sisäisesti tavallisena taulukkona. +Jos vektorin koko kasvaa ja taulukko jää liian pieneksi, +varataan uusi suurempi taulukko, johon kopioidaan +vektorin sisältö. +Näin tapahtuu kuitenkin niin harvoin, että vektorin +funktion \texttt{push\_back} aikavaativuus on +keskimäärin $O(1)$. + +\index{merkkijono@merkkijono} +\index{string@\texttt{string}} + +Myös \key{merkkijono} (\texttt{string}) on dynaaminen taulukko, +jota pystyy käsittelemään lähes samaan +tapaan kuin vektoria. +Merkkijonon käsittelyyn liittyy lisäksi erikoissyntaksia +ja funktioita, joita ei ole muissa tietorakenteissa. +Merkkijonoja voi yhdistää toisiinsa \texttt{+}-merkin avulla. +Funktio $\texttt{substr}(k,x)$ erottaa merkkijonosta +osajonon, joka alkaa kohdasta $k$ ja jonka pituus on $x$. +Funktio $\texttt{find}(\texttt{t})$ etsii kohdan, +jossa osajono \texttt{t} esiintyy merkkijonossa. + +Seuraava koodi esittelee merkkijonon käyttämistä: + +\begin{lstlisting} +string a = "hatti"; +string b = a+a; +cout << b << "\n"; // hattihatti +b[5] = 'v'; +cout << b << "\n"; // hattivatti +string c = b.substr(3,4); +cout << c << "\n"; // tiva +\end{lstlisting} + +\section{Joukkorakenne} + +\index{joukko@joukko} +\index{set@\texttt{set}} +\index{unordered\_set@\texttt{unordered\_set}} + +\key{Joukko} on tietorakenne, +joka sisältää kokoelman alkioita. +Joukon perusoperaatiot ovat alkion lisäys, +haku ja poisto. + +C++ sisältää kaksi +toteutusta joukolle: \texttt{set} ja \texttt{unordered\_set}. +Rakenne \texttt{set} perustuu tasapainoiseen +binääripuuhun, ja sen operaatioiden aikavaativuus +on $O(\log n)$. +Rakenne \texttt{unordered\_set} pohjautuu hajautustauluun, +ja sen operaatioiden aikavaativuus on keskimäärin $O(1)$. + +Usein on makuasia, kumpaa joukon toteutusta käyttää. +Rakenteen \texttt{set} etuna on, että se säilyttää +joukon alkioita järjestyksessä ja tarjoaa +järjestykseen liittyviä funktioita, +joita \texttt{unordered\_set} ei sisällä. +Toisaalta \texttt{unordered\_set} on usein nopeampi rakenne. + +Seuraava koodi luo lukuja sisältävän joukon ja +esittelee sen käyttämistä. +Funktio \texttt{insert} lisää joukkoon alkion, +funktio \texttt{count} laskee alkion määrän joukossa +ja funktio \texttt{erase} poistaa alkion joukosta. + +\begin{lstlisting} +set s; +s.insert(3); +s.insert(2); +s.insert(5); +cout << s.count(3) << "\n"; // 1 +cout << s.count(4) << "\n"; // 0 +s.erase(3); +s.insert(4); +cout << s.count(3) << "\n"; // 0 +cout << s.count(4) << "\n"; // 1 +\end{lstlisting} + +Joukkoa voi käsitellä muuten suunnilleen samalla tavalla +kuin vektoria, mutta joukkoa ei voi indeksoida +\texttt{[]}-merkinnällä. +Seuraava koodi luo joukon, tulostaa sen +alkioiden määrän ja käy sitten läpi kaikki alkiot. +\begin{lstlisting} +set s = {2,5,6,8}; +cout << s.size() << "\n"; // 4 +for (auto x : s) { + cout << x << "\n"; +} +\end{lstlisting} + +Tärkeä joukon ominaisuus on, +että tietty alkio voi esiintyä siinä +enintään kerran. +Niinpä funktio \texttt{count} palauttaa aina +arvon 0 (alkiota ei ole joukossa) tai 1 (alkio on joukossa) +ja funktio \texttt{insert} ei lisää alkiota +uudestaan joukkoon, jos se on siellä valmiina. +Seuraava koodi havainnollistaa asiaa: + +\begin{lstlisting} +set s; +s.insert(5); +s.insert(5); +s.insert(5); +cout << s.count(5) << "\n"; // 1 +\end{lstlisting} + +\index{multiset@\texttt{multiset}} +\index{unordered\_multiset@\texttt{unordered\_multiset}} + +C++ sisältää myös rakenteet +\texttt{multiset} ja \texttt{unordered\_multiset}, +jotka toimivat muuten samalla tavalla kuin \texttt{set} +ja \texttt{unordered\_set}, +mutta sama alkio voi esiintyä +monta kertaa joukossa. +Esimerkiksi seuraavassa koodissa +kaikki luvun 5 kopiot lisätään joukkoon: + +\begin{lstlisting} +multiset s; +s.insert(5); +s.insert(5); +s.insert(5); +cout << s.count(5) << "\n"; // 3 +\end{lstlisting} +Funktio \texttt{erase} poistaa +kaikki alkion esiintymät +\texttt{multiset}-rakenteessa: +\begin{lstlisting} +s.erase(5); +cout << s.count(5) << "\n"; // 0 +\end{lstlisting} +Usein kuitenkin tulisi poistaa +vain yksi esiintymä, +mikä onnistuu näin: +\begin{lstlisting} +s.erase(s.find(5)); +cout << s.count(5) << "\n"; // 2 +\end{lstlisting} + +\section{Hakemisto} + +\index{hakemisto@hakemisto} +\index{map@\texttt{map}} +\index{unordered\_map@\texttt{unordered\_map}} + +\key{Hakemisto} on taulukon yleistys, +joka sisältää kokoelman avain-arvo-pareja. +Siinä missä taulukon avaimet ovat aina peräkkäiset +kokonaisluvut $0,1,\ldots,n-1$, +missä $n$ on taulukon koko, +hakemiston avaimet voivat +olla mitä tahansa tyyppiä +eikä niiden tarvitse olla peräkkäin. + +C++ sisältää kaksi toteutusta hakemistolle +samaan tapaan kuin joukolle. +Rakenne +\texttt{map} perustuu +tasapainoiseen binääripuuhun ja sen +alkioiden käsittely vie aikaa $O(\log n)$, +kun taas rakenne +\texttt{unordered\_map} perustuu +hajautustauluun ja sen alkioiden +käsittely vie keskimäärin aikaa $O(1)$. + +Seuraava koodi toteuttaa hakemiston, +jossa avaimet ovat merkkijonoja ja +arvot ovat kokonaislukuja: + +\begin{lstlisting} +map m; +m["apina"] = 4; +m["banaani"] = 3; +m["cembalo"] = 9; +cout << m["banaani"] << "\n"; // 3 +\end{lstlisting} + +Jos hakemistosta hakee avainta, +jota ei ole siinä, +avain lisätään hakemistoon +automaattisesti oletusarvolla. +Esimerkiksi seuraavassa koodissa +hakemistoon ilmestyy avain ''aybabtu'', +jonka arvona on 0: + +\begin{lstlisting} +map m; +cout << m["aybabtu"] << "\n"; // 0 +\end{lstlisting} +Funktiolla \texttt{count} voi +tutkia, esiintyykö avain hakemistossa: +\begin{lstlisting} +if (m.count("aybabtu")) { + cout << "avain on hakemistossa"; +} +\end{lstlisting} +Seuraava koodi listaa hakemiston +kaikki avaimet ja arvot: +\begin{lstlisting} +for (auto x : m) { + cout << x.first << " " << x.second << "\n"; +} +\end{lstlisting} + +\section{Iteraattorit ja välit} + +\index{iteraattori@iteraattori} + +Monet C++:n standardikirjaston funktiot +käsittelevät tietorakenteiden iteraattoreita +ja niiden määrittelemiä välejä. +\key{Iteraattori} on muuttuja, +joka osoittaa tiettyyn tietorakenteen alkioon. + +Usein tarvittavat iteraattorit ovat \texttt{begin} +ja \texttt{end}, jotka rajaavat välin, +joka sisältää kaikki tietorakenteen alkiot. +Iteraattori \texttt{begin} osoittaa +tietorakenteen ensimmäiseen alkioon, +kun taas iteraattori \texttt{end} osoittaa +tietorakenteen viimeisen alkion jälkeiseen kohtaan. +Tilanne on siis tällainen: + +\begin{center} +\begin{tabular}{llllllllll} +\{ & 3, & 4, & 6, & 8, & 12, & 13, & 14, & 17 & \} \\ +& $\uparrow$ & & & & & & & & $\uparrow$ \\ +& \multicolumn{3}{l}{\texttt{s.begin()}} & & & & & & \texttt{s.end()} \\ +\end{tabular} +\end{center} + +Huomaa epäsymmetria iteraattoreissa: +\texttt{s.begin()} osoittaa tietorakenteen alkioon, +kun taas \texttt{s.end()} osoittaa tietorakenteen ulkopuolelle. +Iteraattoreiden rajaama joukon väli on siis \emph{puoliavoin}. + +\subsubsection{Välien käsittely} + +Iteraattoreita tarvitsee +C++:n standardikirjaston funktioissa, jotka käsittelevät +tietorakenteen välejä. +Yleensä halutaan käsitellä tietorakenteiden kaikkia +alkioita, jolloin funktiolle annetaan +iteraattorit \texttt{begin} ja \texttt{end}. + +Esimerkiksi seuraava koodi järjestää vektorin funktiolla \texttt{sort}, +kääntää sitten alkioiden järjestyksen funktiolla \texttt{reverse} +ja sekoittaa lopuksi alkioiden järjestyksen funktiolla \texttt{random\_shuffle}. + +\index{sort@\texttt{sort}} +\index{reverse@\texttt{reverse}} +\index{random\_shuffle@\texttt{random\_shuffle}} + +\begin{lstlisting} +sort(v.begin(), v.end()); +reverse(v.begin(), v.end()); +random_shuffle(v.begin(), v.end()); +\end{lstlisting} + +Samoja funktioita voi myös käyttää tavallisen taulukon +yhteydessä, jolloin iteraattorin sijasta annetaan +osoitin taulukkoon: + +\begin{lstlisting} +sort(t, t+n); +reverse(t, t+n); +random_shuffle(t, t+n); +\end{lstlisting} + +\subsubsection{Joukon iteraattorit} + +Iteraattoreita tarvitsee usein joukon +alkioiden käsittelyssä. +Seuraava koodi määrittelee iteraattorin +\texttt{it}, joka osoittaa joukon \texttt{s} alkuun: +\begin{lstlisting} +set::iterator it = s.begin(); +\end{lstlisting} +Koodin voi kirjoittaa myös lyhyemmin näin: +\begin{lstlisting} +auto it = s.begin(); +\end{lstlisting} +Iteraattoria vastaavaan joukon alkioon +pääsee käsiksi \texttt{*}-merkinnällä. +Esimerkiksi seuraava koodi tulostaa +joukon ensimmäisen alkion: + +\begin{lstlisting} +auto it = s.begin(); +cout << *it << "\n"; +\end{lstlisting} + +Iteraattoria pystyy liikuttamaan +operaatioilla \texttt{++} (eteenpäin) +ja \texttt{---} (taaksepäin). +Tällöin iteraattori siirtyy seuraavaan +tai edelliseen alkioon joukossa. + +Seuraava koodi tulostaa joukon kaikki alkiot: + +\begin{lstlisting} +for (auto it = s.begin(); it != s.end(); it++) { + cout << *it << "\n"; +} +\end{lstlisting} +Seuraava koodi taas tulostaa joukon +viimeisen alkion: + +\begin{lstlisting} +auto it = s.end(); +it--; +cout << *it << "\n"; +\end{lstlisting} +% Iteraattoria täytyi liikuttaa askel taaksepäin, +% koska se osoitti aluksi joukon viimeisen +% alkion jälkeiseen kohtaan. + +Funktio $\texttt{find}(x)$ palauttaa iteraattorin +joukon alkioon, jonka arvo on $x$. +Poikkeuksena jos alkiota $x$ ei esiinny joukossa, +iteraattoriksi tulee \texttt{end}. + +\begin{lstlisting} +auto it = s.find(x); +if (it == s.end()) cout << "x puuttuu joukosta"; +\end{lstlisting} + +Funktio $\texttt{lower\_bound}(x)$ palauttaa +iteraattorin joukon pienimpään alkioon, +joka on ainakin yhtä suuri kuin $x$. +Vastaavasti $\texttt{upper\_bound}(x)$ palauttaa +iteraattorin pienimpään alkioon, +joka on suurempi kuin $x$. +Jos tällaisia alkioita ei ole joukossa, +funktiot palauttavat arvon \texttt{end}. +Näitä funktioita ei voi käyttää +\texttt{unordered\_set}-rakenteessa, +joka ei pidä yllä alkioiden järjestystä. + +\begin{samepage} +Esimerkiksi seuraava koodi etsii joukosta +alkion, joka on lähinnä lukua $x$: + +\begin{lstlisting} +auto a = s.lower_bound(x); +if (a == s.begin() && a == s.end()) { + cout << "joukko on tyhjä\n"; +} else if (a == s.begin()) { + cout << *a << "\n"; +} else if (a == s.end()) { + a--; + cout << *a << "\n"; +} else { + auto b = a; b--; + if (x-*b < *a-x) cout << *b << "\n"; + else cout << *a << "\n"; +} +\end{lstlisting} + +Koodi käy läpi mahdolliset tapaukset +iteraattorin \texttt{a} avulla. +Iteraattori +osoittaa aluksi pienimpään alkioon, +joka on ainakin yhtä suuri kuin $x$. +Jos \texttt{a} on samaan aikaan \texttt{begin} +ja \texttt{end}, joukko on tyhjä. +Muuten jos \texttt{a} on \texttt{begin}, +sen osoittama alkio on $x$:ää lähin alkio. +Jos taas \texttt{a} on \texttt{end}, +$x$:ää lähin alkio on joukon viimeinen alkio. +Jos mikään edellisistä tapauksista ei päde, +niin $x$:ää lähin alkio +on joko $a$:n osoittama alkio tai sitä edellinen alkio. +\end{samepage} + +\section{Muita tietorakenteita} + +\subsubsection{Bittijoukko} + +\index{bittijoukko@bittijoukko} +\index{bitset@\texttt{bitset}} + +\key{Bittijoukko} (\texttt{bitset}) on taulukko, +jonka jokaisen alkion arvo on 0 tai 1. +Esimerkiksi +seuraava koodi luo bittijoukon, jossa on 10 alkiota. +\begin{lstlisting} +bitset<10> s; +s[2] = 1; +s[5] = 1; +s[6] = 1; +s[8] = 1; +cout << s[4] << "\n"; // 0 +cout << s[5] << "\n"; // 1 +\end{lstlisting} + +Bittijoukon etuna on, että se vie tavallista +taulukkoa vähemmän muistia, +koska jokainen alkio vie +vain yhden bitin muistia. +Esimerkiksi $n$ bitin tallentaminen +\texttt{int}-taulukkona vie $32n$ +bittiä tilaa, mutta bittijoukkona +vain $n$ bittiä tilaa. +Lisäksi bittijoukon sisältöä +voi käsitellä tehokkaasti bittioperaatioilla, +minkä ansiosta sillä voi tehostaa algoritmeja. + +Seuraava koodi näyttää toisen tavan +bittijoukon luomiseen: + +\begin{lstlisting} +bitset<10> s(string("0010011010")); +cout << s[4] << "\n"; // 0 +cout << s[5] << "\n"; // 1 +\end{lstlisting} + +Funktio \texttt{count} palauttaa +bittijoukon ykkösbittien määrän: + +\begin{lstlisting} +bitset<10> s(string("0010011010")); +cout << s.count() << "\n"; // 4 +\end{lstlisting} + +Seuraava koodi näyttää esimerkkejä +bittioperaatioiden käyttämisestä: +\begin{lstlisting} +bitset<10> a(string("0010110110")); +bitset<10> b(string("1011011000")); +cout << (a&b) << "\n"; // 0010010000 +cout << (a|b) << "\n"; // 1011111110 +cout << (a^b) << "\n"; // 1001101110 +\end{lstlisting} + +\subsubsection{Pakka} + +\index{pakka@pakka} +\index{deque@\texttt{deque}} + +\key{Pakka} (\texttt{deque}) on dynaaminen taulukko, +jonka kokoa pystyy muuttamaan tehokkaasti +sekä alku- että loppupäässä. +Pakka sisältää vektorin tavoin +funktiot \texttt{push\_back} +ja \texttt{pop\_back}, mutta siinä on lisäksi myös funktiot +\texttt{push\_front} ja \texttt{pop\_front}, +jotka käsittelevät taulukon alkua. + +Seuraava koodi esittelee pakan käyttämistä: + +\begin{lstlisting} +deque d; +d.push_back(5); // [5] +d.push_back(2); // [5,2] +d.push_front(3); // [3,5,2] +d.pop_back(); // [3,5] +d.pop_front(); // [5] +\end{lstlisting} + +Pakan sisäinen toteutus on monimutkaisempi kuin +vektorissa, minkä vuoksi se on +vektoria raskaampi rakenne. +Kuitenkin lisäyksen ja poiston +aikavaativuus on keskimäärin $O(1)$ molemmissa päissä. + +\subsubsection{Pino} + +\index{pino@pino} +\index{stack@\texttt{stack}} + +\key{Pino} (\texttt{stack}) on tietorakenne, +joka tarjoaa kaksi $O(1)$-aikaista +operaatiota: +alkion lisäys pinon päälle ja alkion +poisto pinon päältä. +Pinossa ei ole mahdollista käsitellä muita +alkioita kuin pinon päällimmäistä alkiota. + +Seuraava koodi esittelee pinon käyttämistä: + +\begin{lstlisting} +stack s; +s.push(3); +s.push(2); +s.push(5); +cout << s.top(); // 5 +s.pop(); +cout << s.top(); // 2 +\end{lstlisting} +\subsubsection{Jono} + +\index{jono@jono} +\index{queue@\texttt{queue}} + +\key{Jono} (\texttt{queue}) on kuin pino, +mutta alkion lisäys tapahtuu jonon loppuun +ja alkion poisto tapahtuu jonon alusta. +Jonossa on mahdollista käsitellä vain +alussa ja lopussa olevaa alkiota. + +Seuraava koodi esittelee jonon käyttämistä: + +\begin{lstlisting} +queue s; +s.push(3); +s.push(2); +s.push(5); +cout << s.front(); // 3 +s.pop(); +cout << s.front(); // 2 +\end{lstlisting} +% +% Huomaa, että rakenteiden \texttt{stack} ja \texttt{queue} +% sijasta voi aina käyttää rakenteita +% \texttt{vector} ja \texttt{deque}, joilla voi +% tehdä kaiken saman ja enemmän. +% Kuitenkin \texttt{stack} ja \texttt{queue} ovat +% kevyempiä ja hieman tehokkaampia rakenteita, +% jos niiden operaatiot riittävät algoritmin toteuttamiseen. + +\subsubsection{Prioriteettijono} + +\index{prioriteettijono@prioriteettijono} +\index{keko@keko} +\index{priority\_queue@\texttt{priority\_queue}} + +\key{Prioriteettijono} (\texttt{priority\_queue}) +pitää yllä joukkoa alkioista. +Sen operaatiot ovat alkion lisäys ja +jonon tyypistä riippuen joko +pienimmän alkion haku ja poisto tai +suurimman alkion haku ja poisto. +Lisäyksen ja poiston aikavaativuus on $O(\log n)$ +ja haun aikavaativuus on $O(1)$. + +Vaikka prioriteettijonon operaatiot +pystyy toteuttamaan myös \texttt{set}-ra\-ken\-teel\-la, +prioriteettijonon etuna on, +että sen kekoon perustuva sisäinen +toteutus on yksinkertaisempi +kuin \texttt{set}-rakenteen tasapainoinen binääripuu, +minkä vuoksi rakenne on kevyempi ja +operaatiot ovat tehokkaampia. + +\begin{samepage} +C++:n prioriteettijono toimii oletuksena niin, +että alkiot ovat järjestyksessä suurimmasta pienimpään +ja jonosta pystyy hakemaan ja poistamaan suurimman alkion. +Seuraava koodi esittelee prioriteettijonon käyttämistä: + +\begin{lstlisting} +priority_queue q; +q.push(3); +q.push(5); +q.push(7); +q.push(2); +cout << q.top() << "\n"; // 7 +q.pop(); +cout << q.top() << "\n"; // 5 +q.pop(); +q.push(6); +cout << q.top() << "\n"; // 6 +q.pop(); +\end{lstlisting} +\end{samepage} + +Seuraava määrittely luo käänteisen prioriteettijonon, +jossa jonosta pystyy hakemaan ja poistamaan pienimmän alkion: + +\begin{lstlisting} +priority_queue,greater> q; +\end{lstlisting} + +\section{Vertailu järjestämiseen} + +Monen tehtävän voi ratkaista tehokkaasti joko +käyttäen sopivia tietorakenteita +tai taulukon järjestämistä. +Vaikka erilaiset ratkaisutavat olisivat kaikki +periaatteessa tehokkaita, niissä voi olla +käytännössä merkittäviä eroja. + +Tarkastellaan ongelmaa, jossa +annettuna on kaksi listaa $A$ ja $B$, +joista kummassakin on $n$ kokonaislukua. +Tehtävänä on selvittää, moniko luku +esiintyy kummassakin listassa. +Esimerkiksi jos listat ovat +\[A = [5,2,8,9,4] \hspace{10px} \textrm{ja} \hspace{10px} B = [3,2,9,5],\] +niin vastaus on 3, koska luvut 2, 5 +ja 9 esiintyvät kummassakin listassa. +Suoraviivainen ratkaisu tehtävään on käydä läpi +kaikki lukuparit ajassa $O(n^2)$, mutta seuraavaksi +keskitymme tehokkaampiin ratkaisuihin. + +\subsubsection{Ratkaisu 1} + +Tallennetaan listan $A$ luvut joukkoon +ja käydään sitten läpi listan $B$ luvut ja +tarkistetaan jokaisesta, esiintyykö se myös listassa $A$. +Joukon ansiosta on tehokasta tarkastaa, +esiintyykö listan $B$ luku listassa $A$. +Kun joukko toteutetaan \texttt{set}-rakenteella, +algoritmin aikavaativuus on $O(n \log n)$. + +\subsubsection{Ratkaisu 2} + +Joukon ei tarvitse säilyttää lukuja +järjestyksessä, joten +\texttt{set}-ra\-ken\-teen sijasta voi +käyttää myös \texttt{unordered\_set}-ra\-ken\-net\-ta. +Tämä on helppo tapa parantaa algoritmin +tehokkuutta, koska +algoritmin toteutus säilyy samana ja vain tietorakenne vaihtuu. +Uuden algoritmin aikavaativuus on $O(n)$. + +\subsubsection{Ratkaisu 3} + +Tietorakenteiden sijasta voimme käyttää järjestämistä. +Järjestetään ensin listat $A$ ja $B$, +minkä jälkeen yhteiset luvut voi löytää +käymällä listat rinnakkain läpi. +Järjestämisen aikavaativuus on $O(n \log n)$ ja +läpikäynnin aikavaativuus on $O(n)$, +joten kokonaisaikavaativuus on $O(n \log n)$. + +\subsubsection{Tehokkuusvertailu} + +Seuraavassa taulukossa on mittaustuloksia +äskeisten algoritmien tehokkuudesta, +kun $n$ vaihtelee ja listojen luvut ovat +satunnaisia lukuja välillä $1 \ldots 10^9$: + +\begin{center} +\begin{tabular}{rrrr} +$n$ & ratkaisu 1 & ratkaisu 2 & ratkaisu 3 \\ +\hline +$10^6$ & $1{,}5$ s & $0{,}3$ s & $0{,}2$ s \\ +$2 \cdot 10^6$ & $3{,}7$ s & $0{,}8$ s & $0{,}3$ s \\ +$3 \cdot 10^6$ & $5{,}7$ s & $1{,}3$ s & $0{,}5$ s \\ +$4 \cdot 10^6$ & $7{,}7$ s & $1{,}7$ s & $0{,}7$ s \\ +$5 \cdot 10^6$ & $10{,}0$ s & $2{,}3$ s & $0{,}9$ s \\ +\end{tabular} +\end{center} + +Ratkaisut 1 ja 2 ovat muuten samanlaisia, +mutta ratkaisu 1 käyttää \texttt{set}-rakennetta, +kun taas ratkaisu 2 käyttää +\texttt{unordered\_set}-rakennetta. +Tässä tapauksessa tällä valinnalla on +merkittävä vaikutus suoritusaikaan, +koska ratkaisu 2 on 4–5 kertaa +nopeampi kuin ratkaisu 1. + +Tehokkain ratkaisu on kuitenkin järjestämistä +käyttävä ratkaisu 3, joka on vielä puolet +nopeampi kuin ratkaisu 2. +Kiinnostavaa on, että sekä ratkaisun 1 että +ratkaisun 3 aikavaativuus on $O(n \log n)$, +mutta siitä huolimatta +ratkaisu 3 vie aikaa vain kymmenesosan. +Tämän voi selittää sillä, että +järjestäminen on kevyt +operaatio ja se täytyy tehdä vain kerran +ratkaisussa 3 algoritmin alussa, +minkä jälkeen algoritmin loppuosa on lineaarinen. +Ratkaisu 1 taas pitää yllä monimutkaista +tasapainoista binääripuuta koko algoritmin ajan. \ No newline at end of file diff --git a/luku05.tex b/luku05.tex new file mode 100644 index 0000000..cf32673 --- /dev/null +++ b/luku05.tex @@ -0,0 +1,756 @@ +\chapter{Complete search} + +\key{Täydellinen haku} +on yleispätevä tapa ratkaista +lähes mikä tahansa ohjelmointitehtävä. +Ideana on käydä läpi raa'alla voimalla kaikki +mahdolliset tehtävän ratkaisut ja tehtävästä riippuen +valita paras ratkaisu +tai laskea ratkaisuiden yhteismäärä. + +Täydellinen haku on hyvä menetelmä, jos kaikki +ratkaisut ehtii käydä läpi, +koska haku on yleensä suoraviivainen toteuttaa +ja se antaa varmasti oikean vastauksen. +Jos täydellinen haku on liian hidas, +seuraavien lukujen ahneet algoritmit tai +dynaaminen ohjelmointi voivat soveltua +tehtävään. + +\section{Osajoukkojen läpikäynti} + +\index{osajoukko@osajoukko} + +Aloitamme tapauksesta, jossa tehtävän +mahdollisia ratkaisuja ovat +$n$-alkioisen joukon osajoukot. +Tällöin täydellisen haun tulee +käydä läpi kaikki joukon osa\-joukot, +joita on yhteensä $2^n$ kappaletta. +Käymme läpi kaksi menetelmää +tällaisen haun toteuttamiseen. + +\subsubsection{Menetelmä 1} + +Kätevä tapa käydä läpi +kaikki joukon osajoukot on +käyttää rekursiota. +Seuraava funktio \texttt{haku} muodostaa +joukon $\{1,2,\ldots,n\}$ osajoukot. +Funktio pitää yllä vektoria \texttt{v}, +johon se kokoaa osajoukossa olevat luvut. +Osajoukkojen muodostaminen alkaa +tekemällä funktiokutsu \texttt{haku(1)}. + +\begin{lstlisting} +void haku(int k) { + if (k == n+1) { + // käsittele osajoukko v + } else { + haku(k+1); + v.push_back(k); + haku(k+1); + v.pop_back(); + } +} +\end{lstlisting} + +Funktion parametri $k$ on luku, +joka on ehdolla lisättäväksi osajoukkoon seuraavaksi. +Joka kutsulla funktio haarautuu kahteen tapaukseen: +joko luku $k$ lisätään tai ei lisätä osajoukkoon. +Aina kun $k=n+1$, kaikki luvut on käyty läpi +ja yksi osajoukko on muodostettu. + +Esimerkiksi kun $n=3$, funktiokutsut +muodostavat seuraavan kuvan mukaisen puun. +Joka kutsussa +vasen haara jättää luvun pois osajoukosta +ja oikea haara lisää sen osajoukkoon. +\begin{center} +\begin{tikzpicture}[scale=.45] + \begin{scope} + \small + \node at (0,0) {$\texttt{haku}(1)$}; + + \node at (-8,-4) {$\texttt{haku}(2)$}; + \node at (8,-4) {$\texttt{haku}(2)$}; + + \path[draw,thick,->] (0,0-0.5) -- (-8,-4+0.5); + \path[draw,thick,->] (0,0-0.5) -- (8,-4+0.5); + + \node at (-12,-8) {$\texttt{haku}(3)$}; + \node at (-4,-8) {$\texttt{haku}(3)$}; + \node at (4,-8) {$\texttt{haku}(3)$}; + \node at (12,-8) {$\texttt{haku}(3)$}; + + \path[draw,thick,->] (-8,-4-0.5) -- (-12,-8+0.5); + \path[draw,thick,->] (-8,-4-0.5) -- (-4,-8+0.5); + \path[draw,thick,->] (8,-4-0.5) -- (4,-8+0.5); + \path[draw,thick,->] (8,-4-0.5) -- (12,-8+0.5); + + \node at (-14,-12) {$\texttt{haku}(4)$}; + \node at (-10,-12) {$\texttt{haku}(4)$}; + \node at (-6,-12) {$\texttt{haku}(4)$}; + \node at (-2,-12) {$\texttt{haku}(4)$}; + \node at (2,-12) {$\texttt{haku}(4)$}; + \node at (6,-12) {$\texttt{haku}(4)$}; + \node at (10,-12) {$\texttt{haku}(4)$}; + \node at (14,-12) {$\texttt{haku}(4)$}; + + \node at (-14,-13.5) {$\emptyset$}; + \node at (-10,-13.5) {$\{3\}$}; + \node at (-6,-13.5) {$\{2\}$}; + \node at (-2,-13.5) {$\{2,3\}$}; + \node at (2,-13.5) {$\{1\}$}; + \node at (6,-13.5) {$\{1,3\}$}; + \node at (10,-13.5) {$\{1,2\}$}; + \node at (14,-13.5) {$\{1,2,3\}$}; + + + \path[draw,thick,->] (-12,-8-0.5) -- (-14,-12+0.5); + \path[draw,thick,->] (-12,-8-0.5) -- (-10,-12+0.5); + \path[draw,thick,->] (-4,-8-0.5) -- (-6,-12+0.5); + \path[draw,thick,->] (-4,-8-0.5) -- (-2,-12+0.5); + \path[draw,thick,->] (4,-8-0.5) -- (2,-12+0.5); + \path[draw,thick,->] (4,-8-0.5) -- (6,-12+0.5); + \path[draw,thick,->] (12,-8-0.5) -- (10,-12+0.5); + \path[draw,thick,->] (12,-8-0.5) -- (14,-12+0.5); +\end{scope} +\end{tikzpicture} +\end{center} + +\subsubsection{Menetelmä 2} + +Toinen tapa käydä osajoukot läpi on hyödyntää kokonaislukujen +bittiesitystä. Jokainen $n$ alkion osajoukko +voidaan esittää $n$ bitin jonona, +joka taas vastaa lukua väliltä $0 \ldots 2^n-1$. +Bittiesityksen ykkösbitit ilmaisevat, +mitkä joukon alkiot on valittu osajoukkoon. + +Tavallinen käytäntö on tulkita kokonaisluvun +bittiesitys osajoukkona niin, +että alkio $k$ kuuluu osajoukkoon, +jos lopusta lukien $k$. bitti on 1. +Esimerkiksi luvun 25 bittiesitys on 11001, +mikä vastaa osajoukkoa $\{1,4,5\}$. + +Seuraava koodi käy läpi $n$ alkion joukon +osajoukkojen bittiesitykset: + +\begin{lstlisting} +for (int b = 0; b < (1< v; + for (int i = 0; i < n; i++) { + if (b&(1< v; +for (int i = 1; i <= n; i++) { + v.push_back(i); +} +do { + // käsittele permutaatio v +} while (next_permutation(v.begin(),v.end())); +\end{lstlisting} + +\section{Peruuttava haku} + +\index{peruuttava haku@peruuttava haku} + +\key{Peruuttava haku} +aloittaa ratkaisun etsimisen tyhjästä +ja laajentaa ratkaisua askel kerrallaan. +Joka askeleella haku haarautuu kaikkiin +mahdollisiin suuntiin, joihin ratkaisua voi laajentaa. +Haaran tutkimisen jälkeen haku peruuttaa takaisin +ja jatkaa muihin mahdollisiin suuntiin. + +\index{kuningatarongelma} + +Tarkastellaan esimerkkinä \key{kuningatarongelmaa}, +jossa laskettavana on, +monellako tavalla $n \times n$ -shakkilaudalle +voidaan asettaa $n$ kuningatarta niin, +että mitkään kaksi kuningatarta eivät uhkaa toisiaan. +Esimerkiksi kun $n=4$, mahdolliset ratkaisut ovat seuraavat: + +\begin{center} +\begin{tikzpicture}[scale=.65] + \begin{scope} + \draw (0, 0) grid (4, 4); + \node at (1.5,3.5) {$K$}; + \node at (3.5,2.5) {$K$}; + \node at (0.5,1.5) {$K$}; + \node at (2.5,0.5) {$K$}; + + \draw (6, 0) grid (10, 4); + \node at (6+2.5,3.5) {$K$}; + \node at (6+0.5,2.5) {$K$}; + \node at (6+3.5,1.5) {$K$}; + \node at (6+1.5,0.5) {$K$}; + + \end{scope} +\end{tikzpicture} +\end{center} + +Tehtävän voi ratkaista peruuttavalla haulla +muodostamalla ratkaisua rivi kerrallaan. +Jokaisella rivillä täytyy valita yksi ruuduista, +johon sijoitetaan kuningatar niin, +ettei se uhkaa mitään aiemmin lisättyä kuningatarta. +Ratkaisu on valmis, kun viimeisellekin +riville on lisätty kuningatar. + +Esimerkiksi kun $n=4$, osa peruuttavan haun muodostamasta +puusta näyttää seuraavalta: + +\begin{center} +\begin{tikzpicture}[scale=.55] + \begin{scope} + \draw (0, 0) grid (4, 4); + + \draw (-9, -6) grid (-5, -2); + \draw (-3, -6) grid (1, -2); + \draw (3, -6) grid (7, -2); + \draw (9, -6) grid (13, -2); + + \node at (-9+0.5,-3+0.5) {$K$}; + \node at (-3+1+0.5,-3+0.5) {$K$}; + \node at (3+2+0.5,-3+0.5) {$K$}; + \node at (9+3+0.5,-3+0.5) {$K$}; + + \draw (2,0) -- (-7,-2); + \draw (2,0) -- (-1,-2); + \draw (2,0) -- (5,-2); + \draw (2,0) -- (11,-2); + + \draw (-11, -12) grid (-7, -8); + \draw (-6, -12) grid (-2, -8); + \draw (-1, -12) grid (3, -8); + \draw (4, -12) grid (8, -8); + \draw[white] (11, -12) grid (15, -8); + \node at (-11+1+0.5,-9+0.5) {$K$}; + \node at (-6+1+0.5,-9+0.5) {$K$}; + \node at (-1+1+0.5,-9+0.5) {$K$}; + \node at (4+1+0.5,-9+0.5) {$K$}; + \node at (-11+0+0.5,-10+0.5) {$K$}; + \node at (-6+1+0.5,-10+0.5) {$K$}; + \node at (-1+2+0.5,-10+0.5) {$K$}; + \node at (4+3+0.5,-10+0.5) {$K$}; + + \draw (-1,-6) -- (-9,-8); + \draw (-1,-6) -- (-4,-8); + \draw (-1,-6) -- (1,-8); + \draw (-1,-6) -- (6,-8); + + \node at (-9,-13) {\ding{55}}; + \node at (-4,-13) {\ding{55}}; + \node at (1,-13) {\ding{55}}; + \node at (6,-13) {\ding{51}}; + + \end{scope} +\end{tikzpicture} +\end{center} + +Kuvan alimmalla tasolla kolme ensimmäistä osaratkaisua +eivät kelpaa, koska niissä kuningattaret uhkaavat +toisiaan. +Sen sijaan neljäs osaratkaisu kelpaa, +ja sitä on mahdollista laajentaa loppuun asti +kokonaiseksi ratkaisuksi +asettamalla vielä kaksi kuningatarta laudalle. + +\begin{samepage} +Seuraava koodi toteuttaa peruuttavan haun: + +\begin{lstlisting} +void haku(int y) { + if (y == n) { + c++; + return; + } + for (int x = 0; x < n; x++) { + if (r1[x] || r2[x+y] || r3[x-y+n-1]) continue; + r1[x] = r2[x+y] = r3[x-y+n-1] = 1; + haku(y+1); + r1[x] = r2[x+y] = r3[x-y+n-1] = 0; + } +} +\end{lstlisting} +\end{samepage} +Haku alkaa kutsumalla funktiota \texttt{haku(0)}. +Laudan koko on muuttujassa $n$, +ja koodi laskee ratkaisuiden määrän +muuttujaan $c$. + +Koodi olettaa, että laudan vaaka- ja pystyrivit +on numeroitu 0:sta alkaen. +Funktio asettaa kuningattaren vaakariville $y$, +kun $0 \le y < n$. +Jos taas $y=n$, yksi ratkaisu on valmis +ja funktio kasvattaa muuttujaa $c$. + +Taulukko \texttt{r1} pitää kirjaa, +millä laudan pystyriveillä on jo kuningatar. +Vastaavasti taulukot \texttt{r2} ja \texttt{r3} +pitävät kirjaa vinoriveistä. +Tällaisille riveille ei voi laittaa enää toista +kuningatarta. +Esimerkiksi $4 \times 4$ -laudan tapauksessa +rivit on numeroitu seuraavasti: + +\begin{center} +\begin{tikzpicture}[scale=.65] + \begin{scope} + \draw (0-6, 0) grid (4-6, 4); + \node at (-6+0.5,3.5) {$0$}; + \node at (-6+1.5,3.5) {$1$}; + \node at (-6+2.5,3.5) {$2$}; + \node at (-6+3.5,3.5) {$3$}; + \node at (-6+0.5,2.5) {$0$}; + \node at (-6+1.5,2.5) {$1$}; + \node at (-6+2.5,2.5) {$2$}; + \node at (-6+3.5,2.5) {$3$}; + \node at (-6+0.5,1.5) {$0$}; + \node at (-6+1.5,1.5) {$1$}; + \node at (-6+2.5,1.5) {$2$}; + \node at (-6+3.5,1.5) {$3$}; + \node at (-6+0.5,0.5) {$0$}; + \node at (-6+1.5,0.5) {$1$}; + \node at (-6+2.5,0.5) {$2$}; + \node at (-6+3.5,0.5) {$3$}; + + \draw (0, 0) grid (4, 4); + \node at (0.5,3.5) {$0$}; + \node at (1.5,3.5) {$1$}; + \node at (2.5,3.5) {$2$}; + \node at (3.5,3.5) {$3$}; + \node at (0.5,2.5) {$1$}; + \node at (1.5,2.5) {$2$}; + \node at (2.5,2.5) {$3$}; + \node at (3.5,2.5) {$4$}; + \node at (0.5,1.5) {$2$}; + \node at (1.5,1.5) {$3$}; + \node at (2.5,1.5) {$4$}; + \node at (3.5,1.5) {$5$}; + \node at (0.5,0.5) {$3$}; + \node at (1.5,0.5) {$4$}; + \node at (2.5,0.5) {$5$}; + \node at (3.5,0.5) {$6$}; + + \draw (6, 0) grid (10, 4); + \node at (6.5,3.5) {$3$}; + \node at (7.5,3.5) {$4$}; + \node at (8.5,3.5) {$5$}; + \node at (9.5,3.5) {$6$}; + \node at (6.5,2.5) {$2$}; + \node at (7.5,2.5) {$3$}; + \node at (8.5,2.5) {$4$}; + \node at (9.5,2.5) {$5$}; + \node at (6.5,1.5) {$1$}; + \node at (7.5,1.5) {$2$}; + \node at (8.5,1.5) {$3$}; + \node at (9.5,1.5) {$4$}; + \node at (6.5,0.5) {$0$}; + \node at (7.5,0.5) {$1$}; + \node at (8.5,0.5) {$2$}; + \node at (9.5,0.5) {$3$}; + + \node at (-4,-1) {\texttt{r1}}; + \node at (2,-1) {\texttt{r2}}; + \node at (8,-1) {\texttt{r3}}; + + \end{scope} +\end{tikzpicture} +\end{center} + +Koodin avulla selviää esimerkiksi, +että tapauksessa $n=8$ on 92 tapaa sijoittaa 8 +kuningatarta $8 \times 8$ -laudalle. +Kun $n$ kasvaa, koodi hidastuu nopeasti, +koska ratkaisujen määrä kasvaa räjähdysmäisesti. +Tapauksen $n=16$ laskeminen vie jo noin minuutin +nykyaikaisella tietokoneella (14772512 ratkaisua). + +\section{Haun optimointi} + +Peruuttavaa hakua on usein mahdollista tehostaa +erilaisten optimointien avulla. +Tavoitteena on lisätä hakuun ''älykkyyttä'' +niin, että haku pystyy havaitsemaan +mahdollisimman aikaisin, +jos muodosteilla oleva ratkaisu ei voi +johtaa kokonaiseen ratkaisuun. +Tällaiset optimoinnit karsivat haaroja +hakupuusta, millä voi olla suuri vaikutus +peruuttavan haun tehokkuuteen. + +Tarkastellaan esimerkkinä tehtävää, +jossa laskettavana on reittien määrä +$n \times n$ -ruudukon +vasemmasta yläkulmasta oikeaan alakulmaan, +kun reitin aikana tulee käydä tarkalleen kerran +jokaisessa ruudussa. +Esimerkiksi $7 \times 7$ -ruudukossa on +111712 mahdollista reittiä vasemmasta yläkulmasta +oikeaan alakulmaan, joista yksi on seuraava: + +\begin{center} +\begin{tikzpicture}[scale=.55] + \begin{scope} + \draw (0, 0) grid (7, 7); + \draw[thick,->] (0.5,6.5) -- (0.5,4.5) -- (2.5,4.5) -- + (2.5,3.5) -- (0.5,3.5) -- (0.5,0.5) -- + (3.5,0.5) -- (3.5,1.5) -- (1.5,1.5) -- + (1.5,2.5) -- (4.5,2.5) -- (4.5,0.5) -- + (5.5,0.5) -- (5.5,3.5) -- (3.5,3.5) -- + (3.5,5.5) -- (1.5,5.5) -- (1.5,6.5) -- + (4.5,6.5) -- (4.5,4.5) -- (5.5,4.5) -- + (5.5,6.5) -- (6.5,6.5) -- (6.5,0.5); + \end{scope} +\end{tikzpicture} +\end{center} + +Keskitymme seuraavaksi nimenomaan tapaukseen $7 \times 7$, +koska se on laskennallisesti sopivan haastava. +Lähdemme liikkeelle suoraviivaisesta peruuttavaa hakua +käyttävästä algoritmista +ja teemme siihen pikkuhiljaa optimointeja, +jotka nopeuttavat hakua eri tavoin. +Mittaamme jokaisen optimoinnin jälkeen +algoritmin suoritusajan sekä rekursiokutsujen yhteismäärän, +jotta näemme selvästi, mikä vaikutus kullakin +optimoinnilla on haun tehokkuuteen. + +\subsubsection{Perusalgoritmi} + +Algoritmin ensimmäisessä versiossa ei ole mitään optimointeja, +vaan peruuttava haku käy läpi kaikki mahdolliset tavat +muodostaa reitti ruudukon vasemmasta yläkulmasta +oikeaan alakulmaan. + +\begin{itemize} +\item +suoritusaika: 483 sekuntia +\item +rekursiokutsuja: 76 miljardia +\end{itemize} + +\subsubsection{Optimointi 1} + +Reitin ensimmäinen askel on joko alaspäin +tai oikealle. Tästä valinnasta seuraavat tilanteet +ovat symmetrisiä ruudukon lävistäjän suhteen. +Esimerkiksi seuraavat ratkaisut ovat +symmetrisiä keskenään: + +\begin{center} +\begin{tabular}{ccc} +\begin{tikzpicture}[scale=.55] + \begin{scope} + \draw (0, 0) grid (7, 7); + \draw[thick,->] (0.5,6.5) -- (0.5,4.5) -- (2.5,4.5) -- + (2.5,3.5) -- (0.5,3.5) -- (0.5,0.5) -- + (3.5,0.5) -- (3.5,1.5) -- (1.5,1.5) -- + (1.5,2.5) -- (4.5,2.5) -- (4.5,0.5) -- + (5.5,0.5) -- (5.5,3.5) -- (3.5,3.5) -- + (3.5,5.5) -- (1.5,5.5) -- (1.5,6.5) -- + (4.5,6.5) -- (4.5,4.5) -- (5.5,4.5) -- + (5.5,6.5) -- (6.5,6.5) -- (6.5,0.5); + \end{scope} +\end{tikzpicture} +& \hspace{20px} +& +\begin{tikzpicture}[scale=.55] + \begin{scope}[yscale=1,xscale=-1,rotate=-90] + \draw (0, 0) grid (7, 7); + \draw[thick,->] (0.5,6.5) -- (0.5,4.5) -- (2.5,4.5) -- + (2.5,3.5) -- (0.5,3.5) -- (0.5,0.5) -- + (3.5,0.5) -- (3.5,1.5) -- (1.5,1.5) -- + (1.5,2.5) -- (4.5,2.5) -- (4.5,0.5) -- + (5.5,0.5) -- (5.5,3.5) -- (3.5,3.5) -- + (3.5,5.5) -- (1.5,5.5) -- (1.5,6.5) -- + (4.5,6.5) -- (4.5,4.5) -- (5.5,4.5) -- + (5.5,6.5) -- (6.5,6.5) -- (6.5,0.5); + \end{scope} +\end{tikzpicture} +\end{tabular} +\end{center} + +Tämän ansiosta voimme tehdä päätöksen, +että reitin ensimmäinen askel on alaspäin, +ja kertoa lopuksi reittien määrän 2:lla. + +\begin{itemize} +\item +suoritusaika: 244 sekuntia +\item +rekursiokutsuja: 38 miljardia +\end{itemize} + +\subsubsection{Optimointi 2} + +Jos reitti menee oikean alakulman ruutuun ennen kuin +se on käynyt kaikissa muissa ruuduissa, +siitä ei voi mitenkään enää saada kelvollista ratkaisua. +Näin on esimerkiksi seuraavassa tilanteessa: + +\begin{center} +\begin{tikzpicture}[scale=.55] + \begin{scope} + \draw (0, 0) grid (7, 7); + \draw[thick,->] (0.5,6.5) -- (0.5,4.5) -- (2.5,4.5) -- + (2.5,3.5) -- (0.5,3.5) -- (0.5,0.5) -- + (3.5,0.5) -- (3.5,1.5) -- (1.5,1.5) -- + (1.5,2.5) -- (4.5,2.5) -- (4.5,0.5) -- + (6.5,0.5); + \end{scope} +\end{tikzpicture} +\end{center} +Niinpä voimme keskeyttää hakuhaaran heti, +jos tulemme oikean alakulman ruutuun liian aikaisin. +\begin{itemize} +\item +suoritusaika: 119 sekuntia +\item +rekursiokutsuja: 20 miljardia +\end{itemize} + +\subsubsection{Optimointi 3} + +Jos reitti osuu seinään niin, että kummallakin puolella +on ruutu, jossa reitti ei ole vielä käynyt, +ruudukko jakautuu kahteen osaan. +Esimerkiksi seuraavassa tilanteessa +sekä vasemmalla että +oikealla puolella on tyhjä ruutu: + +\begin{center} +\begin{tikzpicture}[scale=.55] + \begin{scope} + \draw (0, 0) grid (7, 7); + \draw[thick,->] (0.5,6.5) -- (0.5,4.5) -- (2.5,4.5) -- + (2.5,3.5) -- (0.5,3.5) -- (0.5,0.5) -- + (3.5,0.5) -- (3.5,1.5) -- (1.5,1.5) -- + (1.5,2.5) -- (4.5,2.5) -- (4.5,0.5) -- + (5.5,0.5) -- (5.5,6.5); + \end{scope} +\end{tikzpicture} +\end{center} +Nyt ei ole enää mahdollista käydä kaikissa ruuduissa, +joten voimme keskeyttää hakuhaaran. +Tämä optimointi on hyvin hyödyllinen: + +\begin{itemize} +\item +suoritusaika: 1{,}8 sekuntia +\item +rekursiokutsuja: 221 miljoonaa +\end{itemize} + +\subsubsection{Optimointi 4} + +Äskeisen optimoinnin ideaa voi yleistää: +ruudukko jakaantuu kahteen osaan, +jos nykyisen ruudun ylä- ja alapuolella on +tyhjä ruutu sekä vasemmalla ja oikealla +puolella on seinä tai aiemmin käyty ruutu +(tai päinvastoin). + +Esimerkiksi seuraavassa tilanteessa +nykyisen ruudun ylä- ja alapuolella on +tyhjä ruutu eikä reitti voi enää edetä +molempiin ruutuihin: +\begin{center} +\begin{tikzpicture}[scale=.55] + \begin{scope} + \draw (0, 0) grid (7, 7); + \draw[thick,->] (0.5,6.5) -- (0.5,4.5) -- (2.5,4.5) -- + (2.5,3.5) -- (0.5,3.5) -- (0.5,0.5) -- + (3.5,0.5) -- (3.5,1.5) -- (1.5,1.5) -- + (1.5,2.5) -- (4.5,2.5) -- (4.5,0.5) -- + (5.5,0.5) -- (5.5,4.5) -- (3.5,4.5); + \end{scope} +\end{tikzpicture} +\end{center} +Haku tehostuu entisestään, kun keskeytämme +hakuhaaran kaikissa tällaisissa tapauksissa: + +\begin{itemize} +\item +suoritusaika: 0{,}6 sekuntia +\item +rekursiokutsuja: 69 miljoonaa +\end{itemize} + +~\\ +Nyt on hyvä hetki lopettaa optimointi ja muistella, +mistä lähdimme liikkeelle. +Alkuperäinen algoritmi vei aikaa 483 sekuntia, +ja nyt optimointien jälkeen algoritmi vie aikaa +vain 0{,}6 sekuntia. +Optimointien ansiosta algoritmi nopeutui +siis lähes 1000-kertaisesti. + +Tämä on yleinen ilmiö peruuttavassa haussa, +koska hakupuu on yleensä valtava ja +yksinkertainenkin optimointi voi karsia suuren +määrän haaroja hakupuusta. +Erityisen hyödyllisiä ovat optimoinnit, +jotka kohdistuvat hakupuun yläosaan, +koska ne karsivat eniten haaroja. + +\section{Puolivälihaku} + +\index{puolivxlihaku@puolivälihaku} + +\key{Puolivälihaku} (''meet in the middle'') on tekniikka, +jossa hakutehtävä jaetaan kahteen yhtä suureen osaan. +Kumpaankin osaan tehdään erillinen haku, +ja lopuksi hakujen tulokset yhdistetään. + +Puolivälihaun käyttäminen edellyttää, +että erillisten hakujen tulokset pystyy +yhdistämään tehokkaasti. +Tällöin puolivälihaku on tehokkaampi +kuin yksi haku, joka käy läpi koko hakualueen. +Tyypillisesti puolivälihaku tehostaa algoritmia +niin, että aikavaativuuden kertoimesta $2^n$ +tulee kerroin $2^{n/2}$. + +Tarkastellaan ongelmaa, jossa annettuna +on $n$ lukua sisältävä lista sekä kokonaisluku $x$. +Tehtävänä on selvittää, voiko listalta valita +joukon lukuja niin, että niiden summa on $x$. +Esimerkiksi jos lista on $[2,4,5,9]$ ja $x=15$, +voimme valita listalta luvut $[2,4,9]$, +jolloin $2+4+9=15$. +Jos taas lista säilyy ennallaan ja $x=10$, +mikään valinta ei täytä vaatimusta. + +Tavanomainen ratkaisu tehtävään on käydä kaikki +listan alkioiden osajoukot läpi ja tarkastaa, +onko jonkin osajoukon summa $x$. +Tällainen ratkaisu kuluttaa aikaa $O(2^n)$, +koska erilaisia osajoukkoja on $2^n$. +Seuraavaksi näemme, +miten puolivälihaun avulla on mahdollista luoda +tehokkaampi $O(2^{n/2})$-aikainen ratkaisu. +Huomaa, että aikavaativuuksissa $O(2^n)$ ja +$O(2^{n/2})$ on merkittävä ero, koska +$2^{n/2}$ tarkoittaa samaa kuin $\sqrt{2^n}$. + +Ideana on jakaa syötteenä oleva lista +kahteen listaan $A$ ja $B$, +joista kumpikin sisältää noin puolet luvuista. +Ensimmäinen haku muodostaa kaikki osajoukot +listan $A$ luvuista ja laittaa muistiin niiden summat +listaan $S_A$. +Toinen haku muodostaa vastaavasti listan +$B$ perusteella listan $S_B$. +Tämän jälkeen riittää tarkastaa, +onko mahdollista valita yksi luku listasta $S_A$ +ja toinen luku listasta $S_B$ niin, +että lukujen summa on $x$. +Tämä on mahdollista tarkalleen silloin, +kun alkuperäisen listan luvuista saa summan $x$. + +Tarkastellaan esimerkkiä, +jossa lista on $[2,4,5,9]$ +ja $x=15$. +Puolivälihaku jakaa luvut kahteen +listaan niin, että $A=[2,4]$ +ja $B=[5,9]$. +Näistä saadaan edelleen summalistat +$S_A=[0,2,4,6]$ ja $S_B=[0,5,9,14]$. +Summa $x=15$ on mahdollista muodostaa, +koska voidaan valita $S_A$:sta luku $6$ +ja $S_B$:stä luku $9$. +Tämä valinta vastaa ratkaisua $[2,4,9]$. + +Ratkaisun aikavaativuus on $O(2^{n/2})$, +koska kummassakin listassa $A$ ja $B$ +on $n/2$ lukua ja niiden osajoukkojen +summien laskeminen listoihin $S_A$ ja $S_B$ +vie aikaa $O(2^{n/2})$. +Tämän jälkeen on mahdollista tarkastaa +ajassa $O(2^{n/2})$, voiko summaa $x$ muodostaa +listojen $S_A$ ja $S_B$ luvuista. \ No newline at end of file diff --git a/luku06.tex b/luku06.tex new file mode 100644 index 0000000..74ce2b4 --- /dev/null +++ b/luku06.tex @@ -0,0 +1,789 @@ +\chapter{Greedy algorithms} + +\index{ahne algoritmi@ahne algoritmi} + +\key{Ahne algoritmi} +muodostaa tehtävän ratkaisun +tekemällä joka askeleella +sillä hetkellä parhaalta näyttävän valinnan. +Ahne algoritmi ei koskaan +peruuta tekemiään valintoja vaan +muodostaa ratkaisun suoraan valmiiksi. +Tämän ansiosta ahneet algoritmit ovat +yleensä hyvin tehokkaita. + +Vaikeutena ahneissa algoritmeissa on +keksiä toimiva ahne strategia, +joka tuottaa aina optimaalisen ratkaisun tehtävään. +Ahneen algoritmin tulee olla sellainen, +että kulloinkin parhaalta näyttävät valinnat +tuottavat myös parhaan kokonaisuuden. +Tämän perusteleminen on usein hankalaa. + +\section{Kolikkotehtävä} + +Aloitamme ahneisiin algoritmeihin tutustumisen +tehtävästä, jossa muodostettavana on +rahamäärä $x$ kolikoista. +Kolikoiden arvot ovat $\{c_1,c_2,\ldots,c_k\}$, +ja jokaista kolikkoa on saatavilla rajattomasti. +Tehtävänä on selvittää, mikä on pienin määrä +kolikoita, joilla rahamäärän voi muodostaa. + +Esimerkiksi jos muodostettava +rahamäärä on 520 +ja kolikot ovat eurokolikot eli sentteinä +\[\{1,2,5,10,20,50,100,200\},\] +niin kolikoita tarvitaan vähintään 4. +Tämä on mahdollista valitsemalla kolikot +$200+200+100+20$, joiden summa on 520. + +\subsubsection{Ahne algoritmi} + +Luonteva ahne algoritmi tehtävään +on poistaa rahamäärästä aina mahdollisimman +suuri kolikko, kunnes rahamäärä on 0. +Tämä algoritmi toimii esimerkissä, +koska rahamäärästä 520 +poistetaan ensin kahdesti 200, sitten 100 +ja lopuksi 20. +Mutta toimiiko ahne algoritmi aina oikein? + +Osoittautuu, että eurokolikoiden tapauksessa +ahne algoritmi \emph{toimii} aina oikein, +eli se tuottaa aina ratkaisun, +jossa on pienin määrä kolikoita. +Algoritmin toimivuuden voi perustella +seuraavasti: + +Kutakin kolikkoa 1, 5, 10, 50 ja 100 +on optimiratkaisussa enintään yksi. +Tämä johtuu siitä, että jos +ratkaisussa olisi kaksi tällaista kolikkoa, +saman ratkaisun voisi muodostaa +käyttäen vähemmän kolikoita. +Esimerkiksi jos ratkaisussa on +kolikot $5+5$, ne voi korvata kolikolla 10. + +Vastaavasti kumpaakin kolikkoa 2 ja 20 +on optimiratkaisussa enintään kaksi, +koska kolikot $2+2+2$ voi korvata kolikoilla $5+1$ +ja kolikot $20+20+20$ voi korvata kolikoilla $50+10$. +Lisäksi ratkaisussa ei voi olla yhdistelmiä +$2+2+1$ ja $20+20+10$, +koska ne voi korvata kolikoilla 5 ja 50. + +Näiden havaintojen perusteella +jokaiselle kolikolle $x$ pätee, +että $x$:ää pienemmistä kolikoista +ei ole mahdollista saada aikaan summaa +$x$ tai suurempaa summaa optimaalisesti. +Esimerkiksi jos $x=100$, pienemmistä kolikoista +saa korkeintaan summan $50+20+20+5+2+2=99$. +Niinpä ahne algoritmi, +joka valitsee aina suurimman kolikon, +tuottaa optimiratkaisun. + +Kuten tästä esimerkistä huomaa, +ahneen algoritmin toimivuuden perusteleminen +voi olla työlästä, +vaikka kyseessä olisi yksinkertainen algoritmi. + +\subsubsection{Yleinen tapaus} + +Yleisessä tapauksessa kolikot voivat olla mitä tahansa. +Tällöin suurimman kolikon valitseva ahne algoritmi +\emph{ei} välttämättä tuota optimiratkaisua. + +Jos ahne algoritmi ei toimi, tämän voi osoittaa +näyttämällä vastaesimerkin, jossa algoritmi +antaa väärän vastauksen. +Tässä tehtävässä vastaesimerkki on helppoa keksiä: +jos kolikot ovat $\{1,3,4\}$ ja muodostettava +rahamäärä on 6, ahne algoritmi tuottaa ratkaisun +$4+1+1$, kun taas optimiratkaisu on $3+3$. + +Yleisessä tapauksessa kolikkotehtävän ratkaisuun +ei tunneta ahnetta algoritmia, +mutta palaamme tehtävään seuraavassa luvussa. +Tehtävään on nimittäin olemassa dynaamista +ohjelmointia käyttävä algoritmi, +joka tuottaa optimiratkaisun +millä tahansa kolikoilla ja rahamäärällä. + +\section{Aikataulutus} + +Monet aikataulutukseen liittyvät ongelmat +ratkeavat ahneesti. +Klassinen ongelma on seuraava: +Annettuna on $n$ tapahtumaa, +jotka alkavat ja päättyvät tiettyinä hetkinä. +Tehtäväsi on suunnitella aikataulu, +jota seuraamalla pystyt osallistumaan +mahdollisimman moneen tapahtumaan. +Et voi osallistua tapahtumaan vain osittain. +Esimerkiksi tilanteessa +\begin{center} +\begin{tabular}{lll} +tapahtuma & alkuaika & loppuaika \\ +\hline +$A$ & 1 & 3 \\ +$B$ & 2 & 5 \\ +$C$ & 3 & 9 \\ +$D$ & 6 & 8 \\ +\end{tabular} +\end{center} +on mahdollista osallistua korkeintaan +kahteen tapahtumaan. +Yksi mahdollisuus on osallistua tapahtumiin +$B$ ja $D$ seuraavasti: +\begin{center} +\begin{tikzpicture}[scale=.4] + \begin{scope} + \draw (2, 0) rectangle (6, -1); + \draw[fill=lightgray] (4, -1.5) rectangle (10, -2.5); + \draw (6, -3) rectangle (18, -4); + \draw[fill=lightgray] (12, -4.5) rectangle (16, -5.5); + \node at (2.5,-0.5) {$A$}; + \node at (4.5,-2) {$B$}; + \node at (6.5,-3.5) {$C$}; + \node at (12.5,-5) {$D$}; + \end{scope} +\end{tikzpicture} +\end{center} + +Tehtävän ratkaisuun on mahdollista +keksiä useita ahneita algoritmeja, +mutta mikä niistä toimii kaikissa tapauksissa? + +\subsubsection*{Algoritmi 1} + +Ensimmäinen idea on valita ratkaisuun +mahdollisimman \emph{lyhyitä} tapahtumia. +Esimerkin tapauksessa tällainen +algoritmi valitsee seuraavat tapahtumat: +\begin{center} +\begin{tikzpicture}[scale=.4] + \begin{scope} + \draw[fill=lightgray] (2, 0) rectangle (6, -1); + \draw (4, -1.5) rectangle (10, -2.5); + \draw (6, -3) rectangle (18, -4); + \draw[fill=lightgray] (12, -4.5) rectangle (16, -5.5); + \node at (2.5,-0.5) {$A$}; + \node at (4.5,-2) {$B$}; + \node at (6.5,-3.5) {$C$}; + \node at (12.5,-5) {$D$}; + \end{scope} +\end{tikzpicture} +\end{center} + +Lyhimpien tapahtumien valinta ei ole kuitenkaan +aina toimiva strategia, +vaan algoritmi epäonnistuu esimerkiksi seuraavassa tilanteessa: +\begin{center} +\begin{tikzpicture}[scale=.4] + \begin{scope} + \draw (1, 0) rectangle (7, -1); + \draw[fill=lightgray] (6, -1.5) rectangle (9, -2.5); + \draw (8, -3) rectangle (14, -4); + \end{scope} +\end{tikzpicture} +\end{center} +Kun lyhyt tapahtuma valitaan mukaan, +on mahdollista osallistua vain yhteen tapahtumaan. +Kuitenkin valitsemalla pitkät tapahtumat +olisi mahdollista osallistua kahteen tapahtumaan. + +\subsubsection*{Algoritmi 2} + +Toinen idea on valita aina seuraavaksi tapahtuma, +joka \emph{alkaa} mahdollisimman aikaisin. +Tämä algoritmi valitsee esimerkissä seuraavat tapahtumat: +\begin{center} +\begin{tikzpicture}[scale=.4] + \begin{scope} + \draw[fill=lightgray] (2, 0) rectangle (6, -1); + \draw (4, -1.5) rectangle (10, -2.5); + \draw[fill=lightgray] (6, -3) rectangle (18, -4); + \draw (12, -4.5) rectangle (16, -5.5); + \node at (2.5,-0.5) {$A$}; + \node at (4.5,-2) {$B$}; + \node at (6.5,-3.5) {$C$}; + \node at (12.5,-5) {$D$}; + \end{scope} +\end{tikzpicture} +\end{center} + +Tämäkään algoritmi ei kuitenkaan toimi aina. +Esimerkiksi seuraavassa tilanteessa +algoritmi valitsee vain yhden tapahtuman: +\begin{center} +\begin{tikzpicture}[scale=.4] + \begin{scope} + \draw[fill=lightgray] (1, 0) rectangle (14, -1); + \draw (3, -1.5) rectangle (7, -2.5); + \draw (8, -3) rectangle (12, -4); + \end{scope} +\end{tikzpicture} +\end{center} +Kun ensimmäisenä alkava tapahtuma +valitaan mukaan, mitään muuta tapahtumaa +ei ole mahdollista valita. +Kuitenkin olisi mahdollista osallistua +kahteen tapahtumaan valitsemalla +kaksi myöhempää tapahtumaa. + +\subsubsection*{Algoritmi 3} + +Kolmas idea on valita aina seuraavaksi tapahtuma, +joka \emph{päättyy} mahdollisimman aikaisin. +Tämä algoritmi valitsee esimerkissä seuraavat tapahtumat: +\begin{center} +\begin{tikzpicture}[scale=.4] + \begin{scope} + \draw[fill=lightgray] (2, 0) rectangle (6, -1); + \draw (4, -1.5) rectangle (10, -2.5); + \draw (6, -3) rectangle (18, -4); + \draw[fill=lightgray] (12, -4.5) rectangle (16, -5.5); + \node at (2.5,-0.5) {$A$}; + \node at (4.5,-2) {$B$}; + \node at (6.5,-3.5) {$C$}; + \node at (12.5,-5) {$D$}; + \end{scope} +\end{tikzpicture} +\end{center} + +Osoittautuu, että tämä ahne algoritmi +tuottaa \textit{aina} optimiratkaisun. +Algoritmi toimii, koska on aina kokonaisuuden +kannalta optimaalista valita +ensimmäiseksi tapahtumaksi +mahdollisimman aikaisin päättyvä tapahtuma. +Tämän jälkeen on taas optimaalista +valita seuraava aikatauluun sopiva +mahdollisimman aikaisin +päättyvä tapahtua, jne. + +Yksi tapa perustella valintaa on miettiä, +mitä tapahtuu, jos ensimmäiseksi tapahtumaksi +valitaan jokin muu kuin mahdollisimman +aikaisin päättyvä tapahtuma. +Tällainen valinta ei ole koskaan parempi, +koska myöhemmin päättyvän tapahtuman +jälkeen on joko yhtä paljon tai vähemmän +mahdollisuuksia valita seuraavia tapahtumia +kuin aiemmin päättyvän tapahtuman jälkeen. + +\section{Tehtävät ja deadlinet} + +Annettuna on $n$ tehtävää, +joista jokaisella on kesto ja deadline. +Tehtäväsi on valita järjestys, +jossa suoritat tehtävät. +Saat kustakin tehtävästä $d-x$ pistettä, +missä $d$ on tehtävän deadline ja $x$ +on tehtävän valmistumishetki. +Mikä on suurin mahdollinen +yhteispistemäärä, jonka voit saada tehtävistä? + +Esimerkiksi jos tehtävät ovat +\begin{center} +\begin{tabular}{lll} +tehtävä & kesto & deadline \\ +\hline +$A$ & 4 & 2 \\ +$B$ & 3 & 5 \\ +$C$ & 2 & 7 \\ +$D$ & 4 & 5 \\ +\end{tabular} +\end{center} +niin optimaalinen ratkaisu on suorittaa +tehtävät seuraavasti: +\begin{center} +\begin{tikzpicture}[scale=.4] + \begin{scope} + \draw (0, 0) rectangle (4, -1); + \draw (4, 0) rectangle (10, -1); + \draw (10, 0) rectangle (18, -1); + \draw (18, 0) rectangle (26, -1); + \node at (0.5,-0.5) {$C$}; + \node at (4.5,-0.5) {$B$}; + \node at (10.5,-0.5) {$A$}; + \node at (18.5,-0.5) {$D$}; + + \draw (0,1.5) -- (26,1.5); + \foreach \i in {0,2,...,26} + { + \draw (\i,1.25) -- (\i,1.75); + } + \footnotesize + \node at (0,2.5) {0}; + \node at (10,2.5) {5}; + \node at (20,2.5) {10}; + + \end{scope} +\end{tikzpicture} +\end{center} +Tässä ratkaisussa $C$ tuottaa 5 pistettä, +$B$ tuottaa 0 pistettä, $A$ tuottaa $-7$ pistettä +ja $D$ tuottaa $-8$ pistettä, +joten yhteispistemäärä on $-10$. + +Yllättävää kyllä, tehtävän optimaalinen ratkaisu +ei riipu lainkaan deadlineista, +vaan toimiva ahne strategia on +yksinkertaisesti +suorittaa tehtävät \emph{järjestyksessä keston mukaan} +lyhimmästä pisimpään. +Syynä tähän on, että jos missä tahansa vaiheessa +suoritetaan peräkkäin kaksi tehtävää, +joista ensimmäinen kestää toista kauemmin, +tehtävien järjestyksen vaihtaminen parantaa ratkaisua. +Esimerkiksi jos peräkkäin ovat tehtävät +\begin{center} +\begin{tikzpicture}[scale=.4] + \begin{scope} + \draw (0, 0) rectangle (8, -1); + \draw (8, 0) rectangle (12, -1); + \node at (0.5,-0.5) {$X$}; + \node at (8.5,-0.5) {$Y$}; + +\draw [decoration={brace}, decorate, line width=0.3mm] (7.75,-1.5) -- (0.25,-1.5); +\draw [decoration={brace}, decorate, line width=0.3mm] (11.75,-1.5) -- (8.25,-1.5); + +\footnotesize +\node at (4,-2.5) {$a$}; +\node at (10,-2.5) {$b$}; + + \end{scope} +\end{tikzpicture} +\end{center} +ja $a>b$, niin järjestyksen muuttaminen muotoon +\begin{center} +\begin{tikzpicture}[scale=.4] + \begin{scope} + \draw (0, 0) rectangle (4, -1); + \draw (4, 0) rectangle (12, -1); + \node at (0.5,-0.5) {$Y$}; + \node at (4.5,-0.5) {$X$}; + +\draw [decoration={brace}, decorate, line width=0.3mm] (3.75,-1.5) -- (0.25,-1.5); +\draw [decoration={brace}, decorate, line width=0.3mm] (11.75,-1.5) -- (4.25,-1.5); + +\footnotesize +\node at (2,-2.5) {$b$}; +\node at (8,-2.5) {$a$}; + + \end{scope} +\end{tikzpicture} +\end{center} +antaa $X$:lle $b$ pistettä vähemmän ja $Y$:lle $a$ pistettä enemmän, +joten kokonaismuutos pistemäärään on $a-b > 0$. +Optimiratkaisussa +kaikille peräkkäin suoritettaville tehtäville +tulee päteä, että lyhyempi tulee ennen pidempää, +mistä seuraa, että tehtävät tulee suorittaa +järjestyksessä keston mukaan. + +\section{Keskiluvut} + +Tarkastelemme seuraavaksi ongelmaa, jossa +annettuna on $n$ lukua $a_1,a_2,\ldots,a_n$ +ja tehtävänä on etsiä luku $x$ niin, että summa +\[|a_1-x|^c+|a_2-x|^c+\cdots+|a_n-x|^c\] +on mahdollisimman pieni. +Keskitymme tapauksiin, joissa $c=1$ tai $c=2$. + +\subsubsection{Tapaus $c=1$} + +Tässä tapauksessa minimoitavana on summa +\[|a_1-x|+|a_2-x|+\cdots+|a_n-x|.\] +Esimerkiksi jos luvut ovat $[1,2,9,2,6]$, +niin paras ratkaisu on valita $x=2$, +jolloin summaksi tulee +\[ +|1-2|+|2-2|+|9-2|+|2-2|+|6-2|=12. +\] +Yleisessä tapauksessa paras valinta $x$:n arvoksi +on lukujen \textit{mediaani} +eli keskimmäinen luku järjestyksessä. +Esimerkiksi luvut $[1,2,9,2,6]$ +ovat järjestyksessä $[1,2,2,6,9]$, +joten mediaani on 2. + +Mediaanin valinta on paras ratkaisu, +koska jos $x$ on mediaania pienempi, +$x$:n suurentaminen pienentää summaa, +ja vastaavasti jos $x$ on mediaania suurempi, +$x$:n pienentäminen pienentää summaa. +Niinpä $x$ kannattaa siirtää mahdollisimman +lähelle mediaania eli optimiratkaisu on +valita $x$ mediaaniksi. +Jos $n$ on parillinen ja mediaaneja on kaksi, +kumpikin mediaani sekä kaikki niiden välillä +olevat luvut tuottavat optimaalisen ratkaisun. + +\subsubsection{Tapaus $c=2$} + +Tässä tapauksessa minimoitavana on summa +\[(a_1-x)^2+(a_2-x)^2+\cdots+(a_n-x)^2.\] +Esimerkiksi jos luvut ovat $[1,2,9,2,6]$, +niin paras ratkaisu on $x=4$, +jolloin summaksi tulee +\[ +(1-4)^2+(2-4)^2+(9-4)^2+(2-4)^2+(6-4)^2=46. +\] +Yleisessä tapauksessa paras valinta $x$:n arvoksi on lukujen +\textit{keskiarvo}. +Esimerkissä lukujen keskiarvo on $(1+2+9+2+6)/5=4$. +Tämän tuloksen voi johtaa järjestämällä summan +uudestaan muotoon +\[ +nx^2 - 2x(a_1+a_2+\cdots+a_n) + (a_1^2+a_2^2+\cdots+a_n^2). +\] +Viimeinen osa ei riipu $x$:stä, joten sen voi jättää huomiotta. +Jäljelle jäävistä osista muodostuu funktio +$nx^2-2xs$, kun $s=a_1+a_2+\cdots+a_n$. +Tämä on ylöspäin aukeava paraabeli, +jonka nollakohdat ovat $x=0$ ja $x=2s/n$ +ja pienin arvo on näiden keskikohta +$x=s/n$ eli taulukon lukujen keskiarvo. + +\section{Tiedonpakkaus} + +\index{tiedonpakkaus} +\index{binxxrikoodi@binäärikoodi} +\index{koodisana@koodisana} + +Annettuna on merkkijono ja tehtävänä on +\emph{pakata} se niin, +että tilaa kuluu vähemmän. +Käytämme tähän \key{binäärikoodia}, +joka määrittää kullekin merkille +biteistä muodostuvan \key{koodisanan}. +Tällöin merkkijonon voi pakata +korvaamalla jokaisen merkin vastaavalla koodisanalla. +Esimerkiksi seuraava binäärikoodi määrittää +koodisanat merkeille \texttt{A}–\texttt{D}: +\begin{center} +\begin{tabular}{rr} +merkki & koodisana \\ +\hline +\texttt{A} & 00 \\ +\texttt{B} & 01 \\ +\texttt{C} & 10 \\ +\texttt{D} & 11 \\ +\end{tabular} +\end{center} +Tämä koodi on \key{vakiopituinen} +eli jokainen koodisana on yhtä pitkä. +Esimerkiksi merkkijono +\texttt{AABACDACA} on pakattuna +\[000001001011001000,\] +eli se vie tilaa 18 bittiä. +Pakkausta on kuitenkin mahdollista parantaa +ottamalla käyttöön \key{muuttuvan pituinen} koodi, +jossa koodisanojen pituus voi vaihdella. +Tällöin voimme antaa usein esiintyville merkeille +lyhyen koodisanan ja harvoin esiintyville +merkeille pitkän koodisanan. +Osoittautuu, että yllä olevalle merkkijonolle +\key{optimaalinen} koodi on seuraava: +\begin{center} +\begin{tabular}{rr} +merkki & koodisana \\ +\hline +\texttt{A} & 0 \\ +\texttt{B} & 110 \\ +\texttt{C} & 10 \\ +\texttt{D} & 111 \\ +\end{tabular} +\end{center} +Optimaalinen koodi tuottaa +mahdollisimman lyhyen pakatun merkkijonon. +Tässä tapauksessa optimaalinen koodi +pakkaa merkkijonon muotoon +\[001100101110100,\] +ja tilaa kuluu vain 15 bittiä. +Paremman koodin ansiosta onnistuimme siis säästämään +3 bittiä tilaa pakkauksessa. + +Huomaa, että koodin tulee olla aina sellainen, +että mikään koodisana ei ole toisen koodisanan +alkuosa. +Esimerkiksi ei ole sallittua, että koodissa +olisi molemmat koodisanat 10 ja 1011. +Tämä rajoitus johtuu siitä, +että haluamme myös pystyä palauttamaan +alkuperäisen merkkijonon pakkauksen jälkeen. +Jos koodisana voisi olla toisen alkuosa, +tämä ei välttämättä olisi mahdollista. +Esimerkiksi seuraava koodi +\emph{ei} ole kelvollinen: +\begin{center} +\begin{tabular}{rr} +merkki & koodisana \\ +\hline +\texttt{A} & 10 \\ +\texttt{B} & 11 \\ +\texttt{C} & 1011 \\ +\texttt{D} & 111 \\ +\end{tabular} +\end{center} +Tätä koodia käyttäen ei olisi mahdollista tietää, +tarkoittaako pakattu merkkijono 1011 +merkkijonoa \texttt{AB} vai merkkijonoa \texttt{C}. + +\index{Huffmanin koodaus} + +\subsubsection{Huffmanin koodaus} + +\key{Huffmanin koodaus} on ahne algoritmi, +joka muodostaa optimaalisen koodin +merkkijonon pakkaamista varten. +Se muodostaa merkkien esiintymiskertojen +perustella binääripuun, josta voi lukea +kunkin merkin koodisanan +liikkumalla huipulta merkkiä vastaavaan solmuun. +Liikkuminen vasemmalle vastaa +bittiä 0 ja liikkuminen oikealle +vastaa bittiä 1. + +Aluksi jokaista merkkijonon merkkiä vastaa solmu, +jonka painona on merkin esiintymiskertojen määrä merkkijonossa. +Sitten joka vaiheessa puusta valitaan +kaksi painoltaan pienintä solmua +ja ne yhdistetään luomalla niiden +yläpuolelle uusi solmu, +jonka paino on solmujen yhteispaino. +Näin jatketaan, kunnes kaikki solmut +on yhdistetty ja koodi on valmis. + +Tarkastellaan nyt, miten Huffmanin koodaus +muodostaa optimaalisen koodin merkkijonolle +\texttt{AABACDACA}. +Alkutilanteessa on neljä solmua, +jotka vastaavat merkkijonossa olevia merkkejä: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {$5$}; +\node[draw, circle] (2) at (2,0) {$1$}; +\node[draw, circle] (3) at (4,0) {$2$}; +\node[draw, circle] (4) at (6,0) {$1$}; + +\node[color=blue] at (0,-0.75) {\texttt{A}}; +\node[color=blue] at (2,-0.75) {\texttt{B}}; +\node[color=blue] at (4,-0.75) {\texttt{C}}; +\node[color=blue] at (6,-0.75) {\texttt{D}}; + +%\path[draw,thick,-] (4) -- (5); +\end{tikzpicture} +\end{center} +Merkkiä \texttt{A} vastaavan solmun paino on +5, koska merkki \texttt{A} esiintyy 5 kertaa merkkijonossa. +Muiden solmujen painot on laskettu vastaavalla tavalla. + +Ensimmäinen askel on yhdistää merkkejä \texttt{B} ja \texttt{D} +vastaavat solmut, joiden kummankin paino on 1. +Tuloksena on: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {$5$}; +\node[draw, circle] (3) at (2,0) {$2$}; +\node[draw, circle] (2) at (4,0) {$1$}; +\node[draw, circle] (4) at (6,0) {$1$}; +\node[draw, circle] (5) at (5,1) {$2$}; + +\node[color=blue] at (0,-0.75) {\texttt{A}}; +\node[color=blue] at (2,-0.75) {\texttt{C}}; +\node[color=blue] at (4,-0.75) {\texttt{B}}; +\node[color=blue] at (6,-0.75) {\texttt{D}}; + +\node at (4.3,0.7) {0}; +\node at (5.7,0.7) {1}; + +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (4) -- (5); +\end{tikzpicture} +\end{center} +Tämän jälkeen yhdistetään solmut, joiden paino on 2: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,0) {$5$}; +\node[draw, circle] (3) at (3,1) {$2$}; +\node[draw, circle] (2) at (4,0) {$1$}; +\node[draw, circle] (4) at (6,0) {$1$}; +\node[draw, circle] (5) at (5,1) {$2$}; +\node[draw, circle] (6) at (4,2) {$4$}; + +\node[color=blue] at (1,-0.75) {\texttt{A}}; +\node[color=blue] at (3,1-0.75) {\texttt{C}}; +\node[color=blue] at (4,-0.75) {\texttt{B}}; +\node[color=blue] at (6,-0.75) {\texttt{D}}; + +\node at (4.3,0.7) {0}; +\node at (5.7,0.7) {1}; +\node at (3.3,1.7) {0}; +\node at (4.7,1.7) {1}; + +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (4) -- (5); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (5) -- (6); +\end{tikzpicture} +\end{center} +Lopuksi yhdistetään kaksi viimeistä solmua: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (2,2) {$5$}; +\node[draw, circle] (3) at (3,1) {$2$}; +\node[draw, circle] (2) at (4,0) {$1$}; +\node[draw, circle] (4) at (6,0) {$1$}; +\node[draw, circle] (5) at (5,1) {$2$}; +\node[draw, circle] (6) at (4,2) {$4$}; +\node[draw, circle] (7) at (3,3) {$9$}; + +\node[color=blue] at (2,2-0.75) {\texttt{A}}; +\node[color=blue] at (3,1-0.75) {\texttt{C}}; +\node[color=blue] at (4,-0.75) {\texttt{B}}; +\node[color=blue] at (6,-0.75) {\texttt{D}}; + +\node at (4.3,0.7) {0}; +\node at (5.7,0.7) {1}; +\node at (3.3,1.7) {0}; +\node at (4.7,1.7) {1}; +\node at (2.3,2.7) {0}; +\node at (3.7,2.7) {1}; + +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (4) -- (5); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (5) -- (6); +\path[draw,thick,-] (1) -- (7); +\path[draw,thick,-] (6) -- (7); +\end{tikzpicture} +\end{center} + +Nyt kaikki solmut ovat puussa, joten koodi on valmis. +Puusta voidaan lukea seuraavat koodisanat: +\begin{center} +\begin{tabular}{rr} +merkki & koodisana \\ +\hline +\texttt{A} & 0 \\ +\texttt{B} & 110 \\ +\texttt{C} & 10 \\ +\texttt{D} & 111 \\ +\end{tabular} +\end{center} + +% \subsubsection{Miksi algoritmi toimii?} +% +% Huffmanin koodaus on ahne algoritmi, koska se +% yhdistää aina kaksi solmua, joiden painot ovat +% pienimmät. +% Miksi on varmaa, että tämä menetelmä tuottaa +% aina optimaalisen koodin? +% +% Merkitään $c(x)$ merkin $x$ esiintymiskertojen +% määrää merkkijonossa sekä $s(x)$ +% merkkiä $x$ vastaavan koodisanan pituutta. +% Näitä merkintöjä käyttäen merkkijonon +% bittiesityksen pituus on +% \[\sum_x c(x) \cdot s(x),\] +% missä summa käy läpi kaikki merkkijonon merkit. +% Esimerkiksi äskeisessä esimerkissä +% bittiesityksen pituus on +% \[5 \cdot 1 + 1 \cdot 3 + 2 \cdot 2 + 1 \cdot 3 = 15.\] +% Hyödyllinen havainto on, että $s(x)$ on yhtä suuri kuin +% merkkiä $x$ vastaavan solmun \emph{syvyys} puussa +% eli matka puun huipulta solmuun. +% +% Perustellaan ensin, miksi optimaalista koodia vastaa +% aina binääripuu, jossa jokaisesta solmusta lähtee +% alaspäin joko kaksi haaraa tai ei yhtään haaraa. +% Tehdään vastaoletus, että jostain solmusta lähtisi +% alaspäin vain yksi haara. +% Esimerkiksi seuraavassa puussa tällainen tilanne on solmussa $a$: +% \begin{center} +% \begin{tikzpicture}[scale=0.9] +% \node[draw, circle, minimum size=20pt] (3) at (3,1) {\phantom{$a$}}; +% \node[draw, circle, minimum size=20pt] (2) at (4,0) {$b$}; +% \node[draw, circle, minimum size=20pt] (5) at (5,1) {$a$}; +% \node[draw, circle, minimum size=20pt] (6) at (4,2) {\phantom{$a$}}; +% +% \path[draw,thick,-] (2) -- (5); +% \path[draw,thick,-] (3) -- (6); +% \path[draw,thick,-] (5) -- (6); +% \end{tikzpicture} +% \end{center} +% Tällainen solmu $a$ on kuitenkin aina turha, koska se +% tuo vain yhden bitin lisää polkuihin, jotka kulkevat +% solmun kautta, eikä sen avulla voi erottaa kahta +% koodisanaa toisistaan. Niinpä kyseisen solmun voi poistaa +% puusta, minkä seurauksena syntyy parempi koodi, +% eli optimaalista koodia vastaavassa puussa ei voi olla +% solmua, josta lähtee vain yksi haara. +% +% Perustellaan sitten, miksi on joka vaiheessa optimaalista +% yhdistää kaksi solmua, joiden painot ovat pienimmät. +% Tehdään vastaoletus, että solmun $a$ paino on pienin, +% mutta sitä ei saisi yhdistää aluksi toiseen solmuun, +% vaan sen sijasta tulisi yhdistää solmu $b$ +% ja jokin toinen solmu: +% \begin{center} +% \begin{tikzpicture}[scale=0.9] +% \node[draw, circle, minimum size=20pt] (1) at (0,0) {\phantom{$a$}}; +% \node[draw, circle, minimum size=20pt] (2) at (-2,-1) {\phantom{$a$}}; +% \node[draw, circle, minimum size=20pt] (3) at (2,-1) {$a$}; +% \node[draw, circle, minimum size=20pt] (4) at (-3,-2) {\phantom{$a$}}; +% \node[draw, circle, minimum size=20pt] (5) at (-1,-2) {\phantom{$a$}}; +% \node[draw, circle, minimum size=20pt] (8) at (-2,-3) {$b$}; +% \node[draw, circle, minimum size=20pt] (9) at (0,-3) {\phantom{$a$}}; +% +% \path[draw,thick,-] (1) -- (2); +% \path[draw,thick,-] (1) -- (3); +% \path[draw,thick,-] (2) -- (4); +% \path[draw,thick,-] (2) -- (5); +% \path[draw,thick,-] (5) -- (8); +% \path[draw,thick,-] (5) -- (9); +% \end{tikzpicture} +% \end{center} +% Solmuille $a$ ja $b$ pätee +% $c(a) \le c(b)$ ja $s(a) \le s(b)$. +% Solmut aiheuttavat bittiesityksen pituuteen lisäyksen +% \[c(a) \cdot s(a) + c(b) \cdot s(b).\] +% Tarkastellaan sitten toista tilannetta, +% joka on muuten samanlainen kuin ennen, +% mutta solmut $a$ ja $b$ on vaihdettu keskenään: +% \begin{center} +% \begin{tikzpicture}[scale=0.9] +% \node[draw, circle, minimum size=20pt] (1) at (0,0) {\phantom{$a$}}; +% \node[draw, circle, minimum size=20pt] (2) at (-2,-1) {\phantom{$a$}}; +% \node[draw, circle, minimum size=20pt] (3) at (2,-1) {$b$}; +% \node[draw, circle, minimum size=20pt] (4) at (-3,-2) {\phantom{$a$}}; +% \node[draw, circle, minimum size=20pt] (5) at (-1,-2) {\phantom{$a$}}; +% \node[draw, circle, minimum size=20pt] (8) at (-2,-3) {$a$}; +% \node[draw, circle, minimum size=20pt] (9) at (0,-3) {\phantom{$a$}}; +% +% \path[draw,thick,-] (1) -- (2); +% \path[draw,thick,-] (1) -- (3); +% \path[draw,thick,-] (2) -- (4); +% \path[draw,thick,-] (2) -- (5); +% \path[draw,thick,-] (5) -- (8); +% \path[draw,thick,-] (5) -- (9); +% \end{tikzpicture} +% \end{center} +% Osoittautuu, että tätä puuta vastaava koodi on +% \emph{yhtä hyvä tai parempi} kuin alkuperäinen koodi, joten vastaoletus +% on väärin ja Huffmanin koodaus +% toimiikin oikein, jos se yhdistää aluksi solmun $a$ +% jonkin solmun kanssa. +% Tämän perustelee seuraava epäyhtälöketju: +% \[\begin{array}{rcl} +% c(b) & \ge & c(a) \\ +% c(b)\cdot(s(b)-s(a)) & \ge & c(a)\cdot (s(b)-s(a)) \\ +% c(b)\cdot s(b)-c(b)\cdot s(a) & \ge & c(a)\cdot s(b)-c(a)\cdot s(a) \\ +% c(a)\cdot s(a)+c(b)\cdot s(b) & \ge & c(a)\cdot s(b)+c(b)\cdot s(a) \\ +% \end{array}\] \ No newline at end of file diff --git a/luku07.tex b/luku07.tex new file mode 100644 index 0000000..15f47be --- /dev/null +++ b/luku07.tex @@ -0,0 +1,976 @@ +\chapter{Dynamic programming} + +\index{dynaaminen ohjelmointi@dynaaminen ohjelmointi} + +\key{Dynaaminen ohjelmointi} +on tekniikka, joka yhdistää täydellisen haun +toimivuuden ja ahneiden algoritmien tehokkuuden. +Dynaamisen ohjelmoinnin käyttäminen edellyttää, +että tehtävä jakautuu osaongelmiin, +jotka voidaan käsitellä toisistaan riippumattomasti. + +Dynaamisella ohjelmoinnilla on kaksi käyttötarkoitusta: + +\begin{itemize} +\item +\key{Optimiratkaisun etsiminen}: +Haluamme etsiä ratkaisun, joka on +jollakin tavalla suurin mahdollinen +tai pienin mahdollinen. +\item +\key{Ratkaisuiden määrän laskeminen}: +Haluamme laskea, kuinka monta mahdollista +ratkaisua on olemassa. +\end{itemize} + +Tutustumme dynaamiseen ohjelmointiin ensin +optimiratkaisun etsimisen kautta ja käytämme sitten +samaa ideaa ratkaisujen määrän laskemiseen. + +Dynaamisen ohjelmoinnin ymmärtäminen on yksi merkkipaalu +jokaisen kisakoodarin uralla. +Vaikka menetelmän perusidea on yksinkertainen, +haasteena on oppia soveltamaan sitä sujuvasti +erilaisissa tehtävissä. +Tämä luku esittelee joukon +perusesimerkkejä, joista on hyvä lähteä liikkeelle. + +\section{Kolikkotehtävä} + +Aloitamme dynaamisen ohjelmoinnin tutun tehtävän kautta: +Muodostettavana on rahamäärä $x$ +käyttäen mahdollisimman vähän kolikoita. +Kolikoiden arvot ovat $\{c_1,c_2,\ldots,c_k\}$ +ja jokaista kolikkoa on saatavilla rajattomasti. + +Luvussa 6.1 ratkaisimme tehtävän ahneella algoritmilla, +joka muodostaa rahamäärän valiten mahdollisimman +suuria kolikoita. +Ahne algoritmi toimii esimerkiksi silloin, +kun kolikot ovat eurokolikot, +mutta yleisessä tapauksessa ahne algoritmi +ei välttämättä valitse pienintä määrää kolikoita. + +Nyt on aika ratkaista tehtävä tehokkaasti +dynaamisella ohjelmoinnilla niin, +että algoritmi toimii millä tahansa kolikoilla. +Algoritmi perustuu rekursiiviseen funktioon, +joka käy läpi kaikki vaihtoehdot rahamäärän +muodostamiseen täydellisen haun kaltaisesti. +Algoritmi toimii kuitenkin tehokkaasti, koska +se tallentaa välituloksia muistitaulukkoon, +minkä ansiosta sen ei tarvitse laskea samoja +asioita moneen kertaan. + +\subsubsection{Rekursiivinen esitys} + +\index{rekursioyhtxlz@rekursioyhtälö} + +Dynaamisessa ohjelmoinnissa on ideana esittää +ongelma rekursiivisesti niin, +että ongelman ratkaisun voi laskea +saman ongelman pienempien tapausten ratkaisuista. +Tässä tehtävässä luonteva ongelma on seuraava: +mikä on pienin määrä kolikoita, +joilla voi muodostaa rahamäärän $x$? + +Merkitään $f(x)$ funktiota, +joka antaa vastauksen ongelmaan, +eli $f(x)$ on pienin määrä kolikoita, +joilla voi muodostaa rahamäärän $x$. +Funktion arvot riippuvat siitä, +mitkä kolikot ovat käytössä. +Esimerkiksi jos kolikot ovat $\{1,3,4\}$, +funktion ensimmäiset arvot ovat: + +\[ +\begin{array}{lcl} +f(0) & = & 0 \\ +f(1) & = & 1 \\ +f(2) & = & 2 \\ +f(3) & = & 1 \\ +f(4) & = & 1 \\ +f(5) & = & 2 \\ +f(6) & = & 2 \\ +f(7) & = & 2 \\ +f(8) & = & 2 \\ +f(9) & = & 3 \\ +f(10) & = & 3 \\ +\end{array} +\] + +Nyt $f(0)=0$, koska jos rahamäärä on 0, +ei tarvita yhtään kolikkoa. +Vastaavasti $f(3)=1$, koska rahamäärän 3 +voi muodostaa kolikolla 3, +ja $f(5)=2$, koska rahamäärän 5 +voi muodostaa kolikoilla 1 ja 4. + +Oleellinen ominaisuus funktiossa on, +että arvon $f(x)$ pystyy laskemaan +rekursiivisesti käyttäen pienempiä +funktion arvoja. +Esimerkiksi jos kolikot ovat $\{1,3,4\}$, +on kolme tapaa alkaa muodostaa rahamäärää $x$: +valitaan kolikko 1, 3 tai 4. +Jos valitaan kolikko 1, täytyy +muodostaa vielä rahamäärä $x-1$. +Vastaavasti jos valitaan kolikko 3 tai 4, +täytyy muodostaa rahamäärä $x-3$ tai $x-4$. + +Niinpä rekursiivinen kaava on +\[f(x) = \min(f(x-1),f(x-3),f(x-4))+1,\] +missä funktio $\min$ valitsee pienimmän parametreistaan. +Yleisemmin jos kolikot ovat $\{c_1,c_2,\ldots,c_k\}$, +rekursiivinen kaava on +\[f(x) = \min(f(x-c_1),f(x-c_2),\ldots,f(x-c_k))+1.\] +Funktion pohjatapauksena on +\[f(0)=0,\] +koska rahamäärän 0 muodostamiseen ei tarvita +yhtään kolikkoa. +Lisäksi on hyvä määritellä +\[f(x)=\infty,\hspace{8px}\textrm{jos $x<0$}.\] +Tämä tarkoittaa, että negatiivisen rahamäärän +muodostaminen vaatii äärettömästi kolikoita, +mikä estää sen, että rekursio muodostaisi +ratkaisun, johon kuuluu negatiivinen rahamäärä. + +Nyt voimme toteuttaa funktion C++:lla suoraan +rekursiivisen määritelmän perusteella: + +\begin{lstlisting} +int f(int x) { + if (x == 0) return 0; + if (x < 0) return 1e9; + int u = 1e9; + for (int i = 1; i <= k; i++) { + u = min(u, f(x-c[i])+1); + } + return u; +} +\end{lstlisting} + +Koodi olettaa, että käytettävät kolikot ovat +$\texttt{c}[1], \texttt{c}[2], \ldots, \texttt{c}[k]$, +ja arvo $10^9$ kuvastaa ääretöntä. +Tämä on toimiva funktio, mutta se ei ole vielä tehokas, +koska funktio käy läpi valtavasti erilaisia tapoja +muodostaa rahamäärä. +Seuraavaksi esiteltävä muistitaulukko tekee +funktiosta tehokkaan. + +\subsubsection{Muistitaulukko} + +\index{muistitaulukko@muistitaulukko} + +Dynaaminen ohjelmointi tehostaa +rekursiivisen funktion laskentaa +tallentamalla funktion arvoja \key{muistitaulukkoon}. +Taulukon avulla funktion arvo +tietyllä parametrilla riittää laskea +vain kerran, minkä jälkeen sen voi +hakea suoraan taulukosta. +Tämä muutos nopeuttaa algoritmia ratkaisevasti. + +Tässä tehtävässä muistitaulukoksi sopii taulukko + +\begin{lstlisting} +int d[N]; +\end{lstlisting} + +jonka kohtaan $\texttt{d}[x]$ +lasketaan funktion arvo $f(x)$. +Vakio $N$ valitaan niin, että kaikki +laskettavat funktion arvot mahtuvat taulukkoon. + +Tämän jälkeen funktion voi toteuttaa +tehokkaasti näin: + +\begin{lstlisting} +int f(int x) { + if (x == 0) return 0; + if (x < 0) return 1e9; + if (d[x]) return d[x]; + int u = 1e9; + for (int i = 1; i <= k; i++) { + u = min(u, f(x-c[i])+1); + } + d[x] = u; + return d[x]; +} +\end{lstlisting} + +Funktio käsittelee pohjatapaukset $x=0$ +ja $x<0$ kuten ennenkin. +Sitten funktio tarkastaa, +onko $f(x)$ laskettu jo taulukkoon $\texttt{d}[x]$. +Jos $f(x)$ on laskettu, +funktio palauttaa sen suoraan. +Muussa tapauksessa funktio laskee arvon rekursiivisesti +ja tallentaa sen kohtaan $\texttt{d}[x]$. + +Muistitaulukon ansiosta funktio toimii +nopeasti, koska sen tarvitsee laskea +vastaus kullekin $x$:n arvolle +vain kerran rekursiivisesti. +Heti kun arvo $f(x)$ on tallennettu muistitaulukkoon, +sen saa haettua sieltä suoraan, +kun funktiota kutsutaan seuraavan kerran parametrilla $x$. + +Tuloksena olevan algoritmin aikavaativuus on $O(xk)$, +kun rahamäärä on $x$ ja kolikoiden määrä on $k$. +Käytännössä ratkaisu on mahdollista toteuttaa, +jos $x$ on niin pieni, että on mahdollista varata +riittävän suuri muistitaulukko. + +Huomaa, että muistitaulukon voi muodostaa +myös suoraan silmukalla ilman rekursiota +laskemalla arvot pienimmästä suurimpaan: +\begin{lstlisting} +d[0] = 0; +for (int i = 1; i <= x; i++) { + int u = 1e9; + for (int j = 1; j <= k; j++) { + if (i-c[j] < 0) continue; + u = min(u, d[i-c[j]]+1); + } + d[i] = u; +} +\end{lstlisting} + +Silmukkatoteutus on lyhyempi ja +hieman tehokkaampi kuin rekursiototeutus, +minkä vuoksi kokeneet kisakoodarit +toteuttavat dynaamisen ohjelmoinnin +usein silmukan avulla. +Kuitenkin silmukkatoteutuksen taustalla +on sama rekursiivinen idea kuin ennenkin. + +\subsubsection{Ratkaisun muodostaminen} + +Joskus optimiratkaisun arvon selvittämisen lisäksi +täytyy muodostaa näytteeksi yksi mahdollinen optimiratkaisu. +Tässä tehtävässä tämä tarkoittaa, +että ohjelman täytyy antaa esimerkki +tavasta valita kolikot, +joista muodostuu rahamäärä $x$ +käyttäen mahdollisimman vähän kolikoita. + +Ratkaisun muodostaminen onnistuu lisäämällä +koodiin uuden taulukon, joka kertoo +kullekin rahamäärälle, +mikä kolikko siitä tulee poistaa +optimiratkaisussa. +Seuraavassa koodissa taulukko \texttt{e} +huolehtii asiasta: + +\begin{lstlisting} +d[0] = 0; +for (int i = 1; i <= x; i++) { + d[i] = 1e9; + for (int j = 1; j <= k; j++) { + if (i-c[j] < 0) continue; + int u = d[i-c[j]]+1; + if (u < d[i]) { + d[i] = u; + e[i] = c[j]; + } + } +} +\end{lstlisting} + +Tämän jälkeen rahamäärän $x$ muodostavat +kolikot voi tulostaa näin: + +\begin{lstlisting} +while (x > 0) { + cout << e[x] << "\n"; + x -= e[x]; +} +\end{lstlisting} + +\subsubsection{Ratkaisuiden määrän laskeminen} + +Tarkastellaan sitten kolikkotehtävän muunnelmaa, +joka on muuten samanlainen kuin ennenkin, +mutta laskettavana on mahdollisten ratkaisuiden yhteismäärä +optimaalisen ratkaisun sijasta. +Esimerkiksi jos kolikot ovat $\{1,3,4\}$ ja rahamäärä on 5, +niin ratkaisuja on kaikkiaan 6: + +\begin{multicols}{2} +\begin{itemize} +\item $1+1+1+1+1$ +\item $1+1+3$ +\item $1+3+1$ +\item $3+1+1$ +\item $1+4$ +\item $4+1$ +\end{itemize} +\end{multicols} + +Ratkaisujen määrän laskeminen tapahtuu melko samalla tavalla +kuin optimiratkaisun etsiminen. +Erona on, että optimiratkaisun etsivässä rekursiossa +valitaan pienin tai suurin aiempi arvo, +kun taas ratkaisujen määrän laskevassa rekursiossa lasketaan +yhteen kaikki vaihtoehdot. + +Tässä tapauksessa voimme määritellä funktion $f(x)$, +joka kertoo, monellako tavalla rahamäärän $x$ +voi muodostaa kolikoista. +Esimerkiksi $f(5)=6$, kun kolikot ovat $\{1,3,4\}$. +Funktion $f(x)$ saa laskettua rekursiivisesti kaavalla +\[ f(x) = f(x-c_1)+f(x-c_2)+\cdots+f(x-c_k),\] +koska rahamäärän $x$ muodostamiseksi pitää +valita jokin kolikko $c_i$ ja muodostaa sen jälkeen rahamäärä $x-c_i$. +Pohjatapauksina ovat $f(0)=1$, koska rahamäärä 0 syntyy +ilman yhtään kolikkoa, +sekä $f(x)=0$, kun $x<0$, koska negatiivista rahamäärää +ei ole mahdollista muodostaa. + +Yllä olevassa esimerkissä funktioksi tulee +\[ f(x) = f(x-1)+f(x-3)+f(x-4) \] +ja funktion ensimmäiset arvot ovat: +\[ +\begin{array}{lcl} +f(0) & = & 1 \\ +f(1) & = & 1 \\ +f(2) & = & 1 \\ +f(3) & = & 2 \\ +f(4) & = & 4 \\ +f(5) & = & 6 \\ +f(6) & = & 9 \\ +f(7) & = & 15 \\ +f(8) & = & 25 \\ +f(9) & = & 40 \\ +\end{array} +\] + +Seuraava koodi laskee funktion $f(x)$ arvon +dynaamisella ohjelmoinnilla täyttämällä taulukon +\texttt{d} rahamäärille $0 \ldots x$: + +\begin{lstlisting} +d[0] = 1; +for (int i = 1; i <= x; i++) { + for (int j = 1; j <= k; j++) { + if (i-c[j] < 0) continue; + d[i] += d[i-c[j]]; + } +} +\end{lstlisting} + +Usein ratkaisujen määrä on niin suuri, että sitä ei tarvitse +laskea kokonaan vaan riittää ilmoittaa vastaus +modulo $m$, missä esimerkiksi $m=10^9+7$. +Tämä onnistuu muokkaamalla koodia niin, +että kaikki laskutoimitukset lasketaan modulo $m$. +Tässä tapauksessa riittää lisätä rivin +\begin{lstlisting} + d[i] += d[i-c[j]]; +\end{lstlisting} +jälkeen rivi +\begin{lstlisting} + d[i] %= m; +\end{lstlisting} + +Nyt olemme käyneet läpi kaikki dynaamisen +ohjelmoinnin perusasiat. +Dynaamista ohjelmointia voi soveltaa monilla +tavoilla erilaisissa tilanteissa, +minkä vuoksi tutustumme seuraavaksi +joukkoon tehtäviä, jotka esittelevät +dynaamisen ohjelmoinnin mahdollisuuksia. + +\section{Pisin nouseva alijono} + +\index{pisin nouseva alijono@pisin nouseva alijono} + +Annettuna on taulukko, jossa on $n$ +kokonaislukua $x_1,x_2,\ldots,x_n$. +Tehtävänä on selvittää, +kuinka pitkä on taulukon +\key{pisin nouseva alijono} +eli vasemmalta oikealle kulkeva +ketju taulukon alkioita, +jotka on valittu niin, +että jokainen alkio on edellistä suurempi. +Esimerkiksi taulukossa + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); +\node at (0.5,0.5) {$6$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$5$}; +\node at (3.5,0.5) {$1$}; +\node at (4.5,0.5) {$7$}; +\node at (5.5,0.5) {$4$}; +\node at (6.5,0.5) {$8$}; +\node at (7.5,0.5) {$3$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +pisin nouseva alijono sisältää 4 alkiota: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (1,0) rectangle (2,1); +\fill[color=lightgray] (2,0) rectangle (3,1); +\fill[color=lightgray] (4,0) rectangle (5,1); +\fill[color=lightgray] (6,0) rectangle (7,1); +\draw (0,0) grid (8,1); +\node at (0.5,0.5) {$6$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$5$}; +\node at (3.5,0.5) {$1$}; +\node at (4.5,0.5) {$7$}; +\node at (5.5,0.5) {$4$}; +\node at (6.5,0.5) {$8$}; +\node at (7.5,0.5) {$3$}; + +\draw[thick,->] (1.5,-0.25) .. controls (1.75,-1.00) and (2.25,-1.00) .. (2.4,-0.25); +\draw[thick,->] (2.6,-0.25) .. controls (3.0,-1.00) and (4.0,-1.00) .. (4.4,-0.25); +\draw[thick,->] (4.6,-0.25) .. controls (5.0,-1.00) and (6.0,-1.00) .. (6.5,-0.25); + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Merkitään $f(k)$ kohtaan $k$ päättyvän +pisimmän nousevan alijonon pituutta, +jolloin ratkaisu tehtävään on suurin +arvoista $f(1),f(2),\ldots,f(n)$. +Esimerkiksi yllä olevassa taulukossa +funktion arvot ovat seuraavat: +\[ +\begin{array}{lcl} +f(1) & = & 1 \\ +f(2) & = & 1 \\ +f(3) & = & 2 \\ +f(4) & = & 1 \\ +f(5) & = & 3 \\ +f(6) & = & 2 \\ +f(7) & = & 4 \\ +f(8) & = & 2 \\ +\end{array} +\] + +Arvon $f(k)$ laskemisessa on kaksi vaihtoehtoa, +millainen kohtaan $k$ päättyvä pisin nouseva alijono on: +\begin{enumerate} +\item Pisin nouseva alijono sisältää vain luvun $x_k$, +jolloin $f(k)=1$. +\item Valitaan jokin kohta $i$, jolle pätee $i,line width=2pt] (4.5,-4.5) -- (1.5,-1.5); + \end{scope} +\end{tikzpicture} +\end{center} + +Merkkijonojen \texttt{PALLO} ja \texttt{TALO} viimeinen merkki on sama, +joten niiden editointietäisyys on sama kuin +merkkijonojen \texttt{PALL} ja \texttt{TAL}. +Nyt voidaan poistaa viimeinen \texttt{L} merkkijonosta \texttt{PAL}, +mistä tulee yksi operaatio. +Editointietäisyys on siis yhden suurempi +kuin merkkijonoilla \texttt{PAL} ja \texttt{TAL}, jne. + +\section{Laatoitukset} + +Joskus dynaamisen ohjelmoinnin tila on monimutkaisempi kuin +kiinteä yhdistelmä lukuja. +Tarkastelemme lopuksi tehtävää, jossa +laskettavana on, monellako tavalla +kokoa $1 \times 2$ ja $2 \times 1$ olevilla laatoilla +voi täyttää $n \times m$ -kokoisen ruudukon. +Esimerkiksi ruudukolle kokoa $4 \times 7$ +yksi mahdollinen ratkaisu on +\begin{center} +\begin{tikzpicture}[scale=.65] + \draw (0,0) grid (7,4); + \draw[fill=gray] (0+0.2,0+0.2) rectangle (2-0.2,1-0.2); + \draw[fill=gray] (2+0.2,0+0.2) rectangle (4-0.2,1-0.2); + \draw[fill=gray] (4+0.2,0+0.2) rectangle (6-0.2,1-0.2); + \draw[fill=gray] (0+0.2,1+0.2) rectangle (2-0.2,2-0.2); + \draw[fill=gray] (2+0.2,1+0.2) rectangle (4-0.2,2-0.2); + \draw[fill=gray] (1+0.2,2+0.2) rectangle (3-0.2,3-0.2); + \draw[fill=gray] (1+0.2,3+0.2) rectangle (3-0.2,4-0.2); + \draw[fill=gray] (4+0.2,3+0.2) rectangle (6-0.2,4-0.2); + + \draw[fill=gray] (0+0.2,2+0.2) rectangle (1-0.2,4-0.2); + \draw[fill=gray] (3+0.2,2+0.2) rectangle (4-0.2,4-0.2); + \draw[fill=gray] (6+0.2,2+0.2) rectangle (7-0.2,4-0.2); + \draw[fill=gray] (4+0.2,1+0.2) rectangle (5-0.2,3-0.2); + \draw[fill=gray] (5+0.2,1+0.2) rectangle (6-0.2,3-0.2); + \draw[fill=gray] (6+0.2,0+0.2) rectangle (7-0.2,2-0.2); + +\end{tikzpicture} +\end{center} +ja ratkaisujen yhteismäärä on 781. + +Tehtävän voi ratkaista dynaamisella ohjelmoinnilla +käymällä ruudukkoa läpi rivi riviltä. +Jokainen ratkaisun rivi pelkistyy merkkijonoksi, +jossa on $m$ merkkiä joukosta $\{\sqcap, \sqcup, \sqsubset, \sqsupset \}$. +Esimerkiksi yllä olevassa ratkaisussa on 4 riviä, +jotka vastaavat merkkijonoja +\begin{itemize} +\item +$\sqcap \sqsubset \sqsupset \sqcap \sqsubset \sqsupset \sqcap$, +\item +$\sqcup \sqsubset \sqsupset \sqcup \sqcap \sqcap \sqcup$, +\item +$\sqsubset \sqsupset \sqsubset \sqsupset \sqcup \sqcup \sqcap$ ja +\item +$\sqsubset \sqsupset \sqsubset \sqsupset \sqsubset \sqsupset \sqcup$. +\end{itemize} + +Tehtävään sopiva rekursiivinen funktio on $f(k,x)$, +joka laskee, montako tapaa on muodostaa ratkaisu +ruudukon riveille $1 \ldots k$ niin, +että riviä $k$ vastaa merkkijono $x$. +Dynaaminen ohjelmointi on mahdollista, +koska jokaisen rivin sisältöä +rajoittaa vain edellisen rivin sisältö. + +Riveistä muodostuva ratkaisu on kelvollinen, +jos rivillä 1 ei ole merkkiä $\sqcup$, +rivillä $n$ ei ole merkkiä $\sqcap$ +ja kaikki peräkkäiset rivit ovat \emph{yhteensopivat}. +Esimerkiksi rivit +$\sqcup \sqsubset \sqsupset \sqcup \sqcap \sqcap \sqcup$ ja +$\sqsubset \sqsupset \sqsubset \sqsupset \sqcup \sqcup \sqcap$ +ovat yhteensopivat, +kun taas rivit +$\sqcap \sqsubset \sqsupset \sqcap \sqsubset \sqsupset \sqcap$ ja +$\sqsubset \sqsupset \sqsubset \sqsupset \sqsubset \sqsupset \sqcup$ +eivät ole yhteensopivat. + +Koska rivillä on $m$ merkkiä ja jokaiselle merkille on 4 +vaihtoehtoa, erilaisia rivejä on korkeintaan $4^m$. +Niinpä ratkaisun aikavaativuus on $O(n 4^{2m})$, +koska joka rivillä käydään läpi $O(4^m)$ +vaihtoehtoa rivin sisällölle +ja jokaista vaihtoehtoa kohden on $O(4^m)$ +vaihtoehtoa edellisen rivin sisällölle. +Käytännössä ruudukko kannattaa kääntää niin +päin, että pienempi sivun pituus on $m$:n roolissa, +koska $m$:n suuruus on ratkaiseva ajankäytön kannalta. + +Ratkaisua on mahdollista tehostaa parantamalla rivien esitystapaa merkkijonoina. +Osoittautuu, että ainoa seuraavalla rivillä tarvittava tieto on, +missä kohdissa riviltä lähtee laattoja alaspäin. +Niinpä rivin voikin tallentaa käyttäen vain merkkejä +$\sqcap$ ja $\Box$, missä $\Box$ kokoaa yhteen vanhat merkit +$\sqcup$, $\sqsubset$ ja $\sqsupset$. +Tällöin erilaisia rivejä on vain $2^m$ +ja aikavaativuudeksi tulee $O(n 2^{2m})$. + +Mainittakoon lopuksi, että laatoitusten määrän laskemiseen +on myös yllättävä suora kaava +\[ \prod_{a=1}^{\lceil n/2 \rceil} \prod_{b=1}^{\lceil m/2 \rceil} 4 \cdot (\cos^2 \frac{\pi a}{n + 1} + \cos^2 \frac{\pi b}{m+1}).\] +Tämä kaava on sinänsä hyvin tehokas, +koska se laskee laatoitusten määrän ajassa $O(nm)$, +mutta käytännön ongelma kaavan käyttämisessä +on, kuinka tallentaa välitulokset riittävän tarkkoina lukuina. + + diff --git a/luku08.tex b/luku08.tex new file mode 100644 index 0000000..482b20e --- /dev/null +++ b/luku08.tex @@ -0,0 +1,839 @@ +\chapter{Amortized analysis} + +\index{tasoitettu analyysi@tasoitettu analyysi} + +Monen algoritmin aikavaativuuden pystyy laskemaan +suoraan katsomalla algoritmin rakennetta: +mitä silmukoita algoritmissa on ja miten monta +kertaa niitä suoritetaan. +Joskus kuitenkaan näin suoraviivainen analyysi ei +riitä antamaan todellista kuvaa algoritmin tehokkuudesta. + +\key{Tasoitettu analyysi} soveltuu sellaisten +algoritmien analyysiin, joiden osana on jokin operaatio, +jonka ajankäyttö vaihtelee. +Ideana on tarkastella yksittäisen operaation +sijasta kaikkia operaatioita algoritmin +aikana ja laskea niiden ajankäytölle yhteinen raja. + +\section{Kahden osoittimen tekniikka} + +\index{kahden osoittimen tekniikka} + +\key{Kahden osoittimen tekniikka} on taulukon käsittelyssä +käytettävä menetelmä, jossa taulukkoa käydään läpi +kahden osoittimen avulla. +Molemmat osoittimet liikkuvat algoritmin aikana, +mutta rajoituksena on, että ne voivat liikkua vain +yhteen suuntaan, mikä takaa, että algoritmi toimii tehokkaasti. + +Tutustumme seuraavaksi kahden osoittimen tekniikkaan +kahden esimerkkitehtävän kautta. + +\subsubsection{Alitaulukon summa} + +Annettuna on taulukko, jossa on $n$ positiivista kokonaislukua. +Tehtävänä on selvittää, onko taulukossa alitaulukkoa, +jossa lukujen summa on $x$. +Esimerkiksi taulukossa +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$5$}; +\node at (4.5,0.5) {$1$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$2$}; +\node at (7.5,0.5) {$3$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +on alitaulukko, jossa lukujen summa on 8: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (2,0) rectangle (5,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$5$}; +\node at (4.5,0.5) {$1$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$2$}; +\node at (7.5,0.5) {$3$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Osoittautuu, että tämän tehtävän voi ratkaista +ajassa $O(n)$ kahden osoittimen tekniikalla. +Ideana on käydä taulukkoa läpi kahden osoittimen +avulla, jotka rajaavat välin taulukosta. +Joka vuorolla vasen osoitin liikkuu +yhden askeleen eteenpäin ja oikea osoitin +liikkuu niin kauan eteenpäin kuin summa on enintään $x$. +Jos välin summaksi tulee tarkalleen $x$, ratkaisu on löytynyt. + +Tarkastellaan esimerkkinä algoritmin toimintaa +seuraavassa taulukossa, kun tavoitteena on muodostaa summa $x=8$: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$5$}; +\node at (4.5,0.5) {$1$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$2$}; +\node at (7.5,0.5) {$3$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Aluksi osoittimet rajaavat taulukosta välin, +jonka summa on $1+3+2=6$. +Väli ei voi olla tätä suurempi, +koska seuraava luku 5 veisi summan yli $x$:n. + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (0,0) rectangle (3,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$5$}; +\node at (4.5,0.5) {$1$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$2$}; +\node at (7.5,0.5) {$3$}; + +\draw[thick,->] (0.5,-0.7) -- (0.5,-0.1); +\draw[thick,->] (2.5,-0.7) -- (2.5,-0.1); + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Seuraavaksi vasen osoitin siirtyy askeleen eteenpäin. +Oikea osoitin säilyy paikallaan, koska muuten +summa kasvaisi liian suureksi. + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (1,0) rectangle (3,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$5$}; +\node at (4.5,0.5) {$1$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$2$}; +\node at (7.5,0.5) {$3$}; + +\draw[thick,->] (1.5,-0.7) -- (1.5,-0.1); +\draw[thick,->] (2.5,-0.7) -- (2.5,-0.1); + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Vasen osoitin siirtyy taas askeleen eteenpäin +ja tällä kertaa oikea osoitin siirtyy kolme askelta +eteenpäin. Muodostuu summa $2+5+1=8$ eli taulukosta +on löytynyt väli, jonka lukujen summa on $x$. + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (2,0) rectangle (5,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$5$}; +\node at (4.5,0.5) {$1$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$2$}; +\node at (7.5,0.5) {$3$}; + +\draw[thick,->] (2.5,-0.7) -- (2.5,-0.1); +\draw[thick,->] (4.5,-0.7) -- (4.5,-0.1); + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +% Algoritmin toteutus näyttää seuraavalta: +% +% \begin{lstlisting} +% int s = 0, b = 0; +% for (int a = 1; a <= n; a++) { +% while (b] (0.5,-0.7) -- (0.5,-0.1); +\draw[thick,->] (7.5,-0.7) -- (7.5,-0.1); + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Seuraavaksi vasen osoitin liikkuu askeleen eteenpäin. +Oikea osoitin peruuttaa kolme askelta, minkä jälkeen +summana on $4+7=11$. + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (1,0) rectangle (2,1); +\fill[color=lightgray] (4,0) rectangle (5,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$4$}; +\node at (2.5,0.5) {$5$}; +\node at (3.5,0.5) {$6$}; +\node at (4.5,0.5) {$7$}; +\node at (5.5,0.5) {$9$}; +\node at (6.5,0.5) {$9$}; +\node at (7.5,0.5) {$10$}; + +\draw[thick,->] (1.5,-0.7) -- (1.5,-0.1); +\draw[thick,->] (4.5,-0.7) -- (4.5,-0.1); + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Sitten vasen osoitin siirtyy jälleen askeleen eteenpäin. +Oikea osoitin pysyy paikallaan ja ratkaisu $5+7=12$ on löytynyt. + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (2,0) rectangle (3,1); +\fill[color=lightgray] (4,0) rectangle (5,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$4$}; +\node at (2.5,0.5) {$5$}; +\node at (3.5,0.5) {$6$}; +\node at (4.5,0.5) {$7$}; +\node at (5.5,0.5) {$9$}; +\node at (6.5,0.5) {$9$}; +\node at (7.5,0.5) {$10$}; + +\draw[thick,->] (2.5,-0.7) -- (2.5,-0.1); +\draw[thick,->] (4.5,-0.7) -- (4.5,-0.1); + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Algoritmin alussa taulukon järjestäminen vie +aikaa $O(n \log n)$. +Tämän jälkeen vasen osoitin liikkuu $O(n)$ askelta +eteenpäin ja oikea osoitin liikkuu $O(n)$ askelta +taaksepäin, mihin kuluu aikaa $O(n)$. +Algoritmin kokonaisaikavaativuus on siis $O(n \log n)$. + +Huomaa, että tehtävän voi ratkaista myös +toisella tavalla ajassa +$O(n \log n)$ binäärihaun avulla. +Tässä ratkaisussa jokaiselle taulukon luvulle +etsitään binäärihaulla toista lukua niin, +että lukujen summa olisi yhteensä $x$. +Binäärihaku suoritetaan $n$ kertaa ja +jokainen binäärihaku vie aikaa $O(\log n)$. + +\index{3SUM-ongelma} +Hieman vaikeampi ongelma on \key{3SUM}, +jossa taulukosta tuleekin etsiä kolme lukua, +joiden summa on $x$. +Tämä ongelma on mahdollista ratkaista ajassa $O(n^2)$. +Keksitkö, miten se tapahtuu? + +\section{Lähin pienempi edeltäjä} + +\index{lzhin pienempi edeltxjx@lähin pienempi edeltäjä} + +Tasoitetun analyysin avulla arvioidaan usein +tietorakenteeseen kohdistuvien operaatioiden määrää. +Algoritmin operaatiot voivat jakautua epätasaisesti +niin, että useimmat operaatiot tehdään tietyssä +algoritmin vaiheessa, mutta operaatioiden +yhteismäärä on kuitenkin rajoitettu. + +Tarkastellaan esimerkkinä ongelmaa, +jossa tehtävänä on etsiä kullekin taulukon +alkiolle +\key{lähin pienempi edeltäjä} eli +lähinnä oleva pienempi alkio taulukon alkuosassa. +On mahdollista, ettei tällaista alkiota ole olemassa, +jolloin algoritmin tulee huomata asia. +Osoittautuu, että tehtävä on mahdollista ratkaista +tehokkaasti ajassa $O(n)$ sopivan tietorakenteen avulla. + +Tehokas ratkaisu tehtävään on käydä +taulukko läpi alusta loppuun ja pitää samalla yllä ketjua, +jonka ensimmäinen luku on käsiteltävä taulukon luku +ja jokainen seuraava luku on luvun lähin +pienempi edeltäjä. +Jos ketjussa on vain yksi luku, +käsiteltävällä luvulla ei ole pienempää edeltäjää. +Joka askeleella ketjun alusta poistetaan lukuja +niin kauan, kunnes ketjun ensimmäinen luku on +pienempi kuin käsiteltävä taulukon luku tai ketju on tyhjä. +Tämän jälkeen käsiteltävä luku lisätään ketjun alkuun. + +Tarkastellaan esimerkkinä algoritmin toimintaa +seuraavassa taulukossa: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$2$}; +\node at (4.5,0.5) {$5$}; +\node at (5.5,0.5) {$3$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Aluksi luvut 1, 3 ja 4 liittyvät ketjuun, koska jokainen luku on +edellistä suurempi. Siis luvun 4 lähin pienempi edeltäjä on luku 3, +jonka lähin pienempi edeltäjä on puolestaan luku 1. Tilanne näyttää tältä: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (2,0) rectangle (3,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$2$}; +\node at (4.5,0.5) {$5$}; +\node at (5.5,0.5) {$3$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$2$}; + +\draw[thick,->] (2.5,-0.25) .. controls (2.25,-1.00) and (1.75,-1.00) .. (1.6,-0.25); +\draw[thick,->] (1.4,-0.25) .. controls (1.25,-1.00) and (0.75,-1.00) .. (0.5,-0.25); + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Taulukon seuraava luku 2 on pienempi kuin ketjun kaksi ensimmäistä lukua 4 ja 3. +Niinpä luvut 4 ja 3 poistetaan ketjusta, minkä jälkeen luku 2 +lisätään ketjun alkuun. Sen lähin pienempi edeltäjä on luku 1: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (3,0) rectangle (4,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$2$}; +\node at (4.5,0.5) {$5$}; +\node at (5.5,0.5) {$3$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$2$}; + +\draw[thick,->] (3.5,-0.25) .. controls (3.00,-1.00) and (1.00,-1.00) .. (0.5,-0.25); + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Seuraava luku 5 on suurempi kuin luku 2, +joten se lisätään suoraan ketjun alkuun ja +sen lähin pienempi edeltäjä on luku 2: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (4,0) rectangle (5,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$2$}; +\node at (4.5,0.5) {$5$}; +\node at (5.5,0.5) {$3$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$2$}; + +\draw[thick,->] (3.4,-0.25) .. controls (3.00,-1.00) and (1.00,-1.00) .. (0.5,-0.25); +\draw[thick,->] (4.5,-0.25) .. controls (4.25,-1.00) and (3.75,-1.00) .. (3.6,-0.25); + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Algoritmi jatkaa samalla tavalla taulukon loppuun +ja selvittää jokaisen luvun lähimmän +pienemmän edeltäjän. +Mutta kuinka tehokas algoritmi on? + +Algoritmin tehokkuus riippuu siitä, +kauanko ketjun käsittelyyn kuluu aikaa yhteensä. +Jos uusi luku on suurempi kuin ketjun ensimmäinen +luku, se vain lisätään ketjun alkuun, +mikä on tehokasta. +Joskus taas ketjussa voi olla useita +suurempia lukuja, joiden poistaminen vie aikaa. +Oleellista on kuitenkin, että jokainen +taulukossa oleva luku liittyy +tarkalleen kerran ketjuun ja poistuu +korkeintaan kerran ketjusta. +Niinpä jokainen luku aiheuttaa $O(1)$ +ketjuun liittyvää operaatiota +ja algoritmin kokonaisaikavaativuus on $O(n)$. + +\section{Liukuvan ikkunan minimi} + +\index{liukuva ikkuna} +\index{liukuvan ikkunan minimi@liukuvan ikkunan minimi} + +\key{Liukuva ikkuna} on taulukon halki kulkeva +aktiivinen alitaulukko, jonka pituus on vakio. +Jokaisessa liukuvan ikkunan sijainnissa +halutaan tyypillisesti laskea jotain tietoa +ikkunan alueelle osuvista alkioista. +Kiinnostava tehtävä on pitää yllä +\key{liukuvan ikkunan minimiä}. +Tämä tarkoittaa, että jokaisessa liukuvan ikkunan +sijainnissa tulee ilmoittaa pienin alkio +ikkunan alueella. + +Liukuvan ikkunan minimit voi laskea +lähes samalla tavalla kuin lähimmät +pienimmät edeltäjät. +Ideana on pitää yllä ketjua, jonka alussa +on ikkunan viimeinen luku ja jossa jokainen +luku on edellistä pienempi. Joka vaiheessa +ketjun viimeinen luku on ikkunan pienin luku. +Kun liukuva ikkuna liikkuu eteenpäin ja välille +tulee uusi luku, ketjusta poistetaan kaikki luvut, +jotka ovat uutta lukua suurempia. +Tämän jälkeen uusi luku lisätään ketjun alkuun. +Lisäksi jos ketjun viimeinen luku ei enää kuulu +välille, se poistetaan ketjusta. + +Tarkastellaan esimerkkinä, kuinka algoritmi selvittää +minimit seuraavassa taulukossa, +kun ikkunan koko $k=4$. + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$2$}; +\node at (1.5,0.5) {$1$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$5$}; +\node at (4.5,0.5) {$3$}; +\node at (5.5,0.5) {$4$}; +\node at (6.5,0.5) {$1$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Liukuva ikkuna aloittaa matkansa taulukon vasemmasta reunasta. +Ensimmäisessä ikkunan sijainnissa pienin luku on 1: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (0,0) rectangle (4,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$2$}; +\node at (1.5,0.5) {$1$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$5$}; +\node at (4.5,0.5) {$3$}; +\node at (5.5,0.5) {$4$}; +\node at (6.5,0.5) {$1$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; + +\draw[thick,->] (3.5,-0.25) .. controls (3.25,-1.00) and (2.75,-1.00) .. (2.6,-0.25); +\draw[thick,->] (2.4,-0.25) .. controls (2.25,-1.00) and (1.75,-1.00) .. (1.5,-0.25); + +\end{tikzpicture} +\end{center} + +Kun ikkuna siirtyy eteenpäin, mukaan tulee luku 3, +joka on pienempi kuin luvut 5 ja 4 ketjun alussa. +Niinpä luvut 5 ja 4 poistuvat ketjusta ja luku 3 +siirtyy sen alkuun. Pienin luku on edelleen 1. +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (1,0) rectangle (5,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$2$}; +\node at (1.5,0.5) {$1$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$5$}; +\node at (4.5,0.5) {$3$}; +\node at (5.5,0.5) {$4$}; +\node at (6.5,0.5) {$1$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; + +\draw[thick,->] (4.5,-0.25) .. controls (4.25,-1.00) and (1.75,-1.00) .. (1.5,-0.25); + +\end{tikzpicture} +\end{center} + +Ikkuna siirtyy taas eteenpäin, minkä seurauksena pienin luku 1 +putoaa pois ikkunasta. Niinpä se poistetaan ketjun lopusta +ja uusi pienin luku on 3. Lisäksi uusi ikkunaan tuleva luku 4 +lisätään ketjun alkuun. +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (2,0) rectangle (6,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$2$}; +\node at (1.5,0.5) {$1$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$5$}; +\node at (4.5,0.5) {$3$}; +\node at (5.5,0.5) {$4$}; +\node at (6.5,0.5) {$1$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; + +\draw[thick,->] (5.5,-0.25) .. controls (5.25,-1.00) and (4.75,-1.00) .. (4.5,-0.25); +\end{tikzpicture} +\end{center} + +Seuraavaksi ikkunaan tuleva luku 1 on pienempi +kuin kaikki ketjussa olevat luvut. +Tämän seurauksena koko ketju tyhjentyy ja +siihen jää vain luku 1: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (3,0) rectangle (7,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$2$}; +\node at (1.5,0.5) {$1$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$5$}; +\node at (4.5,0.5) {$3$}; +\node at (5.5,0.5) {$4$}; +\node at (6.5,0.5) {$1$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; + +\fill[color=black] (6.5,-0.25) circle (0.1); + +%\draw[thick,->] (5.5,-0.25) .. controls (5.25,-1.00) and (4.75,-1.00) .. (4.5,-0.25); +\end{tikzpicture} +\end{center} + +Lopuksi ikkuna saapuu viimeiseen sijaintiinsa. +Luku 2 lisätään ketjun alkuun, +mutta ikkunan pienin luku on edelleen 1. +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (4,0) rectangle (8,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$2$}; +\node at (1.5,0.5) {$1$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$5$}; +\node at (4.5,0.5) {$3$}; +\node at (5.5,0.5) {$4$}; +\node at (6.5,0.5) {$1$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; + +\draw[thick,->] (7.5,-0.25) .. controls (7.25,-1.00) and (6.75,-1.00) .. (6.5,-0.25); +\end{tikzpicture} +\end{center} + +Tässäkin algoritmissa jokainen taulukon luku lisätään +ketjuun tarkalleen kerran ja poistetaan ketjusta korkeintaan kerran, +joko ketjun alusta tai ketjun lopusta. +Niinpä algoritmin kokonaisaikavaativuus on $O(n)$. + + + diff --git a/luku09.tex b/luku09.tex new file mode 100644 index 0000000..920936e --- /dev/null +++ b/luku09.tex @@ -0,0 +1,1425 @@ +\chapter{Range queries} + +\index{vxlikysely@välikysely} +\index{summakysely@summakysely} +\index{minimikysely@minimikysely} +\index{maksimikysely@maksimikysely} + +\key{Välikysely} kohdistuu taulukon välille $[a,b]$, +ja tehtävänä on laskea haluttu tieto välillä olevista alkioista. +Tavallisia välikyselyitä ovat: +\begin{itemize} +\item \key{summakysely}: laske välin $[a,b]$ summa +\item \key{minimikysely}: etsi pienin alkio välillä $[a,b]$ +\item \key{maksimikysely}: etsi suurin alkio välillä $[a,b]$ +\end{itemize} +Esimerkiksi seuraavan taulukon välillä $[4,7]$ +summa on $4+6+1+3=14$, minimi on 1 ja maksimi on 6: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (3,0) rectangle (7,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$8$}; +\node at (3.5,0.5) {$4$}; +\node at (4.5,0.5) {$6$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$3$}; +\node at (7.5,0.5) {$4$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Helppo tapa vastata välikyselyyn on +käydä läpi kaikki välin alkiot silmukalla. +Esimerkiksi seuraava funktio toteuttaa summakyselyn: + +\begin{lstlisting} +int summa(int a, int b) { + int s = 0; + for (int i = a; i <= b; i++) { + s += t[i]; + } + return s; +} +\end{lstlisting} + +Yllä oleva funktio toteuttaa summakyselyn +ajassa $O(n)$, mikä on hidasta, +jos taulukko on suuri ja kyselyitä tulee paljon. +Tässä luvussa opimme, miten välikyselyitä pystyy +toteuttamaan huomattavasti nopeammin. + +\section{Staattisen taulukon kyselyt} + +Aloitamme yksinkertaisesta +tilanteesta, jossa taulukko on \key{staattinen} +eli sen sisältö ei muutu kyselyiden välillä. +Tällöin riittää muodostaa ennen kyselyitä +taulukon pohjalta tietorakenne, +josta voi selvittää tehokkaasti vastauksen mihin tahansa väliin +kohdistuvaan kyselyyn. + +\subsubsection{Summakysely} + +\index{summataulukko@summataulukko} + +Summakyselyyn on mahdollista vastata tehokkaasti +muodostamalla taulukosta etukäteen \key{summataulukko}, +jonka kohdassa $k$ on taulukon välin $[1,k]$ summa. +Tämän jälkeen minkä tahansa välin $[a,b]$ +summan saa laskettua $O(1)$-ajassa +summataulukkoa käyttäen. + +Esimerkiksi taulukon +\begin{center} +\begin{tikzpicture}[scale=0.7] +%\fill[color=lightgray] (3,0) rectangle (7,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$8$}; +\node at (4.5,0.5) {$6$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +summataulukko on seuraava: +\begin{center} +\begin{tikzpicture}[scale=0.7] +%\fill[color=lightgray] (3,0) rectangle (7,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$4$}; +\node at (2.5,0.5) {$8$}; +\node at (3.5,0.5) {$16$}; +\node at (4.5,0.5) {$22$}; +\node at (5.5,0.5) {$23$}; +\node at (6.5,0.5) {$27$}; +\node at (7.5,0.5) {$29$}; + + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} + +Seuraava koodi muodostaa taulukosta \texttt{t} +summataulukon \texttt{s} ajassa $O(n)$: +\begin{lstlisting} +for (int i = 1; i <= n; i++) { + s[i] = s[i-1]+t[i]; +} +\end{lstlisting} +Tämän jälkeen summakyselyyn voi vastata +ajassa $O(1)$ seuraavasti: +\begin{lstlisting} +int summa(int a, int b) { + return s[b]-s[a-1]; +} +\end{lstlisting} + +Funktio laskee välin $[a,b]$ summan +vähentämällä välin $[1,b]$ summasta +välin $[1,a-1]$ summan. +Summataulukosta riittää siis hakea kaksi arvoa +ja aikaa kuluu vain $O(1)$. +Huomaa, että 1-indeksoinnin ansiosta +funktio toimii myös tapauksessa $a=1$, +kunhan $\texttt{s}[0]=0$. + +Tarkastellaan esimerkiksi väliä $[4,7]$: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (3,0) rectangle (7,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$8$}; +\node at (4.5,0.5) {$6$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +Välin $[4,7]$ summa on $8+6+1+4=19$. +Tämän saa laskettua tehokkaasti summataulukosta +etsimällä välien $[1,3]$ ja $[1,7]$ summat: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (2,0) rectangle (3,1); +\fill[color=lightgray] (6,0) rectangle (7,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$4$}; +\node at (2.5,0.5) {$8$}; +\node at (3.5,0.5) {$16$}; +\node at (4.5,0.5) {$22$}; +\node at (5.5,0.5) {$23$}; +\node at (6.5,0.5) {$27$}; +\node at (7.5,0.5) {$29$}; + + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +Välin $[4,7]$ summa on siis $27-8=19$. + +Summataulukon idean voi yleistää +myös kaksiulotteiseen taulukkoon, +jolloin summataulukosta voi laskea +minkä tahansa suorakulmaisen alueen +summan $O(1)$-ajassa. +Tällöin summataulukkoon tallennetaan summia +alueista, jotka alkavat taulukon vasemmasta yläkulmasta. + +\begin{samepage} +Seuraava ruudukko havainnollistaa asiaa: +\begin{center} +\begin{tikzpicture}[scale=0.55] +\draw[fill=lightgray] (3,2) rectangle (7,5); +\draw (0,0) grid (10,7); +%\draw[line width=2pt] (3,2) rectangle (7,5); +\node[anchor=center] at (6.5, 2.5) {$A$}; +\node[anchor=center] at (2.5, 2.5) {$B$}; +\node[anchor=center] at (6.5, 5.5) {$C$}; +\node[anchor=center] at (2.5, 5.5) {$D$}; +\end{tikzpicture} +\end{center} +\end{samepage} + +Harmaan suorakulmion summan saa laskettua kaavalla +\[S(A) - S(B) - S(C) + S(D),\] +missä $S(X)$ tarkoittaa summaa vasemmasta +yläkulmasta kirjaimen $X$ osoittamaan kohtaan asti. + +\subsubsection{Minimikysely} + +Myös minimikyselyyn on mahdollista +vastata $O(1)$-ajassa sopivan esikäsittelyn avulla, +joskin tämä on vaikeampaa kuin summakyselyssä. +Huomaa, että minimikysely ja maksimikysely on +mahdollista toteuttaa aina samalla tavalla, +joten riittää keskittyä minimikyselyn toteutukseen. + +Ideana on laskea etukäteen taulukon jokaiselle +$2^k$-kokoiselle välille, mikä on kyseisen välin minimi. +Esimerkiksi taulukosta + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$8$}; +\node at (4.5,0.5) {$6$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +lasketaan seuraavat minimit: + +\begin{center} +\begin{tabular}{ccc} + +\begin{tabular}{ccc} +väli & koko & minimi \\ +\hline +$[1,1]$ & 1 & 1 \\ +$[2,2]$ & 1 & 3 \\ +$[3,3]$ & 1 & 4 \\ +$[4,4]$ & 1 & 8 \\ +$[5,5]$ & 1 & 6 \\ +$[6,6]$ & 1 & 1 \\ +$[7,7]$ & 1 & 4 \\ +$[8,8]$ & 1 & 2 \\ +\end{tabular} + +& + +\begin{tabular}{ccc} +väli & koko & minimi \\ +\hline +$[1,2]$ & 2 & 1 \\ +$[2,3]$ & 2 & 3 \\ +$[3,4]$ & 2 & 4 \\ +$[4,5]$ & 2 & 6 \\ +$[5,6]$ & 2 & 1 \\ +$[6,7]$ & 2 & 1 \\ +$[7,8]$ & 2 & 2 \\ +\\ +\end{tabular} + +& + +\begin{tabular}{ccc} +väli & koko & minimi \\ +\hline +$[1,4]$ & 4 & 1 \\ +$[2,5]$ & 4 & 3 \\ +$[3,6]$ & 4 & 1 \\ +$[4,7]$ & 4 & 1 \\ +$[5,8]$ & 4 & 1 \\ +$[1,8]$ & 8 & 1 \\ +\\ +\\ +\end{tabular} + +\end{tabular} + +\end{center} + +Taulukon $2^k$-välien määrä on $O(n \log n)$, +koska jokaisesta taulukon kohdasta alkaa +$O(\log n)$ väliä. +Kaikkien $2^k$-välien minimit pystytään laskemaan +ajassa $O(n \log n)$, koska jokainen $2^k$-väli +muodostuu kahdesta $2^{k-1}$ välistä ja +$2^k$-välin minimi on pienempi $2^{k-1}$-välien minimeistä. + +Tämän jälkeen minkä tahansa välin $[a,b]$ minimin +saa laskettua $O(1)$-ajassa miniminä kahdesta $2^k$-välistä, +missä $k=\lfloor \log_2(b-a+1) \rfloor$. +Ensimmäinen väli alkaa kohdasta $a$ +ja toinen väli päättyy kohtaan $b$. +Parametri $k$ on valittu niin, +että kaksi $2^k$-kokoista väliä +kattaa koko välin $[a,b]$. + +Tarkastellaan esimerkiksi väliä $[2,7]$: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (1,0) rectangle (7,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$8$}; +\node at (4.5,0.5) {$6$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +Välin $[2,7]$ pituus on 6 ja $\lfloor \log_2(6) \rfloor = 2$. +Niinpä välin minimin saa selville kahden 4-pituisen +välin minimistä. +Välit ovat $[2,5]$ ja $[4,7]$: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (1,0) rectangle (5,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$8$}; +\node at (4.5,0.5) {$6$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (3,0) rectangle (7,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$8$}; +\node at (4.5,0.5) {$6$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$2$}; + + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +Välin $[2,5]$ minimi on 3 ja välin $[4,7]$ minimi on 1. +Tämän seurauksena välin $[2,7]$ minimi on pienempi näistä eli 1. +% +% Mainittakoon, että $O(1)$-aikaiset minimikyselyt pystyy +% toteuttamaan myös niin, että esikäsittely vie aikaa +% vain $O(n)$ eikä $O(n \log n)$. +% Tämä on kuitenkin selvästi vaikeampaa, eikä +% sillä ole merkitystä kisakoodauksessa. + +\section{Binääri-indeksipuu} + +\index{binxxri-indeksipuu@binääri-indeksipuu} +\index{Fenwick-puu} + +\key{Binääri-indeksipuu} eli \key{Fenwick-puu} on +summataulukkoa muistuttava tietorakenne, +joka toteuttaa kaksi operaatiota: +taulukon välin $[a,b]$ summakysely +sekä taulukon kohdassa $k$ olevan luvun päivitys. +Kummankin operaation aikavaativuus on $O(\log n)$. + +Binääri-indeksipuun etuna summataulukkoon verrattuna on, +että taulukkoa pystyy päivittämään tehokkaasti +summakyselyiden välissä. +Summataulukossa tämä ei olisi mahdollista, +vaan koko summataulukko tulisi muodostaa uudestaan $O(n)$-ajassa +taulukon päivityksen jälkeen. + +\subsubsection{Rakenne} + +Binääri-indeksipuu on taulukko, jonka +kohdassa $k$ on kohtaan $k$ päättyvän välin lukujen summa +alkuperäisessä taulukossa. +Välin pituus on suurin 2:n potenssi, jolla $k$ on jaollinen. +Esimerkiksi jos $k=6$, välin pituus on 2, koska +6 on jaollinen 2:lla mutta ei ole jaollinen 4:llä. + +\begin{samepage} +Esimerkiksi taulukkoa +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$3$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$8$}; +\node at (4.5,0.5) {$6$}; +\node at (5.5,0.5) {$1$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$2$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +\end{samepage} +vastaava binääri-indeksipuu on seuraava: +\begin{center} +\begin{tikzpicture}[scale=0.7] +%\fill[color=lightgray] (3,0) rectangle (7,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$4$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$16$}; +\node at (4.5,0.5) {$6$}; +\node at (5.5,0.5) {$7$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$29$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; + +\draw[->,thick] (0.5,-0.9) -- (0.5,-0.1); +\draw[->,thick] (2.5,-0.9) -- (2.5,-0.1); +\draw[->,thick] (4.5,-0.9) -- (4.5,-0.1); +\draw[->,thick] (6.5,-0.9) -- (6.5,-0.1); +\draw[->,thick] (1.5,-1.9) -- (1.5,-0.1); +\draw[->,thick] (5.5,-1.9) -- (5.5,-0.1); +\draw[->,thick] (3.5,-2.9) -- (3.5,-0.1); +\draw[->,thick] (7.5,-3.9) -- (7.5,-0.1); + +\draw (0,-1) -- (1,-1) -- (1,-1.5) -- (0,-1.5) -- (0,-1); +\draw (2,-1) -- (3,-1) -- (3,-1.5) -- (2,-1.5) -- (2,-1); +\draw (4,-1) -- (5,-1) -- (5,-1.5) -- (4,-1.5) -- (4,-1); +\draw (6,-1) -- (7,-1) -- (7,-1.5) -- (6,-1.5) -- (6,-1); +\draw (0,-2) -- (2,-2) -- (2,-2.5) -- (0,-2.5) -- (0,-2); +\draw (4,-2) -- (6,-2) -- (6,-2.5) -- (4,-2.5) -- (4,-2); +\draw (0,-3) -- (4,-3) -- (4,-3.5) -- (0,-3.5) -- (0,-3); +\draw (0,-4) -- (8,-4) -- (8,-4.5) -- (0,-4.5) -- (0,-4); +\end{tikzpicture} +\end{center} + +Esimerkiksi binääri-indeksipuun kohdassa 6 on luku 7, +koska välin $[5,6]$ lukujen summa on $6+1=7$. + +\subsubsection{Summakysely} + +Binääri-indeksipuun perusoperaatio on välin $[1,k]$ +summan laskeminen, missä $k$ on mikä tahansa taulukon kohta. +Tällaisen summan pystyy muodostamaan aina laskemalla yhteen +puussa olevia välien summia. + +Esimerkiksi välin $[1,7]$ +summa muodostuu seuraavista summista: +\begin{center} +\begin{tikzpicture}[scale=0.7] +%\fill[color=lightgray] (3,0) rectangle (7,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$4$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$16$}; +\node at (4.5,0.5) {$6$}; +\node at (5.5,0.5) {$7$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$29$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; + +\draw[->,thick] (0.5,-0.9) -- (0.5,-0.1); +\draw[->,thick] (2.5,-0.9) -- (2.5,-0.1); +\draw[->,thick] (4.5,-0.9) -- (4.5,-0.1); +\draw[->,thick] (6.5,-0.9) -- (6.5,-0.1); +\draw[->,thick] (1.5,-1.9) -- (1.5,-0.1); +\draw[->,thick] (5.5,-1.9) -- (5.5,-0.1); +\draw[->,thick] (3.5,-2.9) -- (3.5,-0.1); +\draw[->,thick] (7.5,-3.9) -- (7.5,-0.1); + +\draw (0,-1) -- (1,-1) -- (1,-1.5) -- (0,-1.5) -- (0,-1); +\draw (2,-1) -- (3,-1) -- (3,-1.5) -- (2,-1.5) -- (2,-1); +\draw (4,-1) -- (5,-1) -- (5,-1.5) -- (4,-1.5) -- (4,-1); +\draw[fill=lightgray] (6,-1) -- (7,-1) -- (7,-1.5) -- (6,-1.5) -- (6,-1); +\draw (0,-2) -- (2,-2) -- (2,-2.5) -- (0,-2.5) -- (0,-2); +\draw[fill=lightgray] (4,-2) -- (6,-2) -- (6,-2.5) -- (4,-2.5) -- (4,-2); +\draw[fill=lightgray] (0,-3) -- (4,-3) -- (4,-3.5) -- (0,-3.5) -- (0,-3); +\draw (0,-4) -- (8,-4) -- (8,-4.5) -- (0,-4.5) -- (0,-4); +\end{tikzpicture} +\end{center} + +Välin $[1,7]$ summa on siis $16+7+4=27$. +Binääri-indeksipuun rakenteen ansiosta +jokainen summaan kuuluva väli on eripituinen. +Niinpä summa muodostuu aina $O(\log n)$ välin summasta. + +Summataulukon tavoin binääri-indeksipuusta voi laskea +tehokkaasti minkä tahansa taulukon välin summan, +koska välin $[a,b]$ summa saadaan vähentämällä +välin $[1,b]$ summasta välin $[1,a-1]$ summa. +Aikavaativuus on edelleen $O(\log n)$, +koska riittää laskea kaksi $[1,k]$-välin summaa. + +\subsubsection{Taulukon päivitys} + +Kun taulukon kohdassa $k$ oleva luku muuttuu, +tämä vaikuttaa useaan +binääri-indeksi\-puussa olevaan summaan. +Esimerkiksi jos kohdassa 3 oleva luku muuttuu, +seuraavat välien summat muuttuvat: +\begin{center} +\begin{tikzpicture}[scale=0.7] +%\fill[color=lightgray] (3,0) rectangle (7,1); +\draw (0,0) grid (8,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$4$}; +\node at (2.5,0.5) {$4$}; +\node at (3.5,0.5) {$16$}; +\node at (4.5,0.5) {$6$}; +\node at (5.5,0.5) {$7$}; +\node at (6.5,0.5) {$4$}; +\node at (7.5,0.5) {$29$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; + +\draw[->,thick] (0.5,-0.9) -- (0.5,-0.1); +\draw[->,thick] (2.5,-0.9) -- (2.5,-0.1); +\draw[->,thick] (4.5,-0.9) -- (4.5,-0.1); +\draw[->,thick] (6.5,-0.9) -- (6.5,-0.1); +\draw[->,thick] (1.5,-1.9) -- (1.5,-0.1); +\draw[->,thick] (5.5,-1.9) -- (5.5,-0.1); +\draw[->,thick] (3.5,-2.9) -- (3.5,-0.1); +\draw[->,thick] (7.5,-3.9) -- (7.5,-0.1); + +\draw (0,-1) -- (1,-1) -- (1,-1.5) -- (0,-1.5) -- (0,-1); +\draw[fill=lightgray] (2,-1) -- (3,-1) -- (3,-1.5) -- (2,-1.5) -- (2,-1); +\draw (4,-1) -- (5,-1) -- (5,-1.5) -- (4,-1.5) -- (4,-1); +\draw (6,-1) -- (7,-1) -- (7,-1.5) -- (6,-1.5) -- (6,-1); +\draw (0,-2) -- (2,-2) -- (2,-2.5) -- (0,-2.5) -- (0,-2); +\draw (4,-2) -- (6,-2) -- (6,-2.5) -- (4,-2.5) -- (4,-2); +\draw[fill=lightgray] (0,-3) -- (4,-3) -- (4,-3.5) -- (0,-3.5) -- (0,-3); +\draw[fill=lightgray] (0,-4) -- (8,-4) -- (8,-4.5) -- (0,-4.5) -- (0,-4); +\end{tikzpicture} +\end{center} + +Myös tässä tapauksessa kaikki välit, +joihin muutos vaikuttaa, ovat eripituisia, +joten muutos kohdistuu $O(\log n)$ kohtaan +binääri-indeksipuussa. + +\subsubsection{Toteutus} + +Binääri-indeksipuun operaatiot on mahdollista toteuttaa +lyhyesti ja tehokkaasti bittien käsittelyn avulla. +Oleellinen bittioperaatio on $k \& -k$, +joka eristää luvusta $k$ viimeisenä olevan ykkösbitin. +Esimerkiksi $6 \& -6=2$, koska luku $6$ on bittimuodossa 110 +ja luku $2$ on bittimuodossa on 10. + +Osoittautuu, että summan laskemisessa +binääri-indeksipuun kohtaa $k$ tulee muuttaa joka askeleella niin, +että siitä poistetaan luku $k \& -k$. +Vastaavasti taulukon päivityksessä kohtaa $k$ tulee muuttaa joka askeleella niin, +että siihen lisätään luku $k \& -k$. + + +Seuraavat funktiot olettavat, että binääri-indeksipuu on +tallennettu taulukkoon \texttt{b} ja se muodostuu kohdista $1 \ldots n$. + +Funktio \texttt{summa} laskee välin $[1,k]$ summan: +\begin{lstlisting} +int summa(int k) { + int s = 0; + while (k >= 1) { + s += b[k]; + k -= k&-k; + } + return s; +} +\end{lstlisting} + +Funktio \texttt{lisaa} kasvattaa taulukon kohtaa $k$ arvolla $x$: +\begin{lstlisting} +void lisaa(int k, int x) { + while (k <= n) { + b[k] += x; + k += k&-k; + } +} +\end{lstlisting} + +Kummankin yllä olevan funktion aikavaativuus on $O(\log n)$, +koska funktiot muuttavat $O(\log n)$ kohtaa +binääri-indeksipuussa ja uuteen kohtaan siirtyminen +vie aikaa $O(1)$ bittioperaation avulla. + +\section{Segmenttipuu} + +\index{segmenttipuu@segmenttipuu} + +\key{Segmenttipuu} on tietorakenne, +jonka operaatiot ovat taulukon välin $[a,b]$ välikysely +sekä kohdan $k$ arvon päivitys. +Segmenttipuun avulla voi toteuttaa summakyselyn, +minimikyselyn ja monia muitakin kyselyitä niin, +että kummankin operaation aikavaativuus on $O(\log n)$. + +Segmenttipuun etuna binääri-indeksipuuhun verrattuna on, +että se on yleisempi tietorakenne. +Binääri-indeksipuulla voi toteuttaa vain summakyselyn, +mutta segmenttipuu sallii muitakin kyselyitä. +Toisaalta segmenttipuu vie enemmän muistia ja +on hieman vaikeampi toteuttaa kuin binääri-indeksipuu. + +\subsubsection{Rakenne} + +Segmenttipuussa on $2n-1$ solmua niin, +että alimmalla tasolla on $n$ solmua, +jotka kuvaavat taulukon sisällön, +ja ylemmillä tasoilla on välikyselyihin +tarvittavaa tietoa. +Segmenttipuun sisältö riippuu siitä, +mikä välikysely puun tulee toteuttaa. +Oletamme aluksi, että välikysely on tuttu summakysely. + +Esimerkiksi taulukkoa +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\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$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +vastaa seuraava segmenttipuu: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\node[anchor=center] at (0.5, 0.5) {5}; +\node[anchor=center] at (1.5, 0.5) {8}; +\node[anchor=center] at (2.5, 0.5) {6}; +\node[anchor=center] at (3.5, 0.5) {3}; +\node[anchor=center] at (4.5, 0.5) {2}; +\node[anchor=center] at (5.5, 0.5) {7}; +\node[anchor=center] at (6.5, 0.5) {2}; +\node[anchor=center] at (7.5, 0.5) {6}; + +\node[draw, circle] (a) at (1,2.5) {13}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,minimum size=22pt] (b) at (3,2.5) {9}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); +\node[draw, circle,minimum size=22pt] (c) at (5,2.5) {9}; +\path[draw,thick,-] (c) -- (4.5,1); +\path[draw,thick,-] (c) -- (5.5,1); +\node[draw, circle,minimum size=22pt] (d) at (7,2.5) {8}; +\path[draw,thick,-] (d) -- (6.5,1); +\path[draw,thick,-] (d) -- (7.5,1); + +\node[draw, circle] (i) at (2,4.5) {22}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\node[draw, circle] (j) at (6,4.5) {17}; +\path[draw,thick,-] (j) -- (c); +\path[draw,thick,-] (j) -- (d); + +\node[draw, circle] (m) at (4,6.5) {39}; +\path[draw,thick,-] (m) -- (i); +\path[draw,thick,-] (m) -- (j); +\end{tikzpicture} +\end{center} + +Jokaisessa segmenttipuun solmussa on tietoa +$2^k$-kokoisesta välistä taulukossa. +Tässä tapauksessa solmussa oleva arvo kertoo, +mikä on taulukon lukujen summa solmua vastaavalla +välillä. +Kunkin solmun arvo saadaan laskemalla yhteen +solmun alapuolella vasemmalla ja oikealla +olevien solmujen arvot. + +Segmenttipuu on mukavinta rakentaa niin, +että taulukon koko on 2:n potenssi, +jolloin tuloksena on täydellinen binääripuu. +Jatkossa oletamme aina, +että taulukko täyttää tämän vaatimuksen. +Jos taulukon koko ei ole 2:n potenssi, +sen loppuun voi lisätä tyhjää niin, +että koosta tulee 2:n potenssi. + +\subsubsection{Välikysely} + +Segmenttipuussa vastaus välikyselyyn lasketaan +väliin kuuluvista solmuista, +jotka ovat mahdollisimman korkealla puussa. +Jokainen solmu antaa vastauksen väliin kuuluvalle osavälille, +ja vastaus kyselyyn selviää yhdistämällä +segmenttipuusta saadut osavälejä koskeva tiedot. + +Tarkastellaan esimerkiksi seuraavaa taulukon väliä: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=gray!50] (2,0) rectangle (8,1); +\draw (0,0) grid (8,1); + +\node[anchor=center] at (0.5, 0.5) {5}; +\node[anchor=center] at (1.5, 0.5) {8}; +\node[anchor=center] at (2.5, 0.5) {6}; +\node[anchor=center] at (3.5, 0.5) {3}; +\node[anchor=center] at (4.5, 0.5) {2}; +\node[anchor=center] at (5.5, 0.5) {7}; +\node[anchor=center] at (6.5, 0.5) {2}; +\node[anchor=center] at (7.5, 0.5) {6}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\end{tikzpicture} +\end{center} +Lukujen summa välillä $[3,8]$ on $6+3+2+7+2+6=26$. +Segmenttipuusta summa saadaan laskettua seuraavien +osasummien avulla: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\node[anchor=center] at (0.5, 0.5) {5}; +\node[anchor=center] at (1.5, 0.5) {8}; +\node[anchor=center] at (2.5, 0.5) {6}; +\node[anchor=center] at (3.5, 0.5) {3}; +\node[anchor=center] at (4.5, 0.5) {2}; +\node[anchor=center] at (5.5, 0.5) {7}; +\node[anchor=center] at (6.5, 0.5) {2}; +\node[anchor=center] at (7.5, 0.5) {6}; + +\node[draw, circle] (a) at (1,2.5) {13}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,fill=gray!50,minimum size=22pt] (b) at (3,2.5) {9}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); +\node[draw, circle,minimum size=22pt] (c) at (5,2.5) {9}; +\path[draw,thick,-] (c) -- (4.5,1); +\path[draw,thick,-] (c) -- (5.5,1); +\node[draw, circle,minimum size=22pt] (d) at (7,2.5) {8}; +\path[draw,thick,-] (d) -- (6.5,1); +\path[draw,thick,-] (d) -- (7.5,1); + +\node[draw, circle] (i) at (2,4.5) {22}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\node[draw, circle,fill=gray!50] (j) at (6,4.5) {17}; +\path[draw,thick,-] (j) -- (c); +\path[draw,thick,-] (j) -- (d); + +\node[draw, circle] (m) at (4,6.5) {39}; +\path[draw,thick,-] (m) -- (i); +\path[draw,thick,-] (m) -- (j); +\end{tikzpicture} +\end{center} +Taulukon välin summaksi tulee osasummista $9+17=26$. + +Kun vastaus välikyselyyn lasketaan mahdollisimman +korkealla segmenttipuussa olevista solmuista, +väliin kuuluu enintään kaksi solmua +jokaiselta segmenttipuun tasolta. +Tämän ansiosta välikyselyssä +tarvittavien solmujen yhteismäärä on vain $O(\log n)$. + +\subsubsection{Taulukon päivitys} + +Kun taulukossa oleva arvo muuttuu, +segmenttipuussa täytyy päivittää +kaikkia solmuja, joiden arvo +riippuu muutetusta taulukon kohdasta. +Tämä tapahtuu kulkemalla puuta ylöspäin huipulle +asti ja tekemällä muutokset. + +\begin{samepage} +Seuraava kuva näyttää, mitkä solmut segmenttipuussa muuttuvat, +jos taulukon luku 7 muuttuu. + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=gray!50] (5,0) rectangle (6,1); +\draw (0,0) grid (8,1); + +\node[anchor=center] at (0.5, 0.5) {5}; +\node[anchor=center] at (1.5, 0.5) {8}; +\node[anchor=center] at (2.5, 0.5) {6}; +\node[anchor=center] at (3.5, 0.5) {3}; +\node[anchor=center] at (4.5, 0.5) {2}; +\node[anchor=center] at (5.5, 0.5) {7}; +\node[anchor=center] at (6.5, 0.5) {2}; +\node[anchor=center] at (7.5, 0.5) {6}; + +\node[draw, circle] (a) at (1,2.5) {13}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,minimum size=22pt] (b) at (3,2.5) {9}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); +\node[draw, circle,minimum size=22pt,fill=gray!50] (c) at (5,2.5) {9}; +\path[draw,thick,-] (c) -- (4.5,1); +\path[draw,thick,-] (c) -- (5.5,1); +\node[draw, circle,minimum size=22pt] (d) at (7,2.5) {8}; +\path[draw,thick,-] (d) -- (6.5,1); +\path[draw,thick,-] (d) -- (7.5,1); + +\node[draw, circle] (i) at (2,4.5) {22}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\node[draw, circle,fill=gray!50] (j) at (6,4.5) {17}; +\path[draw,thick,-] (j) -- (c); +\path[draw,thick,-] (j) -- (d); + +\node[draw, circle,fill=gray!50] (m) at (4,6.5) {39}; +\path[draw,thick,-] (m) -- (i); +\path[draw,thick,-] (m) -- (j); +\end{tikzpicture} +\end{center} +\end{samepage} + +Polku segmenttipuun pohjalta huipulle muodostuu aina $O(\log n)$ solmusta, +joten taulukon arvon muuttuminen vaikuttaa $O(\log n)$ solmuun puussa. + +\subsubsection{Puun tallennus} + +Segmenttipuun voi tallentaa muistiin +$2N$ alkion taulukkona, +jossa $N$ on riittävän suuri 2:n potenssi. +Tällaisen segmenttipuun avulla voi ylläpitää +taulukkoa, jonka indeksialue on $[0,N-1]$. + +Segmenttipuun taulukon +kohdassa 1 on puun ylimmän solmun arvo, +kohdat 2 ja 3 sisältävät seuraavan tason +solmujen arvot, jne. +Segmenttipuun alin taso eli varsinainen +taulukon sisältä tallennetaan +kohdasta $N$ alkaen. +Niinpä taulukon kohdassa $k$ oleva alkio +on segmenttipuun taulukossa kohdassa $k+N$. + +Esimerkiksi segmenttipuun +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\node[anchor=center] at (0.5, 0.5) {5}; +\node[anchor=center] at (1.5, 0.5) {8}; +\node[anchor=center] at (2.5, 0.5) {6}; +\node[anchor=center] at (3.5, 0.5) {3}; +\node[anchor=center] at (4.5, 0.5) {2}; +\node[anchor=center] at (5.5, 0.5) {7}; +\node[anchor=center] at (6.5, 0.5) {2}; +\node[anchor=center] at (7.5, 0.5) {6}; + +\node[draw, circle] (a) at (1,2.5) {13}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,minimum size=22pt] (b) at (3,2.5) {9}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); +\node[draw, circle,minimum size=22pt] (c) at (5,2.5) {9}; +\path[draw,thick,-] (c) -- (4.5,1); +\path[draw,thick,-] (c) -- (5.5,1); +\node[draw, circle,minimum size=22pt] (d) at (7,2.5) {8}; +\path[draw,thick,-] (d) -- (6.5,1); +\path[draw,thick,-] (d) -- (7.5,1); + +\node[draw, circle] (i) at (2,4.5) {22}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\node[draw, circle] (j) at (6,4.5) {17}; +\path[draw,thick,-] (j) -- (c); +\path[draw,thick,-] (j) -- (d); + +\node[draw, circle] (m) at (4,6.5) {39}; +\path[draw,thick,-] (m) -- (i); +\path[draw,thick,-] (m) -- (j); +\end{tikzpicture} +\end{center} +voi tallentaa taulukkoon seuraavasti ($N=8$): +\begin{center} +\begin{tikzpicture}[scale=0.7] +%\fill[color=lightgray] (3,0) rectangle (7,1); +\draw (0,0) grid (15,1); + +\node at (0.5,0.5) {$39$}; +\node at (1.5,0.5) {$22$}; +\node at (2.5,0.5) {$17$}; +\node at (3.5,0.5) {$13$}; +\node at (4.5,0.5) {$9$}; +\node at (5.5,0.5) {$9$}; +\node at (6.5,0.5) {$8$}; +\node at (7.5,0.5) {$5$}; +\node at (8.5,0.5) {$8$}; +\node at (9.5,0.5) {$6$}; +\node at (10.5,0.5) {$3$}; +\node at (11.5,0.5) {$2$}; +\node at (12.5,0.5) {$7$}; +\node at (13.5,0.5) {$2$}; +\node at (14.5,0.5) {$6$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\node at (8.5,1.4) {$9$}; +\node at (9.5,1.4) {$10$}; +\node at (10.5,1.4) {$11$}; +\node at (11.5,1.4) {$12$}; +\node at (12.5,1.4) {$13$}; +\node at (13.5,1.4) {$14$}; +\node at (14.5,1.4) {$15$}; +\end{tikzpicture} +\end{center} +Tätä tallennustapaa käyttäen kohdassa $k$ +olevalle solmulle pätee, että +\begin{itemize} +\item ylempi solmu on kohdassa $\lfloor k/2 \rfloor$, +\item vasen alempi solmu on kohdassa $2k$ ja +\item oikea alempi solmu on kohdassa $2k+1$. +\end{itemize} +Huomaa, että tämän seurauksena solmun kohta on parillinen, +jos se on vasemmalla ylemmästä solmusta katsoen, +ja pariton, jos se on oikealla. + +\subsubsection{Toteutus} + +Tarkastellaan seuraavaksi välikyselyn ja päivityksen +toteutusta segmenttipuuhun. +Seuraavat funktiot olettavat, että segmenttipuu +on tallennettu $2n-1$-kokoi\-seen taulukkoon $\texttt{p}$ +edellä kuvatulla tavalla. + +Funktio \texttt{summa} laskee summan +välillä $a \ldots b$: + +\begin{lstlisting} +int summa(int a, int b) { + a += N; b += N; + int s = 0; + while (a <= b) { + if (a%2 == 1) s += p[a++]; + if (b%2 == 0) s += p[b--]; + a /= 2; b /= 2; + } + return s; +} +\end{lstlisting} + +Funktio aloittaa summan laskeminen segmenttipuun +pohjalta ja liikkuu askel kerrallaan ylemmille tasoille. +Funktio laskee välin summan muuttujaan $s$ +yhdistämällä puussa olevia osasummia. +Välin reunalla oleva osasumma lisätään summaan +aina silloin, kun se ei kuulu ylemmän tason osasummaan. + +Funktio \texttt{lisaa} kasvattaa kohdan $k$ arvoa $x$:llä: + +\begin{lstlisting} +void lisaa(int k, int x) { + k += N; + p[k] += x; + for (k /= 2; k >= 1; k /= 2) { + p[k] = p[2*k]+p[2*k+1]; + } +} +\end{lstlisting} +Ensin funktio tekee muutoksen puun alimmalle +tasolle taulukkoon. +Tämän jälkeen se päivittää kaikki osasummat +puun huipulle asti. +Taulukon \texttt{p} indeksoinnin ansiosta +kohdasta $k$ alemmalla tasolla +ovat kohdat $2k$ ja $2k+1$. + +Molemmat segmenttipuun operaatiot toimivat ajassa +$O(\log n)$, koska $n$ lukua sisältävässä +segmenttipuussa on $O(\log n)$ tasoa +ja operaatiot siirtyvät askel kerrallaan +segmenttipuun tasoja ylöspäin. + +\subsubsection{Muut kyselyt} + +Segmenttipuu mahdollistaa summan lisäksi minkä +tahansa välikyselyn, +jossa vierekkäisten välien $[a,b]$ ja $[b+1,c]$ +tuloksista pystyy laskemaan tehokkaasti +välin $[a,c]$ tuloksen. +Tällaisia kyselyitä +ovat esimerkiksi minimi ja maksimi, +suurin yhteinen tekijä +sekä bittioperaatiot and, or ja xor. + +\begin{samepage} +Esimerkiksi seuraavan segmenttipuun avulla voi laskea +taulukon välien minimejä: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (8,1); + +\node[anchor=center] at (0.5, 0.5) {5}; +\node[anchor=center] at (1.5, 0.5) {8}; +\node[anchor=center] at (2.5, 0.5) {6}; +\node[anchor=center] at (3.5, 0.5) {3}; +\node[anchor=center] at (4.5, 0.5) {1}; +\node[anchor=center] at (5.5, 0.5) {7}; +\node[anchor=center] at (6.5, 0.5) {2}; +\node[anchor=center] at (7.5, 0.5) {6}; + +\node[draw, circle,minimum size=22pt] (a) at (1,2.5) {5}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,minimum size=22pt] (b) at (3,2.5) {3}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); +\node[draw, circle,minimum size=22pt] (c) at (5,2.5) {1}; +\path[draw,thick,-] (c) -- (4.5,1); +\path[draw,thick,-] (c) -- (5.5,1); +\node[draw, circle,minimum size=22pt] (d) at (7,2.5) {2}; +\path[draw,thick,-] (d) -- (6.5,1); +\path[draw,thick,-] (d) -- (7.5,1); + +\node[draw, circle,minimum size=22pt] (i) at (2,4.5) {3}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\node[draw, circle,minimum size=22pt] (j) at (6,4.5) {1}; +\path[draw,thick,-] (j) -- (c); +\path[draw,thick,-] (j) -- (d); + +\node[draw, circle,minimum size=22pt] (m) at (4,6.5) {1}; +\path[draw,thick,-] (m) -- (i); +\path[draw,thick,-] (m) -- (j); +\end{tikzpicture} +\end{center} +\end{samepage} + +Tässä segmenttipuussa jokainen puun solmu kertoo, +mikä on pienin luku sen alapuolella olevassa +taulukon osassa. +Segmenttipuun ylin luku on pienin luku +koko taulukon alueella. +Puun toteutus on samanlainen kuin summan laskemisessa, +mutta joka kohdassa pitää laskea summan sijasta +lukujen minimi. + +\subsubsection{Binäärihaku puussa} + +Segmenttipuun sisältämää tietoa voi käyttää +binäärihaun kaltaisesti aloittamalla +haun puun huipulta. +Näin on mahdollista selvittää esimerkiksi +minimisegmenttipuusta $O(\log n)$-ajassa, +missä kohdassa on taulukon pienin luku. + +Esimerkiksi seuraavassa puussa pienin +alkio on 1, jonka sijainti löytyy +kulkemalla puussa huipulta alaspäin: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (8,0) grid (16,1); + +\node[anchor=center] at (8.5, 0.5) {9}; +\node[anchor=center] at (9.5, 0.5) {5}; +\node[anchor=center] at (10.5, 0.5) {7}; +\node[anchor=center] at (11.5, 0.5) {1}; +\node[anchor=center] at (12.5, 0.5) {6}; +\node[anchor=center] at (13.5, 0.5) {2}; +\node[anchor=center] at (14.5, 0.5) {3}; +\node[anchor=center] at (15.5, 0.5) {2}; + +%\node[anchor=center] at (1,2.5) {13}; + +\node[draw, circle,minimum size=22pt] (e) at (9,2.5) {5}; +\path[draw,thick,-] (e) -- (8.5,1); +\path[draw,thick,-] (e) -- (9.5,1); +\node[draw, circle,minimum size=22pt] (f) at (11,2.5) {1}; +\path[draw,thick,-] (f) -- (10.5,1); +\path[draw,thick,-] (f) -- (11.5,1); +\node[draw, circle,minimum size=22pt] (g) at (13,2.5) {2}; +\path[draw,thick,-] (g) -- (12.5,1); +\path[draw,thick,-] (g) -- (13.5,1); +\node[draw, circle,minimum size=22pt] (h) at (15,2.5) {2}; +\path[draw,thick,-] (h) -- (14.5,1); +\path[draw,thick,-] (h) -- (15.5,1); + +\node[draw, circle,minimum size=22pt] (k) at (10,4.5) {1}; +\path[draw,thick,-] (k) -- (e); +\path[draw,thick,-] (k) -- (f); +\node[draw, circle,minimum size=22pt] (l) at (14,4.5) {2}; +\path[draw,thick,-] (l) -- (g); +\path[draw,thick,-] (l) -- (h); + +\node[draw, circle,minimum size=22pt] (n) at (12,6.5) {1}; +\path[draw,thick,-] (n) -- (k); +\path[draw,thick,-] (n) -- (l); + + +\path[draw=red,thick,->,line width=2pt] (n) -- (k); +\path[draw=red,thick,->,line width=2pt] (k) -- (f); +\path[draw=red,thick,->,line width=2pt] (f) -- (11.5,1); +\end{tikzpicture} +\end{center} + +\section{Lisätekniikoita} + +\subsubsection{Indeksien pakkaus} + +Taulukon päälle rakennettujen tietorakenteiden +rajoituksena on, että alkiot on indeksoitu +kokonaisluvuin $1,2,3,$ jne. +Tästä seuraa ongelmia, +jos tarvittavat indeksit ovat suuria. +Esimerkiksi indeksin $10^9$ käyttäminen +vaatisi, että taulukossa olisi $10^9$ alkiota, +mikä ei ole realistista. + +\index{indeksien pakkaus@indeksien pakkaus} + +Tätä rajoitusta on kuitenkin mahdollista +kiertää usein käyttämällä \key{indeksien pakkausta}, +jolloin indeksit jaetaan +uudestaan niin, että ne ovat +kokonaisluvut $1,2,3,$ jne. +Tämä on mahdollista silloin, kun kaikki +algoritmin aikana tarvittavat indeksit +ovat tiedossa algoritmin alussa. + +Ideana on korvata jokainen alkuperäinen +indeksi $x$ indeksillä $p(x)$, +missä $p$ jakaa indeksit uudestaan. +Vaatimuksena on, että indeksien järjestys +ei muutu, eli jos $a > k$ tuottaa puolestaan +luvun, jossa luvun $x$ bittejä +on siirretty $k$ askelta oikealle eli +luvun lopusta lähtee pois $k$ viimeistä bittiä. + +Esimerkiksi $14 < < 2 = 56$, +koska $14$ on bitteinä 1110, +josta tulee bittisiirron jälkeen 111000 eli $56$. +Vastaavasti $49 > > 3 = 6$, +koska $49$ on bitteinä 110001, +josta tulee bittisiirron jälkeen 110 eli $6$. + +Huomaa, että vasen bittisiirto $x < < k$ +vastaa luvun $x$ kertomista $2^k$:lla +ja oikea bittisiirto $x > > k$ +vastaa luvun $x$ jakamista $2^k$:lla +alaspäin pyöristäen. + +\subsubsection{Bittien käsittely} + +Luvun bitit indeksoidaan oikealta vasemmalle +nollasta alkaen. +Luvussa $1 < < k$ on yksi ykkösbitti +kohdassa $k$ ja kaikki muut bitit ovat nollia, joten sen avulla voi käsitellä +muiden lukujen yksittäisiä bittejä. + +Luvun $x$ bitti $k$ on ykkösbitti, jos +$x$ \& $(1 < < k) = (1 < < k)$. +Lauseke $x$ | $(1 < < k)$ asettaa luvun $x$ bitin $k$ +ykköseksi, lauseke +$x$ \& \textasciitilde $(1 < < k)$ +asettaa luvun $x$ bitin $k$ nollaksi ja +lauseke $x$ $\XOR$ $(1 < < k)$ +muuttaa luvun $x$ bitin $k$ käänteiseksi. +% +% Seuraava koodi muuttaa luvun bittejä: +% +% \begin{lstlisting} +% int x = 181; // 10110101 +% cout << (x|(1<<2)) << "\n"; // 181 = 10110101 +% cout << (x|(1<<3)) << "\n"; // 189 = 10111101 +% cout << (x&~(1<<2)) << "\n"; // 177 = 10110001 +% cout << (x&~(1<<3)) << "\n"; // 181 = 10110101 +% cout << (x^(1<<2)) << "\n"; // 177 = 10110001 +% cout << (x^(1<<3)) << "\n"; // 189 = 10111101 +% \end{lstlisting} +% +% % Bittiesityksen vasemmanpuoleisin bitti on eniten merkitsevä +% % (\textit{most significant}) ja +% % oikeanpuoleisin bitti on vähiten merkitsevä (\textit{least significant}). + +Lauseke $x$ \& $(x-1)$ muuttaa luvun $x$ viimeisen +ykkösbitin nollaksi, ja lauseke $x$ \& $-x$ nollaa +luvun $x$ kaikki bitit paitsi viimeisen ykkösbitin. +Lauseke $x$ | $(x-1)$ vuorostaan muuttaa kaikki +viimeisen ykkösbitin jälkeiset bitit ykkösiksi. + +Huomaa myös, että positiivinen luku $x$ on muotoa $2^k$, +jos $x$ \& $(x-1) = 0$. +% +% Seuraava koodi esittelee operaatioita: +% +% \begin{lstlisting} +% int x = 168; // 10101000 +% cout << (x&(x-1)) << "\n"; // 160 = 10100000 +% cout << (x&-x) << "\n"; // 8 = 00001000 +% cout << (x|(x-1)) << "\n"; // 175 = 10101111 +% \end{lstlisting} + +\subsubsection*{Lisäfunktiot} + +Kääntäjä g++ sisältää mm. seuraavat funktiot +bittien käsittelyyn: + +\begin{itemize} +\item +$\texttt{\_\_builtin\_clz}(x)$: +nollien määrä bittiesityksen alussa +\item +$\texttt{\_\_builtin\_ctz}(x)$: +nollien määrä bittiesityksen lopussa +\item +$\texttt{\_\_builtin\_popcount}(x)$: +ykkösten määrä bittiesityksessä +\item +$\texttt{\_\_builtin\_parity}(x)$: +ykkösten määrän parillisuus +\end{itemize} +\begin{samepage} +Nämä funktiot käsittelevät \texttt{int}-lukuja, +mutta funktioista on myös \texttt{long long} -versiot, +joiden lopussa on pääte \texttt{ll}. + +Seuraava koodi esittelee funktioiden käyttöä: + +\begin{lstlisting} +int x = 5328; // 00000000000000000001010011010000 +cout << __builtin_clz(x) << "\n"; // 19 +cout << __builtin_ctz(x) << "\n"; // 4 +cout << __builtin_popcount(x) << "\n"; // 5 +cout << __builtin_parity(x) << "\n"; // 1 +\end{lstlisting} +\end{samepage} + +\section{Joukon bittiesitys} + +Joukon $\{0,1,2,\ldots,n-1\}$ +jokaista osajoukkoa +vastaa $n$-bittinen luku, +jossa ykkösbitit ilmaisevat, +mitkä alkiot ovat mukana osajoukossa. +Esimerkiksi joukkoa $\{1,3,4,8\}$ +vastaa bittiesitys 100011010 eli luku +$2^8+2^4+2^3+2^1=282$. + +Joukon bittiesitys vie vähän muistia, +koska tieto kunkin alkion kuulumisesta +osajoukkoon vie vain yhden bitin tilaa. +Lisäksi bittimuodossa tallennettua joukkoa +on tehokasta käsitellä bittioperaatioilla. + +\subsubsection{Joukon käsittely} + +Seuraavan koodin muuttuja $x$ +sisältää joukon $\{0,1,2,\ldots,31\}$ +osajoukon. +Koodi lisää luvut 1, 3, 4 ja 8 +joukkoon ja tulostaa +joukon sisällön. + +\begin{lstlisting} +// x on tyhjä joukko +int x = 0; +// lisätään luvut 1, 3, 4 ja 8 joukkoon +x |= (1<<1); +x |= (1<<3); +x |= (1<<4); +x |= (1<<8); +// tulostetaan joukon sisältö +for (int i = 0; i < 32; i++) { + if (x&(1< 1 && (b&(1<,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (4); +\path[draw,thick,->,>=latex] (2) -- (5); +\path[draw,thick,->,>=latex] (4) -- (5); +\path[draw,thick,->,>=latex] (4) -- (1); +\path[draw,thick,->,>=latex] (3) -- (1); +\end{tikzpicture} +\end{center} + +Yllä olevassa verkossa on polku solmusta +3 solmuun 5, joka kulkee kaaria +$3 \rightarrow 1 \rightarrow 2 \rightarrow 5$. +Sen sijaan verkossa ei ole polkua +solmusta 5 solmuun 3. + + +\index{sykli@sykli} +\index{syklitzn verkko@syklitön verkko} + +\key{Sykli} on polku, jonka ensimmäinen +ja viimeinen solmu on sama. +Esimerkiksi yllä olevassa verkossa on sykli +$1 \rightarrow 2 \rightarrow 4 \rightarrow 1$. +Jos verkossa ei ole yhtään sykliä, se on \key{syklitön}. + +\subsubsection{Kaarten painot} + +\index{painotettu verkko@painotettu verkko} + +\key{Painotetussa} verkossa +jokaiseen kaareen liittyy \key{paino}. +Usein painot kuvaavat kaarien pituuksia. +Esimerkiksi seuraava verkko on painotettu: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$1$}; +\node[draw, circle] (2) at (4,3) {$2$}; +\node[draw, circle] (3) at (1,1) {$3$}; +\node[draw, circle] (4) at (4,1) {$4$}; +\node[draw, circle] (5) at (6,2) {$5$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:5] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:1] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:7] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:7] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:3] {} (5); +\end{tikzpicture} +\end{center} + +Nyt polun pituus on +polulla olevien kaarten painojen summa. +Esimerkiksi yllä olevassa verkossa +polun $1 \rightarrow 2 \rightarrow 5$ +pituus on $5+7=12$ ja polun +$1 \rightarrow 3 \rightarrow 4 \rightarrow 5$ pituus on $1+7+3=11$. +Jälkimmäinen on lyhin polku solmusta 1 solmuun 5. + +\subsubsection{Naapurit ja asteet} + +\index{naapuri@naapuri} +\index{aste@aste} + +Kaksi solmua ovat \key{naapureita}, +jos ne ovat verkossa +\key{vierekkäin} eli niiden välillä on kaari. +Solmun \key{aste} on +sen naapurien määrä. +Esimerkiksi seuraavassa verkossa +solmun 2 naapurit ovat 1, 4 ja 5, +joten sen aste on 3. + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$1$}; +\node[draw, circle] (2) at (4,3) {$2$}; +\node[draw, circle] (3) at (1,1) {$3$}; +\node[draw, circle] (4) at (4,1) {$4$}; +\node[draw, circle] (5) at (6,2) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (2) -- (4); +\path[draw,thick,-] (2) -- (5); +%\path[draw,thick,-] (4) -- (5); +\end{tikzpicture} +\end{center} + +Verkon solmujen asteiden summa on aina $2m$, +missä $m$ on kaarten määrä. +Tämä johtuu siitä, että jokainen kaari lisää +kahden solmun astetta yhdellä. +Niinpä solmujen asteiden summa on aina parillinen. + +\index{szznnollinen verkko@säännöllinen verkko} +\index{tzydellinen verkko@täydellinen verkko} + +Verkko on \key{säännöllinen}, +jos jokaisen solmun aste on vakio $d$. +Verkko on \key{täydellinen}, +jos jokaisen solmun aste on $n-1$ eli +verkossa on kaikki mahdolliset kaaret +solmujen välillä. + +\index{lzhtzaste@lähtöaste} +\index{tuloaste@tuloaste} + +Suunnatussa verkossa \key{lähtöaste} +on solmusta lähtevien kaarten määrä ja +\key{tuloaste} on solmuun tulevien +kaarten määrä. +Esimerkiksi seuraavassa verkossa solmun 2 +lähtöaste on 1 ja tuloaste on 2. + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$1$}; +\node[draw, circle] (2) at (4,3) {$2$}; +\node[draw, circle] (3) at (1,1) {$3$}; +\node[draw, circle] (4) at (4,1) {$4$}; +\node[draw, circle] (5) at (6,2) {$5$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (1) -- (3); +\path[draw,thick,->,>=latex] (1) -- (4); +\path[draw,thick,->,>=latex] (3) -- (4); +\path[draw,thick,->,>=latex] (2) -- (4); +\path[draw,thick,<-,>=latex] (2) -- (5); +\end{tikzpicture} +\end{center} + +\subsubsection{Väritykset} + +\index{vxritys@väritys} +\index{kaksijakoinen verkko@kaksijakoinen verkko} + +Verkon \key{värityksessä} jokaiselle solmulle valitaan +tietty väri niin, että millään kahdella +vierekkäisellä solmulla ei ole samaa väriä. + +Verkko on \key{kaksijakoinen}, +jos se on mahdollista värittää kahdella värillä. +Osoittautuu, että verkko on kaksijakoinen tarkalleen silloin, +kun siinä ei ole sykliä, johon kuuluu +pariton määrä solmuja. +Esimerkiksi verkko +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$2$}; +\node[draw, circle] (2) at (4,3) {$3$}; +\node[draw, circle] (3) at (1,1) {$5$}; +\node[draw, circle] (4) at (4,1) {$6$}; +\node[draw, circle] (5) at (-2,1) {$4$}; +\node[draw, circle] (6) at (-2,3) {$1$}; +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (2) -- (4); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (5) -- (6); +\end{tikzpicture} +\end{center} +on kaksijakoinen, koska sen voi värittää seuraavasti: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle, fill=blue!40] (1) at (1,3) {$2$}; +\node[draw, circle, fill=red!40] (2) at (4,3) {$3$}; +\node[draw, circle, fill=red!40] (3) at (1,1) {$5$}; +\node[draw, circle, fill=blue!40] (4) at (4,1) {$6$}; +\node[draw, circle, fill=red!40] (5) at (-2,1) {$4$}; +\node[draw, circle, fill=blue!40] (6) at (-2,3) {$1$}; +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (2) -- (4); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (5) -- (6); +\end{tikzpicture} +\end{center} + +\subsubsection{Yksinkertaisuus} + +\index{yksinkertainen verkko@yksinkertainen verkko} + +Verkko on \key{yksinkertainen}, +jos mistään solmusta ei ole kaarta itseensä +eikä minkään kahden solmun välillä ole +monta kaarta samaan suuntaan. +Usein oletuksena on, että verkko on yksinkertainen. +Esimerkiksi verkko +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$2$}; +\node[draw, circle] (2) at (4,3) {$3$}; +\node[draw, circle] (3) at (1,1) {$5$}; +\node[draw, circle] (4) at (4,1) {$6$}; +\node[draw, circle] (5) at (-2,1) {$4$}; +\node[draw, circle] (6) at (-2,3) {$1$}; + +\path[draw,thick,-] (1) edge [bend right=20] (2); +\path[draw,thick,-] (2) edge [bend right=20] (1); +%\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (2) -- (4); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (5) -- (6); + +\tikzset{every loop/.style={in=135,out=190}} +\path[draw,thick,-] (5) edge [loop left] (5); +\end{tikzpicture} +\end{center} +\emph{ei} ole yksinkertainen, koska solmusta 4 on kaari itseensä +ja solmujen 2 ja 3 välillä on kaksi kaarta. + +\section{Verkko muistissa} + +On monia tapoja pitää verkkoa muistissa algoritmissa. +Sopiva tietorakenne riippuu siitä, +kuinka suuri verkko on ja +millä tavoin algoritmi käsittelee sitä. +Seuraavaksi käymme läpi kolme tavallista vaihtoehtoa. + +\subsubsection{Vieruslistaesitys} + +\index{vieruslista@vieruslista} + +Tavallisin tapa pitää verkkoa muistissa on +luoda jokaisesta solmusta \key{vieruslista}, +joka sisältää kaikki solmut, +joihin solmusta pystyy siirtymään kaarta pitkin. +Vieruslistaesitys on tavallisin verkon esitysmuoto, ja +useimmat algoritmit pystyy toteuttamaan +tehokkaasti sitä käyttäen. + +Kätevä tapa tallentaa verkon vieruslistaesitys on luoda taulukko, +jossa jokainen alkio on vektori: +\begin{lstlisting} +vector v[N]; +\end{lstlisting} + +Taulukossa solmun $s$ vieruslista on kohdassa $\texttt{v}[s]$. +Vakio $N$ on valittu niin suureksi, +että kaikki vieruslistat mahtuvat taulukkoon. +Esimerkiksi verkon + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (3,1) {$4$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (2) -- (4); +\path[draw,thick,->,>=latex] (3) -- (4); +\path[draw,thick,->,>=latex] (4) -- (1); +\end{tikzpicture} +\end{center} +voi tallentaa seuraavasti: +\begin{lstlisting} +v[1].push_back(2); +v[2].push_back(3); +v[2].push_back(4); +v[3].push_back(4); +v[4].push_back(1); +\end{lstlisting} + +Jos verkko on suuntaamaton, sen voi tallentaa samalla tavalla, +mutta silloin jokainen kaari lisätään kumpaankin suuntaan. + +Painotetun verkon tapauksessa rakennetta voi laajentaa näin: + +\begin{lstlisting} +vector> v[N]; +\end{lstlisting} + +Nyt vieruslistalla on pareja, joiden ensimmäinen kenttä on +kaaren kohdesolmu ja toinen kenttä on kaaren paino. +Esimerkiksi verkon + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (3,1) {$4$}; + +\path[draw,thick,->,>=latex] (1) -- node[font=\small,label=above:5] {} (2); +\path[draw,thick,->,>=latex] (2) -- node[font=\small,label=above:7] {} (3); +\path[draw,thick,->,>=latex] (2) -- node[font=\small,label=left:6] {} (4); +\path[draw,thick,->,>=latex] (3) -- node[font=\small,label=right:5] {} (4); +\path[draw,thick,->,>=latex] (4) -- node[font=\small,label=left:2] {} (1); +\end{tikzpicture} +\end{center} +voi tallentaa seuraavasti: +\begin{lstlisting} +v[1].push_back({2,5}); +v[2].push_back({3,7}); +v[2].push_back({4,6}); +v[3].push_back({4,5}); +v[4].push_back({1,2}); +\end{lstlisting} + +Vieruslistaesityksen etuna on, että sen avulla on nopeaa selvittää, +mihin solmuihin tietystä solmusta pääsee kulkemaan. +Esimerkiksi seuraava silmukka käy läpi kaikki solmut, +joihin pääsee solmusta $s$: + +\begin{lstlisting} +for (auto u : v[s]) { + // käsittele solmu u +} +\end{lstlisting} + +\subsubsection{Vierusmatriisiesitys} + +\index{vierusmatriisi@vierusmatriisi} + +\key{Vierusmatriisi} on kaksiulotteinen taulukko, +joka kertoo jokaisesta mahdollisesta kaaresta, +onko se mukana verkossa. +Vierusmatriisista on nopeaa tarkistaa, +onko kahden solmun välillä kaari. +Toisaalta matriisi vie paljon tilaa, +jos verkko on suuri. +Vierusmatriisi tallennetaan taulukkona + +\begin{lstlisting} +int v[N][N]; +\end{lstlisting} + +jossa arvo $\texttt{v}[a][b]$ ilmaisee, +onko kaari solmusta $a$ solmuun $b$ mukana verkossa. +Jos kaari on mukana verkossa, +niin $\texttt{v}[a][b]=1$, +ja muussa tapauksessa $\texttt{v}[a][b]=0$. +Nyt esimerkiksi verkkoa +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (3,1) {$4$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (2) -- (4); +\path[draw,thick,->,>=latex] (3) -- (4); +\path[draw,thick,->,>=latex] (4) -- (1); +\end{tikzpicture} +\end{center} +vastaa seuraava vierusmatriisi: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (4,4); +\node at (0.5,0.5) {1}; +\node at (1.5,0.5) {0}; +\node at (2.5,0.5) {0}; +\node at (3.5,0.5) {0}; +\node at (0.5,1.5) {0}; +\node at (1.5,1.5) {0}; +\node at (2.5,1.5) {0}; +\node at (3.5,1.5) {1}; +\node at (0.5,2.5) {0}; +\node at (1.5,2.5) {0}; +\node at (2.5,2.5) {1}; +\node at (3.5,2.5) {1}; +\node at (0.5,3.5) {0}; +\node at (1.5,3.5) {1}; +\node at (2.5,3.5) {0}; +\node at (3.5,3.5) {0}; +\node at (-0.5,0.5) {4}; +\node at (-0.5,1.5) {3}; +\node at (-0.5,2.5) {2}; +\node at (-0.5,3.5) {1}; +\node at (0.5,4.5) {1}; +\node at (1.5,4.5) {2}; +\node at (2.5,4.5) {3}; +\node at (3.5,4.5) {4}; +\end{tikzpicture} +\end{center} + +Jos verkko on painotettu, vierusmatriisiesitystä voi +laajentaa luontevasti niin, että matriisissa kerrotaan +kaaren paino, jos kaari on olemassa. +Tätä esitystapaa käyttäen esimerkiksi verkkoa + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (3,1) {$4$}; + +\path[draw,thick,->,>=latex] (1) -- node[font=\small,label=above:5] {} (2); +\path[draw,thick,->,>=latex] (2) -- node[font=\small,label=above:7] {} (3); +\path[draw,thick,->,>=latex] (2) -- node[font=\small,label=left:6] {} (4); +\path[draw,thick,->,>=latex] (3) -- node[font=\small,label=right:5] {} (4); +\path[draw,thick,->,>=latex] (4) -- node[font=\small,label=left:2] {} (1); +\end{tikzpicture} +\end{center} +\begin{samepage} +vastaa seuraava vierusmatriisi: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (4,4); +\node at (0.5,0.5) {2}; +\node at (1.5,0.5) {0}; +\node at (2.5,0.5) {0}; +\node at (3.5,0.5) {0}; +\node at (0.5,1.5) {0}; +\node at (1.5,1.5) {0}; +\node at (2.5,1.5) {0}; +\node at (3.5,1.5) {5}; +\node at (0.5,2.5) {0}; +\node at (1.5,2.5) {0}; +\node at (2.5,2.5) {7}; +\node at (3.5,2.5) {6}; +\node at (0.5,3.5) {0}; +\node at (1.5,3.5) {5}; +\node at (2.5,3.5) {0}; +\node at (3.5,3.5) {0}; +\node at (-0.5,0.5) {4}; +\node at (-0.5,1.5) {3}; +\node at (-0.5,2.5) {2}; +\node at (-0.5,3.5) {1}; +\node at (0.5,4.5) {1}; +\node at (1.5,4.5) {2}; +\node at (2.5,4.5) {3}; +\node at (3.5,4.5) {4}; +\end{tikzpicture} +\end{center} +\end{samepage} + +\subsubsection{Kaarilistaesitys} + +\index{kaarilista@kaarilista} + +\key{Kaarilista} sisältää kaikki verkon kaaret. +Kaarilista on hyvä tapa tallentaa verkko, +jos algoritmissa täytyy käydä läpi +kaikki verkon kaaret eikä ole tarvetta +etsiä kaarta alkusolmun perusteella. + +Kaarilistan voi tallentaa vektoriin +\begin{lstlisting} +vector> v; +\end{lstlisting} +jossa jokaisessa solmussa on parina kaaren +alku- ja loppusolmu. +Tällöin esimerkiksi verkon + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (3,1) {$4$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (2) -- (4); +\path[draw,thick,->,>=latex] (3) -- (4); +\path[draw,thick,->,>=latex] (4) -- (1); +\end{tikzpicture} +\end{center} +voi tallentaa seuraavasti: +\begin{lstlisting} +v.push_back({1,2}); +v.push_back({2,3}); +v.push_back({2,4}); +v.push_back({3,4}); +v.push_back({4,1}); +\end{lstlisting} + +\noindent +Painotetun verkon tapauksessa rakennetta voi laajentaa +esimerkiksi näin: +\begin{lstlisting} +vector,int>> v; +\end{lstlisting} +Nyt listalla on pareja, joiden ensimmäinen jäsen +sisältää parina kaaren alku- ja loppusolmun, +ja toinen jäsen on kaaren paino. +Esimerkiksi verkon + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (3,1) {$4$}; + +\path[draw,thick,->,>=latex] (1) -- node[font=\small,label=above:5] {} (2); +\path[draw,thick,->,>=latex] (2) -- node[font=\small,label=above:7] {} (3); +\path[draw,thick,->,>=latex] (2) -- node[font=\small,label=left:6] {} (4); +\path[draw,thick,->,>=latex] (3) -- node[font=\small,label=right:5] {} (4); +\path[draw,thick,->,>=latex] (4) -- node[font=\small,label=left:2] {} (1); +\end{tikzpicture} +\end{center} +\begin{samepage} +voi tallentaa seuraavasti: +\begin{lstlisting} +v.push_back({{1,2},5}); +v.push_back({{2,3},7}); +v.push_back({{2,4},6}); +v.push_back({{3,4},5}); +v.push_back({{4,1},2}); +\end{lstlisting} +\end{samepage} \ No newline at end of file diff --git a/luku12.tex b/luku12.tex new file mode 100644 index 0000000..829f7fd --- /dev/null +++ b/luku12.tex @@ -0,0 +1,640 @@ +\chapter{Graph search} + +Tässä luvussa tutustumme +syvyyshakuun ja leveyshakuun, jotka +ovat keskeisiä menetelmiä verkon läpikäyntiin. +Molemmat algoritmit lähtevät liikkeelle +tietystä alkusolmusta ja +käyvät läpi kaikki solmut, +joihin alkusolmusta pääsee. +Algoritmien erona on, +missä järjestyksessä ne kulkevat verkossa. + +\section{Syvyyshaku} + +\index{syvyyshaku@syvyyshaku} + +\key{Syvyyshaku} +on suoraviivainen menetelmä verkon läpikäyntiin. +Algoritmi lähtee liikkeelle tietystä +verkon solmusta ja etenee siitä +kaikkiin solmuihin, jotka ovat +saavutettavissa kaaria kulkemalla. + +Syvyyshaku etenee verkossa syvyyssuuntaisesti +eli kulkee eteenpäin verkossa niin kauan +kuin vastaan tulee uusia solmuja. +Tämän jälkeen haku perääntyy kokeilemaan +muita suuntia. +Algoritmi pitää kirjaa vierailemistaan solmuista, +jotta se käsittelee kunkin solmun vain kerran. + +\subsubsection*{Esimerkki} + +Tarkastellaan syvyyshaun toimintaa +seuraavassa verkossa: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); +\end{tikzpicture} +\end{center} +Syvyyshaku voi lähteä liikkeelle +mistä tahansa solmusta, +mutta oletetaan nyt, +että haku lähtee liikkeelle solmusta 1. + +Solmun 1 naapurit ovat solmut 2 ja 4, +joista haku etenee ensin solmuun 2: +\begin{center} +\begin{tikzpicture} +\node[draw, circle,fill=lightgray] (1) at (1,5) {$1$}; +\node[draw, circle,fill=lightgray] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); + +\path[draw=red,thick,->,line width=2pt] (1) -- (2); +\end{tikzpicture} +\end{center} +Tämän jälkeen haku etenee vastaavasti +solmuihin 3 ja 5: +\begin{center} +\begin{tikzpicture} +\node[draw, circle,fill=lightgray] (1) at (1,5) {$1$}; +\node[draw, circle,fill=lightgray] (2) at (3,5) {$2$}; +\node[draw, circle,fill=lightgray] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle,fill=lightgray] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); + +\path[draw=red,thick,->,line width=2pt] (1) -- (2); +\path[draw=red,thick,->,line width=2pt] (2) -- (3); +\path[draw=red,thick,->,line width=2pt] (3) -- (5); +\end{tikzpicture} +\end{center} +Solmun 5 naapurit ovat 2 ja 3, +mutta haku on käynyt jo molemmissa, +joten on aika peruuttaa taaksepäin. +Myös solmujen 3 ja 2 naapurit on käyty, +joten haku peruuttaa solmuun 1 asti. +Siitä lähtee kaari, josta pääsee +solmuun 4: +\begin{center} +\begin{tikzpicture} +\node[draw, circle,fill=lightgray] (1) at (1,5) {$1$}; +\node[draw, circle,fill=lightgray] (2) at (3,5) {$2$}; +\node[draw, circle,fill=lightgray] (3) at (5,4) {$3$}; +\node[draw, circle,fill=lightgray] (4) at (1,3) {$4$}; +\node[draw, circle,fill=lightgray] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); + +\path[draw=red,thick,->,line width=2pt] (1) -- (4); +\end{tikzpicture} +\end{center} +Tämän jälkeen haku päättyy, +koska se on käynyt kaikissa solmuissa. + +Syvyyshaun aikavaativuus on $O(n+m)$, +missä $n$ on solmujen määrä ja $m$ on kaarten määrä, +koska haku käsittelee kerran jokaisen solmun ja kaaren. + +\subsubsection*{Toteutus} + +Syvyyshaku on yleensä mukavinta toteuttaa +rekursiolla. +Seuraava funktio \texttt{haku} +suorittaa syvyyshaun sille parametrina +annetusta solmusta lähtien. +Funktio olettaa, että +verkko on tallennettu vieruslistoina +taulukkoon +\begin{lstlisting} +vector v[N]; +\end{lstlisting} +ja pitää lisäksi yllä taulukkoa +\begin{lstlisting} +int z[N]; +\end{lstlisting} +joka kertoo, missä solmuissa haku on käynyt. +Alussa taulukon jokainen arvo on 0, +ja kun haku saapuu solmuun $s$, +kohtaan \texttt{z}[$s$] merkitään 1. +Funktion toteutus on seuraavanlainen: +\begin{lstlisting} +void haku(int s) { + if (z[s]) return; + z[s] = 1; + // solmun s käsittely tähän + for (auto u: v[s]) { + haku(u); + } +} +\end{lstlisting} + +\section{Leveyshaku} + +\index{leveyshaku@leveyshaku} + +\key{Leveyshaku} +käy solmut läpi järjestyksessä sen mukaan, +kuinka kaukana ne ovat alkusolmusta. +Niinpä leveyshaun avulla pystyy laskemaan +etäisyyden alkusolmusta kaikkiin +muihin solmuihin. +Leveyshaku on kuitenkin vaikeampi +toteuttaa kuin syvyyshaku. + +Leveyshakua voi ajatella niin, +että se käy solmuja läpi kerros kerrallaan. +Ensin haku käy läpi solmut, +joihin pääsee yhdellä kaarella +alkusolmusta. +Tämän jälkeen vuorossa ovat +solmut, joihin pääsee kahdella +kaarella alkusolmusta, jne. +Sama jatkuu, kunnes uusia käsiteltäviä +solmuja ei enää ole. + +\subsubsection*{Esimerkki} + +Tarkastellaan leveyshaun toimintaa +seuraavassa verkossa: + +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,5) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; +\node[draw, circle] (6) at (5,3) {$6$}; + + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (5) -- (6); +\end{tikzpicture} +\end{center} +Oletetaan jälleen, +että haku alkaa solmusta 1. +Haku etenee ensin kaikkiin solmuihin, +joihin pääsee alkusolmusta: +\\ +\begin{center} +\begin{tikzpicture} +\node[draw, circle,fill=lightgray] (1) at (1,5) {$1$}; +\node[draw, circle,fill=lightgray] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,5) {$3$}; +\node[draw, circle,fill=lightgray] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; +\node[draw, circle] (6) at (5,3) {$6$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (5) -- (6); + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (2) -- (5); + +\path[draw=red,thick,->,line width=2pt] (1) -- (2); +\path[draw=red,thick,->,line width=2pt] (1) -- (4); +\end{tikzpicture} +\end{center} +Seuraavaksi haku etenee solmuihin 3 ja 5: +\\ +\begin{center} +\begin{tikzpicture} +\node[draw, circle,fill=lightgray] (1) at (1,5) {$1$}; +\node[draw, circle,fill=lightgray] (2) at (3,5) {$2$}; +\node[draw, circle,fill=lightgray] (3) at (5,5) {$3$}; +\node[draw, circle,fill=lightgray] (4) at (1,3) {$4$}; +\node[draw, circle,fill=lightgray] (5) at (3,3) {$5$}; +\node[draw, circle] (6) at (5,3) {$6$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (5) -- (6); + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (2) -- (5); + +\path[draw=red,thick,->,line width=2pt] (2) -- (3); +\path[draw=red,thick,->,line width=2pt] (2) -- (5); +\end{tikzpicture} +\end{center} +Viimeisenä haku etenee solmuun 6: +\\ +\begin{center} +\begin{tikzpicture} +\node[draw, circle,fill=lightgray] (1) at (1,5) {$1$}; +\node[draw, circle,fill=lightgray] (2) at (3,5) {$2$}; +\node[draw, circle,fill=lightgray] (3) at (5,5) {$3$}; +\node[draw, circle,fill=lightgray] (4) at (1,3) {$4$}; +\node[draw, circle,fill=lightgray] (5) at (3,3) {$5$}; +\node[draw, circle,fill=lightgray] (6) at (5,3) {$6$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (5) -- (6); + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (2) -- (5); + +\path[draw=red,thick,->,line width=2pt] (3) -- (6); +\path[draw=red,thick,->,line width=2pt] (5) -- (6); +\end{tikzpicture} +\end{center} +Leveyshaun tuloksena selviää etäisyys +kuhunkin verkon solmuun alkusolmusta. +Etäisyys on sama kuin kerros, +jossa solmu käsiteltiin haun aikana: + +\begin{tabular}{ll} +\\ +solmu & etäisyys \\ +\hline +1 & 0 \\ +2 & 1 \\ +3 & 2 \\ +4 & 1 \\ +5 & 2 \\ +6 & 3 \\ +\\ +\end{tabular} + +Leveyshaun aikavaativuus on syvyyshaun tavoin $O(n+m)$, +missä $n$ on solmujen määrä ja $m$ on kaarten määrä. + +\subsubsection*{Toteutus} + +Leveyshaku on syvyyshakua hankalampi toteuttaa, +koska haku käy läpi solmuja verkon eri +puolilta niiden etäisyyden mukaan. +Tyypillinen toteutus on pitää yllä jonoa +käsiteltävistä solmuista. +Joka askeleella otetaan käsittelyyn seuraava +solmu jonosta ja uudet solmut lisätään +jonon perälle. + +Seuraava koodi toteuttaa leveyshaun +solmusta $x$ lähtien. +Koodi olettaa, että verkko on tallennettu +vieruslistoina, ja pitää yllä jonoa +\begin{lstlisting} +queue q; +\end{lstlisting} +joka sisältää solmut käsittelyjärjestyksessä. +Koodi lisää aina uudet vastaan tulevat solmut +jonon perään ja ottaa seuraavaksi käsiteltävän +solmun jonon alusta, +minkä ansiosta solmut käsitellään +kerroksittain alkusolmusta lähtien. + +Lisäksi koodi käyttää taulukoita +\begin{lstlisting} +int z[N], e[N]; +\end{lstlisting} +niin, että taulukko \texttt{z} sisältää tiedon, +missä solmuissa haku on käynyt, +ja taulukkoon \texttt{e} lasketaan lyhin +etäisyys alkusolmusta kaikkiin verkon solmuihin. +Toteutuksesta tulee seuraavanlainen: +\begin{lstlisting} +z[s] = 1; e[x] = 0; +q.push(x); +while (!q.empty()) { + int s = q.front(); q.pop(); + // solmun s käsittely tähän + for (auto u : v[s]) { + if (z[u]) continue; + z[u] = 1; e[u] = e[s]+1; + q.push(u); + } +} +\end{lstlisting} + +\section{Sovelluksia} + +Verkon läpikäynnin avulla +saa selville monia asioita +verkon rakenteesta. +Läpikäynnin voi yleensä aina toteuttaa +joko syvyyshaulla tai leveyshaulla, +mutta käytännössä syvyyshaku on parempi valinta, +koska sen toteutus on helpompi. +Oletamme seuraavaksi, että käsiteltävänä on +suuntaamaton verkko. + +\subsubsection{Yhtenäisyyden tarkastaminen} + +\index{yhtenxisyys@yhtenäisyys} + +Verkko on yhtenäinen, +jos mistä tahansa solmuista +pääsee kaikkiin muihin solmuihin. +Niinpä verkon yhtenäisyys selviää +aloittamalla läpikäynti +jostakin verkon solmusta ja +tarkastamalla, pääseekö siitä kaikkiin solmuihin. + +Esimerkiksi verkossa +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (2) at (7,5) {$2$}; +\node[draw, circle] (1) at (3,5) {$1$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (5) at (7,3) {$5$}; +\node[draw, circle] (4) at (3,3) {$4$}; + +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (2) -- (5); +\end{tikzpicture} +\end{center} +solmusta $1$ alkava syvyyshaku löytää seuraavat +solmut: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (2) at (7,5) {$2$}; +\node[draw, circle,fill=lightgray] (1) at (3,5) {$1$}; +\node[draw, circle,fill=lightgray] (3) at (5,4) {$3$}; +\node[draw, circle] (5) at (7,3) {$5$}; +\node[draw, circle,fill=lightgray] (4) at (3,3) {$4$}; + +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (2) -- (5); + +\path[draw=red,thick,->,line width=2pt] (1) -- (3); +\path[draw=red,thick,->,line width=2pt] (3) -- (4); + +\end{tikzpicture} +\end{center} + +Koska syvyyshaku ei pääse kaikkiin solmuihin, +tämä tarkoittaa, että verkko ei ole yhtenäinen. +Vastaavalla tavalla voi etsiä myös verkon komponentit +käymällä solmut läpi ja aloittamalla uuden syvyyshaun +aina, jos käsiteltävä solmu ei kuulu vielä mihinkään komponenttiin. + +\subsubsection{Syklin etsiminen} + +\index{sykli@sykli} + +Verkossa on sykli, +jos jonkin komponentin läpikäynnin +aikana tulee vastaan solmu, +jonka naapuri on jo käsitelty +ja solmuun ei ole saavuttu kyseisen naapurin kautta. +Esimerkiksi verkossa +\begin{center} +\begin{tikzpicture} +\node[draw, circle,fill=lightgray] (2) at (7,5) {$2$}; +\node[draw, circle,fill=lightgray] (1) at (3,5) {$1$}; +\node[draw, circle,fill=lightgray] (3) at (5,4) {$3$}; +\node[draw, circle,fill=lightgray] (5) at (7,3) {$5$}; +\node[draw, circle] (4) at (3,3) {$4$}; + +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (3) -- (5); + +\path[draw=red,thick,->,line width=2pt] (1) -- (3); +\path[draw=red,thick,->,line width=2pt] (3) -- (2); +\path[draw=red,thick,->,line width=2pt] (2) -- (5); + +\end{tikzpicture} +\end{center} +on sykli, koska tultaessa solmusta 2 solmuun 5 +havaitaan, että naapurina oleva solmu 3 on jo käsitelty. +Niinpä verkossa täytyy olla solmun 3 kautta +kulkeva sykli. +Tällainen sykli on esimerkiksi +$3 \rightarrow 2 \rightarrow 5 \rightarrow 3$. + +Syklin olemassaolon voi myös päätellä laskemalla, +montako solmua ja kaarta komponentissa on. +Jos komponentissa on $c$ solmua ja siinä ei ole sykliä, +niin siinä on oltava tarkalleen $c-1$ kaarta. +Jos kaaria on $c$ tai enemmän, niin komponentissa +on varmasti sykli. + +\subsubsection{Kaksijakoisuuden tarkastaminen} + +\index{kaksijakoisuus@kaksijakoisuus} + +Verkko on kaksijakoinen, +jos sen solmut voi värittää +kahdella värillä +niin, että kahta samanväristä +solmua ei ole vierekkäin. +On yllättävän helppoa selvittää +verkon läpikäynnin avulla, +onko verkko kaksijakoinen. + +Ideana on värittää alkusolmu +siniseksi, sen kaikki naapurit +punaiseksi, niiden kaikki naapurit +siniseksi, jne. +Jos jossain vaiheessa +ilmenee ristiriita +(saman solmun tulisi olla sekä +sininen että punainen), +verkko ei ole kaksijakoinen. +Muuten verkko on kaksijakoinen +ja yksi väritys on muodostunut. + +Esimerkiksi verkko +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (2) at (5,5) {$2$}; +\node[draw, circle] (1) at (3,5) {$1$}; +\node[draw, circle] (3) at (7,4) {$3$}; +\node[draw, circle] (5) at (5,3) {$5$}; +\node[draw, circle] (4) at (3,3) {$4$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (5) -- (4); +\path[draw,thick,-] (4) -- (1); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (5) -- (3); +\end{tikzpicture} +\end{center} +ei ole kaksijakoinen, koska +läpikäynti solmusta 1 alkaen +aiheuttaa seuraavan ristiriidan: +\begin{center} +\begin{tikzpicture} +\node[draw, circle,fill=red!40] (2) at (5,5) {$2$}; +\node[draw, circle,fill=blue!40] (1) at (3,5) {$1$}; +\node[draw, circle,fill=blue!40] (3) at (7,4) {$3$}; +\node[draw, circle,fill=red!40] (5) at (5,3) {$5$}; +\node[draw, circle] (4) at (3,3) {$4$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (5) -- (4); +\path[draw,thick,-] (4) -- (1); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (5) -- (3); + +\path[draw=red,thick,->,line width=2pt] (1) -- (2); +\path[draw=red,thick,->,line width=2pt] (2) -- (3); +\path[draw=red,thick,->,line width=2pt] (3) -- (5); +\path[draw=red,thick,->,line width=2pt] (5) -- (2); +\end{tikzpicture} +\end{center} +Tässä vaiheessa havaitaan, +että sekä solmun 2 että solmun 5 väri on punainen, +vaikka solmut ovat vierekkäin verkossa, +joten verkko ei ole kaksijakoinen. + +Tämä algoritmi on luotettava tapa selvittää +verkon kaksijakoisuus, +koska kun värejä on vain kaksi, +ensimmäisen solmun värin valinta +määrittää kaikkien muiden +samassa komponentissa olevien +solmujen värin. +Ei ole merkitystä, +kumman värin ensimmäinen +solmu saa. + +Huomaa, että yleensä ottaen on vaikeaa +selvittää, voiko verkon solmut +värittää $k$ värillä niin, +ettei missään kohtaa ole vierekkäin +kahta samanväristä solmua. +Edes tapaukseen $k=3$ ei tunneta +mitään tehokasta algoritmia, +vaan kyseessä on NP-vaikea ongelma. +% +% \section{Labyrintin käsittely} +% +% Labyrintti on ruudukko, joka muodostuu lattia- ja seinäruuduista, +% ja labyrintissa on sallittua kulkea lattiaruutuja pitkin. +% Labyrinttia +% \begin{center} +% \begin{tikzpicture}[scale=0.7] +% \fill[color=gray] (0,0) rectangle (8,1); +% \fill[color=gray] (0,5) rectangle (8,6); +% \fill[color=gray] (0,0) rectangle (1,6); +% \fill[color=gray] (7,0) rectangle (8,6); +% +% \fill[color=gray] (2,0) rectangle (3,4); +% \fill[color=gray] (4,2) rectangle (6,4); +% +% \draw (0,0) grid (8,6); +% +% \node at (1.5,1.5) {$a$}; +% \node at (6.5,3.5) {$b$}; +% \end{tikzpicture} +% \end{center} +% vastaa luontevasti verkko +% \begin{center} +% \begin{tikzpicture}[scale=0.7] +% \node[draw,circle,minimum size=20pt] (a) at (1,1) {$a$}; +% \node[draw,circle,minimum size=20pt] (b) at (1,2.5) {}; +% \node[draw,circle,minimum size=20pt] (c) at (1,4) {}; +% \node[draw,circle,minimum size=20pt] (d) at (1,5.5) {}; +% \node[draw,circle,minimum size=20pt] (e) at (2.5,5.5) {}; +% \node[draw,circle,minimum size=20pt] (f) at (4,5.5) {}; +% \node[draw,circle,minimum size=20pt] (g) at (5.5,5.5) {}; +% \node[draw,circle,minimum size=20pt] (h) at (7,5.5) {}; +% \node[draw,circle,minimum size=20pt] (i) at (8.5,5.5) {}; +% \node[draw,circle,minimum size=20pt] (j) at (8.5,4) {$b$}; +% \node[draw,circle,minimum size=20pt] (k) at (8.5,2.5) {}; +% \node[draw,circle,minimum size=20pt] (l) at (8.5,1) {}; +% \node[draw,circle,minimum size=20pt] (m) at (7,1) {}; +% \node[draw,circle,minimum size=20pt] (n) at (5.5,1) {}; +% \node[draw,circle,minimum size=20pt] (o) at (4,1) {}; +% \node[draw,circle,minimum size=20pt] (p) at (4,2.5) {}; +% \node[draw,circle,minimum size=20pt] (q) at (4,4) {}; +% +% \path[draw,thick,-] (a) -- (b); +% \path[draw,thick,-] (b) -- (c); +% \path[draw,thick,-] (c) -- (d); +% \path[draw,thick,-] (d) -- (e); +% \path[draw,thick,-] (e) -- (f); +% \path[draw,thick,-] (f) -- (g); +% \path[draw,thick,-] (g) -- (h); +% \path[draw,thick,-] (h) -- (i); +% \path[draw,thick,-] (i) -- (j); +% \path[draw,thick,-] (j) -- (k); +% \path[draw,thick,-] (k) -- (l); +% \path[draw,thick,-] (l) -- (m); +% \path[draw,thick,-] (m) -- (n); +% \path[draw,thick,-] (n) -- (o); +% \path[draw,thick,-] (o) -- (p); +% \path[draw,thick,-] (p) -- (q); +% \path[draw,thick,-] (q) -- (f); +% \end{tikzpicture} +% \end{center} +% jossa verkon solmuja ovat labyrintin lattiaruudut +% ja solmujen välillä on kaari, jos lattiaruudusta +% toiseen pääsee kulkemaan yhdellä askeleella. +% Niinpä erilaiset labyrinttiin liittyvät ongelmat +% palautuvat verkko-ongelmiksi. +% +% Esimerkiksi syvyyshaulla pystyy selvittämään, +% onko ruudusta $a$ reittiä ruutuun $b$ +% ja leveyshaku kertoo lisäksi, +% mikä on pienin mahdollinen askelten määrä reitillä. +% Samoin voi esimerkiksi vaikkapa, kuinka monta +% toisistaan erillistä huonetta labyrintissa on +% sekä kuinka monta ruutua huoneissa on. +% +% Labyrintin tapauksessa ei kannata muodostaa erikseen +% verkkoa, vaan syvyyshaun ja leveyshaun voi toteuttaa +% suoraan labyrintin ruudukkoon. + diff --git a/luku13.tex b/luku13.tex new file mode 100644 index 0000000..42cb7a6 --- /dev/null +++ b/luku13.tex @@ -0,0 +1,818 @@ +\chapter{Shortest paths} + +\index{lyhin polku@lyhin polku} + +Lyhimmän polun etsiminen alkusolmusta loppusolmuun +on keskeinen verkko-ongelma, joka esiintyy usein +käytännön tilanteissa. +Esimerkiksi tieverkostossa +luonteva ongelma on selvittää, +mikä on lyhin reitti kahden kaupungin välillä, +kun tiedossa ovat kaupunkien väliset tiet ja niiden pituudet. + +Jos verkon kaarilla ei ole painoja, +polun pituus on sama kuin kaarten +määrä polulla, jolloin lyhimmän polun +voi etsiä leveyshaulla. +Tässä luvussa keskitymme kuitenkin +tapaukseen, jossa kaarilla on painot. +Tällöin lyhimpien polkujen etsimiseen +tarvitaan kehittyneempiä algoritmeja. + +\section{Bellman–Fordin algoritmi} + +\index{Bellman–Fordin algoritmi} + +\key{Bellman–Fordin algoritmi} etsii +lyhimmän polun alkusolmusta +kaikkiin muihin verkon solmuihin. +Algoritmi toimii kaikenlaisissa verkoissa, +kunhan verkossa ei ole sykliä, +jonka kaarten yhteispaino on negatiivinen. +Jos verkossa on negatiivinen sykli, +algoritmi huomaa tilanteen. + +Algoritmi pitää yllä etäisyysarvioita +alkusolmusta kaikkiin muihin verkon solmuihin. +Alussa alkusolmun etäisyysarvio on 0 +ja muiden solmujen etäisyys\-arvio on ääretön. +Algoritmi parantaa arvioita +etsimällä verkosta kaaria, +jotka lyhentävät polkuja, +kunnes mitään arviota ei voi enää parantaa. + +\subsubsection{Esimerkki} + +Tarkastellaan Bellman–Fordin +algoritmin toimintaa seuraavassa verkossa: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (1,3) {1}; +\node[draw, circle] (2) at (4,3) {2}; +\node[draw, circle] (3) at (1,1) {3}; +\node[draw, circle] (4) at (4,1) {4}; +\node[draw, circle] (5) at (6,2) {5}; +\node[color=red] at (1,3+0.55) {$0$}; +\node[color=red] at (4,3+0.55) {$\infty$}; +\node[color=red] at (1,1-0.55) {$\infty$}; +\node[color=red] at (4,1-0.55) {$\infty$}; +\node[color=red] at (6,2-0.55) {$\infty$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:2] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:3] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:$-2$] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:3] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:2] {} (5); +\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (4); +\end{tikzpicture} +\end{center} +Verkon jokaiseen solmun viereen on merkitty etäisyysarvio. +Alussa alkusolmun etäisyysarvio on 0 +ja muiden solmujen etäisyysarvio on +ääretön. + +Algoritmi etsii verkosta kaaria, +jotka parantavat etäisyysarvioita. +Aluksi kaikki solmusta 0 lähtevät kaaret +parantavat arvioita: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (1,3) {1}; +\node[draw, circle] (2) at (4,3) {2}; +\node[draw, circle] (3) at (1,1) {3}; +\node[draw, circle] (4) at (4,1) {4}; +\node[draw, circle] (5) at (6,2) {5}; +\node[color=red] at (1,3+0.55) {$0$}; +\node[color=red] at (4,3+0.55) {$2$}; +\node[color=red] at (1,1-0.55) {$3$}; +\node[color=red] at (4,1-0.55) {$7$}; +\node[color=red] at (6,2-0.55) {$\infty$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:2] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:3] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:$-2$] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:3] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:2] {} (5); +\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (4); + +\path[draw=red,thick,->,line width=2pt] (1) -- (2); +\path[draw=red,thick,->,line width=2pt] (1) -- (3); +\path[draw=red,thick,->,line width=2pt] (1) -- (4); +\end{tikzpicture} +\end{center} +Sitten kaaret $2 \rightarrow 5$ ja $3 \rightarrow 4$ +parantavat arvioita: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (1,3) {1}; +\node[draw, circle] (2) at (4,3) {2}; +\node[draw, circle] (3) at (1,1) {3}; +\node[draw, circle] (4) at (4,1) {4}; +\node[draw, circle] (5) at (6,2) {5}; +\node[color=red] at (1,3+0.55) {$0$}; +\node[color=red] at (4,3+0.55) {$2$}; +\node[color=red] at (1,1-0.55) {$3$}; +\node[color=red] at (4,1-0.55) {$1$}; +\node[color=red] at (6,2-0.55) {$7$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:2] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:3] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:$-2$] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:3] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:2] {} (5); +\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (4); + +\path[draw=red,thick,->,line width=2pt] (2) -- (5); +\path[draw=red,thick,->,line width=2pt] (3) -- (4); +\end{tikzpicture} +\end{center} +Lopuksi tulee vielä yksi parannus: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (1,3) {1}; +\node[draw, circle] (2) at (4,3) {2}; +\node[draw, circle] (3) at (1,1) {3}; +\node[draw, circle] (4) at (4,1) {4}; +\node[draw, circle] (5) at (6,2) {5}; +\node[color=red] at (1,3+0.55) {$0$}; +\node[color=red] at (4,3+0.55) {$2$}; +\node[color=red] at (1,1-0.55) {$3$}; +\node[color=red] at (4,1-0.55) {$1$}; +\node[color=red] at (6,2-0.55) {$3$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:2] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:3] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:$-2$] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:3] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:2] {} (5); +\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (4); + +\path[draw=red,thick,->,line width=2pt] (4) -- (5); +\end{tikzpicture} +\end{center} + +Tämän jälkeen mikään kaari +ei paranna etäisyysarvioita. +Tämä tarkoittaa, että etäisyydet +ovat lopulliset, eli joka solmussa +on nyt pienin etäisyys alkusolmusta +kyseiseen solmuun. + +Esimerkiksi pienin etäisyys 3 +solmusta 1 solmuun 5 toteutuu käyttämällä +seuraavaa polkua: + +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (1,3) {1}; +\node[draw, circle] (2) at (4,3) {2}; +\node[draw, circle] (3) at (1,1) {3}; +\node[draw, circle] (4) at (4,1) {4}; +\node[draw, circle] (5) at (6,2) {5}; +\node[color=red] at (1,3+0.55) {$0$}; +\node[color=red] at (4,3+0.55) {$2$}; +\node[color=red] at (1,1-0.55) {$3$}; +\node[color=red] at (4,1-0.55) {$1$}; +\node[color=red] at (6,2-0.55) {$3$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:2] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:3] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:$-2$] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:3] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:2] {} (5); +\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (4); + +\path[draw=red,thick,->,line width=2pt] (1) -- (3); +\path[draw=red,thick,->,line width=2pt] (3) -- (4); +\path[draw=red,thick,->,line width=2pt] (4) -- (5); +\end{tikzpicture} +\end{center} + +\subsubsection{Toteutus} + +Seuraava Bellman–Fordin algoritmin toteutus +etsii lyhimmät polut solmusta $x$ +kaikkiin muihin verkon solmuihin. +Koodi olettaa, että verkko on tallennettuna +vieruslistoina taulukossa +\begin{lstlisting} +vector> v[N]; +\end{lstlisting} +niin, että parissa on ensin kaaren kohdesolmu +ja sitten kaaren paino. + +Algoritmi muodostuu $n-1$ kierroksesta, +joista jokaisella algoritmi käy läpi kaikki +verkon kaaret ja koettaa parantaa etäisyysarvioita. +Algoritmi laskee taulukkoon \texttt{e} +etäisyyden solmusta $x$ kuhunkin verkon solmuun. +Koodissa oleva alkuarvo $10^9$ kuvastaa +ääretöntä. + +\begin{lstlisting} +for (int i = 1; i <= n; i++) e[i] = 1e9; +e[x] = 0; +for (int i = 1; i <= n-1; i++) { + for (int a = 1; a <= n; a++) { + for (auto b : v[a]) { + e[b.first] = min(e[b.first],e[a]+b.second); + } + } +} +\end{lstlisting} + +Algoritmin aikavaativuus on $O(nm)$, +koska se muodostuu $n-1$ kierroksesta ja +käy läpi jokaisen kierroksen aikana kaikki $m$ kaarta. +Jos verkossa ei ole negatiivista sykliä, +kaikki etäisyysarviot ovat lopulliset $n-1$ +kierroksen jälkeen, koska jokaisessa lyhimmässä +polussa on enintään $n-1$ kaarta. + +Käytännössä kaikki lopulliset etäisyysarviot +saadaan usein laskettua selvästi alle $n-1$ kierroksessa, +joten mahdollinen tehostus algoritmiin on lopettaa heti, +kun mikään etäisyysarvio ei parane kierroksen aikana. + +\subsubsection{Negatiivinen sykli} + +\index{negatiivinen sykli@negatiivinen sykli} + +Bellman–Fordin algoritmin avulla voi myös tarkastaa, +onko verkossa sykliä, +jonka pituus on negatiivinen. +Esimerkiksi verkossa + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {$1$}; +\node[draw, circle] (2) at (2,1) {$2$}; +\node[draw, circle] (3) at (2,-1) {$3$}; +\node[draw, circle] (4) at (4,0) {$4$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:$3$] {} (2); +\path[draw,thick,-] (2) -- node[font=\small,label=above:$1$] {} (4); +\path[draw,thick,-] (1) -- node[font=\small,label=below:$5$] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:$-7$] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=right:$2$] {} (3); +\end{tikzpicture} +\end{center} +\noindent +on negatiivinen sykli $2 \rightarrow 3 \rightarrow 4 \rightarrow 2$, +jonka pituus on $-4$. + +Jos verkossa on negatiivinen sykli, +sen kautta kulkevaa polkua voi lyhentää äärettömästi +toistamalla negatiivista sykliä uudestaan ja uudestaan, +minkä vuoksi lyhimmän polun käsite ei ole mielekäs. + +Negatiivisen syklin voi tunnistaa +Bellman–Fordin algoritmilla +suorittamalla algoritmia $n$ kierrosta. +Jos viimeinen kierros parantaa jotain +etäisyysarviota, verkossa on negatiivinen sykli. +Huomaa, että algoritmi etsii negatiivista sykliä +koko verkon alueelta alkusolmusta välittämättä. + +\subsubsection{SPFA-algoritmi} + +\index{SPFA-algoritmi} + +\key{SPFA-algoritmi} (''Shortest Path Faster Algorithm'') +on Bellman–Fordin algoritmin muunnelma, +joka on usein alkuperäistä algoritmia tehokkaampi. +Se ei tutki joka kierroksella koko verkkoa läpi +parantaakseen etäisyysarvioita, vaan valitsee +tutkittavat kaaret älykkäämmin. + +Algoritmi pitää yllä jonoa solmuista, +joiden kautta saattaa pystyä parantamaan etäisyysarvioita. +Algoritmi lisää jonoon aluksi alkusolmun $x$ +ja valitsee aina seuraavan +tutkittavan solmun $a$ jonon alusta. +Aina kun kaari $a \rightarrow b$ parantaa +etäisyysarviota, algoritmi lisää jonoon solmun $b$. + +Seuraavassa toteutuksessa jonona on \texttt{queue}-rakenne +\texttt{q}. Lisäksi taulukko \texttt{z} kertoo, +onko solmu valmiina jonossa, jolloin algoritmi ei +lisää solmua jonoon uudestaan. + +\begin{lstlisting} +for (int i = 1; i <= n; i++) e[i] = 1e9; +e[x] = 0; +q.push(x); +while (!q.empty()) { + int a = q.front(); q.pop(); + z[a] = 0; + for (auto b : v[a]) { + if (e[a]+b.second < e[b.first]) { + e[b.first] = e[a]+b.second; + if (!z[b]) {q.push(b); z[b] = 1;} + } + } +} +\end{lstlisting} + +SPFA-algoritmin tehokkuus riippuu verkon rakenteesta: +algoritmi on keskimäärin hyvin tehokas, mutta +sen pahimman tapauksen aikavaativuus on edelleen +$O(nm)$ ja on mahdollista +laatia syötteitä, jotka saavat algoritmin yhtä hitaaksi +kuin tavallisen Bellman–Fordin algoritmin. + +\section{Dijkstran algoritmi} + +\index{Dijkstran algoritmi@Dijkstran algoritmi} + +\key{Dijkstran algoritmi} etsii Bellman–Fordin +algoritmin tavoin lyhimmät polut +alkusolmusta kaikkiin muihin solmuihin. +Dijkstran algoritmi on tehokkaampi kuin +Bellman–Fordin algoritmi, +minkä ansiosta se soveltuu suurten +verkkojen käsittelyyn. +Algoritmi vaatii kuitenkin, +ettei verkossa ole negatiivisia kaaria. + +Dijkstran algoritmi vastaa +Bellman–Fordin algoritmia siinä, +että se pitää +yllä etäisyysarvioita solmuihin +ja parantaa niitä algoritmin aikana. +Algoritmin tehokkuus perustuu +siihen, että sen riittää käydä läpi +verkon kaaret vain kerran +hyödyntäen tietoa, +ettei verkossa ole negatiivisia kaaria. + +\subsubsection{Esimerkki} + +Tarkastellaan Dijkstran algoritmin toimintaa +seuraavassa verkossa, kun alkusolmuna +on solmu 1: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {3}; +\node[draw, circle] (2) at (4,3) {4}; +\node[draw, circle] (3) at (1,1) {2}; +\node[draw, circle] (4) at (4,1) {1}; +\node[draw, circle] (5) at (6,2) {5}; + +\node[color=red] at (1,3+0.6) {$\infty$}; +\node[color=red] at (4,3+0.6) {$\infty$}; +\node[color=red] at (1,1-0.6) {$\infty$}; +\node[color=red] at (4,1-0.6) {$0$}; +\node[color=red] at (6,2-0.6) {$\infty$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:6] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5); +\end{tikzpicture} +\end{center} +Bellman–Fordin algoritmin tavoin +alkusolmun etäisyysarvio on 0 +ja kaikissa muissa solmuissa etäisyysarvio +on aluksi ääretön. + +Dijkstran algoritmi +ottaa joka askeleella käsittelyyn +sellaisen solmun, +jota ei ole vielä käsitelty +ja jonka etäisyysarvio on +mahdollisimman pieni. +Alussa tällainen solmu on solmu 1, +jonka etäisyysarvio on 0. + +Kun solmu tulee käsittelyyn, +algoritmi käy läpi kaikki +siitä lähtevät kaaret ja +parantaa etäisyysarvioita +niiden avulla: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {3}; +\node[draw, circle] (2) at (4,3) {4}; +\node[draw, circle] (3) at (1,1) {2}; +\node[draw, circle, fill=lightgray] (4) at (4,1) {1}; +\node[draw, circle] (5) at (6,2) {5}; + +\node[color=red] at (1,3+0.6) {$\infty$}; +\node[color=red] at (4,3+0.6) {$9$}; +\node[color=red] at (1,1-0.6) {$5$}; +\node[color=red] at (4,1-0.6) {$0$}; +\node[color=red] at (6,2-0.6) {$1$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:6] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5); + +\path[draw=red,thick,->,line width=2pt] (4) -- (2); +\path[draw=red,thick,->,line width=2pt] (4) -- (3); +\path[draw=red,thick,->,line width=2pt] (4) -- (5); +\end{tikzpicture} +\end{center} +Solmun 1 käsittely paransi etäisyysarvioita +solmuihin 2, 4 ja 5, +joiden uudet etäisyydet ovat nyt 5, 9 ja 1. + +Seuraavaksi käsittelyyn tulee solmu 5, +jonka etäisyys on 1: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (1,3) {3}; +\node[draw, circle] (2) at (4,3) {4}; +\node[draw, circle] (3) at (1,1) {2}; +\node[draw, circle, fill=lightgray] (4) at (4,1) {1}; +\node[draw, circle, fill=lightgray] (5) at (6,2) {5}; + +\node[color=red] at (1,3+0.6) {$\infty$}; +\node[color=red] at (4,3+0.6) {$3$}; +\node[color=red] at (1,1-0.6) {$5$}; +\node[color=red] at (4,1-0.6) {$0$}; +\node[color=red] at (6,2-0.6) {$1$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:6] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5); + +\path[draw=red,thick,->,line width=2pt] (5) -- (2); +\end{tikzpicture} +\end{center} +Tämän jälkeen vuorossa on solmu 4: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {3}; +\node[draw, circle, fill=lightgray] (2) at (4,3) {4}; +\node[draw, circle] (3) at (1,1) {2}; +\node[draw, circle, fill=lightgray] (4) at (4,1) {1}; +\node[draw, circle, fill=lightgray] (5) at (6,2) {5}; + +\node[color=red] at (1,3+0.6) {$9$}; +\node[color=red] at (4,3+0.6) {$3$}; +\node[color=red] at (1,1-0.6) {$5$}; +\node[color=red] at (4,1-0.6) {$0$}; +\node[color=red] at (6,2-0.6) {$1$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:6] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5); + +\path[draw=red,thick,->,line width=2pt] (2) -- (1); +\end{tikzpicture} +\end{center} + +Dijkstran algoritmissa on hienoutena, +että aina kun solmu tulee käsittelyyn, +sen etäisyysarvio on siitä lähtien lopullinen. +Esimerkiksi tässä vaiheessa +etäisyydet 0, 1 ja 3 ovat lopulliset +etäisyydet solmuihin 1, 5 ja 4. + +Algoritmi käsittelee vastaavasti +vielä kaksi viimeistä solmua, +minkä jälkeen algoritmin päätteeksi +etäisyydet ovat: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle, fill=lightgray] (1) at (1,3) {3}; +\node[draw, circle, fill=lightgray] (2) at (4,3) {4}; +\node[draw, circle, fill=lightgray] (3) at (1,1) {2}; +\node[draw, circle, fill=lightgray] (4) at (4,1) {1}; +\node[draw, circle, fill=lightgray] (5) at (6,2) {5}; + +\node[color=red] at (1,3+0.6) {$7$}; +\node[color=red] at (4,3+0.6) {$3$}; +\node[color=red] at (1,1-0.6) {$5$}; +\node[color=red] at (4,1-0.6) {$0$}; +\node[color=red] at (6,2-0.6) {$1$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:6] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5); +\end{tikzpicture} +\end{center} + +\subsubsection{Negatiiviset kaaret} + +Dijkstran algoritmin tehokkuus perustuu siihen, +että verkossa ei ole negatiivisia kaaria. +Jos verkossa on negatiivinen kaari, +algoritmi ei välttämättä toimi oikein. +Tarkastellaan esimerkkinä seuraavaa verkkoa: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {$1$}; +\node[draw, circle] (2) at (2,1) {$2$}; +\node[draw, circle] (3) at (2,-1) {$3$}; +\node[draw, circle] (4) at (4,0) {$4$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:2] {} (2); +\path[draw,thick,-] (2) -- node[font=\small,label=above:3] {} (4); +\path[draw,thick,-] (1) -- node[font=\small,label=below:6] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:$-5$] {} (4); +\end{tikzpicture} +\end{center} +\noindent +Lyhin polku solmusta 1 solmuun 4 on +$1 \rightarrow 3 \rightarrow 4$, +ja sen pituus on 1. +Dijkstran algoritmi löytää +kuitenkin keveimpiä kaaria seuraten +polun $1 \rightarrow 2 \rightarrow 4$. +Algoritmi ei pysty ottamaan huomioon, +että alemmalla polulla kaaren paino $-5$ +kumoaa aiemman suuren kaaren painon $6$. + +\subsubsection{Toteutus} + +Seuraava Dijkstran algoritmin toteutus laskee +pienimmän etäisyyden solmusta $x$ kaikkiin muihin solmuihin. +Verkko on tallennettu taulukkoon \texttt{v} +vieruslistoina, joissa on pareina kohdesolmu +ja kaaren pituus. + +Dijkstran algoritmin tehokas toteutus vaatii, +että verkosta pystyy löytämään +nopeasti vielä käsittelemättömän solmun, +jonka etäisyysarvio on pienin. +Sopiva tietorakenne tähän on prioriteettijono, +jossa solmut ovat järjestyksessä etäisyys\-arvioiden mukaan. +Prioriteettijonon avulla +seuraavaksi käsiteltävän solmun saa selville logaritmisessa ajassa. + +Seuraavassa toteutuksessa prioriteettijono sisältää +pareja, joiden ensimmäinen kenttä on etäisyysarvio +ja toinen kenttä on solmun tunniste: +\begin{lstlisting} +priority_queue> q; +\end{lstlisting} +Pieni hankaluus on, +että Dijkstran algoritmissa täytyy saada selville +\emph{pienimmän} etäisyysarvion solmu, +kun taas C++:n prioriteettijono antaa oletuksena +\emph{suurimman} alkion. +Helppo ratkaisu on tallentaa etäisyysarviot +\emph{negatiivisina}, jolloin C++:n prioriteettijonoa +voi käyttää suoraan. + +Koodi merkitsee taulukkoon \texttt{z}, +onko solmu käsitelty, +ja pitää yllä etäisyysarvioita taulukossa \texttt{e}. +Alussa alkusolmun etäisyysarvio on 0 +ja jokaisen muun solmun etäisyysarviona +on ääretöntä vastaava $10^9$. + +\begin{lstlisting} +for (int i = 1; i <= n; i++) e[i] = 1e9; +e[x] = 0; +q.push({0,x}); +while (!q.empty()) { + int a = q.top().second; q.pop(); + if (z[a]) continue; + z[a] = 1; + for (auto b : v[a]) { + if (e[a]+b.second < e[b]) { + e[b] = e[a]+b.second; + q.push({-e[b],b}); + } + } +} +\end{lstlisting} + +Yllä olevan toteutuksen aikavaativuus on $O(n+m \log m)$, +koska algoritmi käy läpi kaikki verkon solmut +ja lisää jokaista kaarta kohden korkeintaan +yhden etäisyysarvion prioriteettijonoon. + +\section{Floyd–Warshallin algoritmi} + +\index{Floyd–Warshallin algoritmi} + +\key{Floyd–Warshallin algoritmi} +on toisenlainen lähestymistapa +lyhimpien polkujen etsintään. +Toisin kuin muut tämän luvun algoritmit, +se etsii yhdellä kertaa lyhimmät polut kaikkien +verkon solmujen välillä. + + +Algoritmi ylläpitää kaksiulotteista +taulukkoa etäisyyksistä solmujen +välillä. +Ensin taulukkoon on merkitty +etäisyydet käyttäen vain solmujen +välisiä kaaria. +Tämän jälkeen algoritmi +päivittää etäisyyksiä, +kun verkon solmut saavat yksi kerrallaan +toimia välisolmuina poluilla. + +\subsubsection{Esimerkki} + +Tarkastellaan Floyd–Warshallin +algoritmin toimintaa seuraavassa verkossa: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$3$}; +\node[draw, circle] (2) at (4,3) {$4$}; +\node[draw, circle] (3) at (1,1) {$2$}; +\node[draw, circle] (4) at (4,1) {$1$}; +\node[draw, circle] (5) at (6,2) {$5$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5); +\end{tikzpicture} +\end{center} + +Algoritmi merkitsee aluksi taulukkoon +etäisyyden 0 jokaisesta solmusta itseensä +sekä etäisyyden $x$, jos solmuparin välillä +on kaari, jonka pituus on $x$. +Muiden solmuparien etäisyys on aluksi ääretön. + +Tässä verkossa taulukosta tulee: +\begin{center} +\begin{tabular}{r|rrrrr} + & 1 & 2 & 3 & 4 & 5 \\ +\hline +1 & 0 & 5 & $\infty$ & 9 & 1 \\ +2 & 5 & 0 & 2 & $\infty$ & $\infty$ \\ +3 & $\infty$ & 2 & 0 & 7 & $\infty$ \\ +4 & 9 & $\infty$ & 7 & 0 & 2 \\ +5 & 1 & $\infty$ & $\infty$ & 2 & 0 \\ +\end{tabular} +\end{center} +\vspace{10pt} +Algoritmin toiminta muodostuu peräkkäisistä kierroksista. +Jokaisella kierroksella valitaan yksi uusi solmu, +joka saa toimia välisolmuna poluilla, +ja algoritmi parantaa taulukon +etäisyyksiä muodostaen polkuja tämän solmun avulla. + +Ensimmäisellä kierroksella solmu 1 on välisolmu. +Tämän ansiosta solmujen 2 ja 4 välille muodostuu +polku, jonka pituus on 14, +koska solmu 1 yhdistää ne toisiinsa. +Vastaavasti solmut 2 ja 5 yhdistyvät polulla, +jonka pituus on 6. + +\begin{center} +\begin{tabular}{r|rrrrr} + & 1 & 2 & 3 & 4 & 5 \\ +\hline +1 & 0 & 5 & $\infty$ & 9 & 1 \\ +2 & 5 & 0 & 2 & \textbf{14} & \textbf{6} \\ +3 & $\infty$ & 2 & 0 & 7 & $\infty$ \\ +4 & 9 & \textbf{14} & 7 & 0 & 2 \\ +5 & 1 & \textbf{6} & $\infty$ & 2 & 0 \\ +\end{tabular} +\end{center} +\vspace{10pt} + +Toisella kierroksella solmu 2 saa toimia välisolmuna. +Tämä mahdollistaa uudet polut solmuparien 1 ja 3 +sekä 3 ja 5 välille: + +\begin{center} +\begin{tabular}{r|rrrrr} + & 1 & 2 & 3 & 4 & 5 \\ +\hline +1 & 0 & 5 & \textbf{7} & 9 & 1 \\ +2 & 5 & 0 & 2 & 14 & 6 \\ +3 & \textbf{7} & 2 & 0 & 7 & \textbf{8} \\ +4 & 9 & 14 & 7 & 0 & 2 \\ +5 & 1 & 6 & \textbf{8} & 2 & 0 \\ +\end{tabular} +\end{center} +\vspace{10pt} + +Kolmannella kierroksella solmu 3 saa toimia välisolmuna, +jolloin syntyy uusi polku solmuparin 2 ja 4 välille: + +\begin{center} +\begin{tabular}{r|rrrrr} + & 1 & 2 & 3 & 4 & 5 \\ +\hline +1 & 0 & 5 & 7 & 9 & 1 \\ +2 & 5 & 0 & 2 & \textbf{9} & 6 \\ +3 & 7 & 2 & 0 & 7 & 8 \\ +4 & 9 & \textbf{9} & 7 & 0 & 2 \\ +5 & 1 & 6 & 8 & 2 & 0 \\ +\end{tabular} +\end{center} +\vspace{10pt} + + +Algoritmin toiminta jatkuu samalla tavalla +niin, että kukin solmu tulee vuorollaan +välisolmuksi. +Algoritmin päätteeksi taulukko sisältää +lyhimmän etäisyyden minkä tahansa +solmuparin välillä: + +\begin{center} +\begin{tabular}{r|rrrrr} + & 1 & 2 & 3 & 4 & 5 \\ +\hline +1 & 0 & 5 & 7 & 3 & 1 \\ +2 & 5 & 0 & 2 & 9 & 6 \\ +3 & 7 & 2 & 0 & 7 & 8 \\ +4 & 3 & 9 & 7 & 0 & 2 \\ +5 & 1 & 6 & 8 & 2 & 0 \\ +\end{tabular} +\end{center} + +Esimerkiksi taulukosta selviää, että lyhin polku +solmusta 2 solmuun 4 on pituudeltaan 8. +Tämä vastaa seuraavaa polkua: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$3$}; +\node[draw, circle] (2) at (4,3) {$4$}; +\node[draw, circle] (3) at (1,1) {$2$}; +\node[draw, circle] (4) at (4,1) {$1$}; +\node[draw, circle] (5) at (6,2) {$5$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5); +\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5); + +\path[draw=red,thick,->,line width=2pt] (3) -- (4); +\path[draw=red,thick,->,line width=2pt] (4) -- (5); +\path[draw=red,thick,->,line width=2pt] (5) -- (2); +\end{tikzpicture} +\end{center} + +\subsubsection{Toteutus} + +Floyd–Warshallin algoritmin etuna on, +että se on helppoa toteuttaa. +Seuraava toteutus muodostaa etäisyysmatriisin +\texttt{d}, jossa $\texttt{d}[a][b]$ +on pienin etäisyys polulla solmusta $a$ solmuun $b$. +Aluksi algoritmi alustaa matriisin \texttt{d} +verkon vierusmatriisin \texttt{v} perusteella +(arvo $10^9$ kuvastaa ääretöntä): + +\begin{lstlisting} +for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + if (i == j) d[i][j] = 0; + else if (v[i][j]) d[i][j] = v[i][j]; + else d[i][j] = 1e9; + } +} +\end{lstlisting} + +Tämän jälkeen lyhimmät polut löytyvät seuraavasti: + +\begin{lstlisting} +for (int k = 1; k <= n; k++) { + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + d[i][j] = min(d[i][j], d[i][k]+d[k][j]); + } + } +} +\end{lstlisting} + +Algoritmin aikavaativuus on +$O(n^3)$, koska siinä on kolme sisäkkäistä +silmukkaa, +jotka käyvät läpi verkon solmut. + +Koska Floyd–Warshallin +algoritmin toteutus on yksinkertainen, +algoritmi voi olla hyvä valinta jopa silloin, +kun haettavana on yksittäinen +lyhin polku verkossa. +Tämä on kuitenkin mahdollista vain silloin, +kun verkko on niin pieni, +että kuutiollinen aikavaativuus on riittävä. diff --git a/luku14.tex b/luku14.tex new file mode 100644 index 0000000..ce93b20 --- /dev/null +++ b/luku14.tex @@ -0,0 +1,555 @@ +\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. + diff --git a/luku15.tex b/luku15.tex new file mode 100644 index 0000000..99172cf --- /dev/null +++ b/luku15.tex @@ -0,0 +1,745 @@ +\chapter{Spanning trees} + +\index{virittxvx puu@virittävä puu} + +\key{Virittävä puu} on kokoelma +verkon kaaria, +joka kytkee kaikki +verkon solmut toisiinsa. +Kuten puut yleensäkin, +virittävä puu on yhtenäinen ja syklitön. +Virittävän puun muodostamiseen +on yleensä monia tapoja. + +Esimerkiksi verkossa +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} +yksi mahdollinen virittävä puu on seuraava: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} + +Virittävän puun paino on siihen kuuluvien kaarten painojen summa. +Esimerkiksi yllä olevan puun paino on $3+5+9+3+2=22$. + +\key{Pienin virittävä puu} +on virittävä puu, jonka paino on mahdollisimman pieni. +Yllä olevan verkon pienin virittävä puu +on painoltaan 20, ja sen voi muodostaa seuraavasti: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +%\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +%\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +%\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} + +Vastaavasti \key{suurin virittävä puu} +on virittävä puu, jonka paino on mahdollisimman suuri. +Yllä olevan verkon suurin virittävä puu on +painoltaan 32: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; +%\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +%\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +%\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} + +Huomaa, että voi olla monta erilaista +tapaa muodostaa pienin tai +suurin virittävä puu, eli puut eivät ole yksikäsitteisiä. + +Tässä luvussa tutustumme algoritmeihin, +jotka muodostavat verkon pienimmän tai suurimman +virittävän puun. +Osoittautuu, että virittävien puiden etsiminen +on siinä mielessä helppo ongelma, +että monenlaiset ahneet menetelmät tuottavat +optimaalisen ratkaisun. + +Käymme läpi kaksi algoritmia, jotka molemmat valitsevat +puuhun mukaan kaaria painojärjestyksessä. +Keskitymme pienimmän virittävän puun etsimiseen, +mutta samoilla algoritmeilla voi muodostaa myös suurimman virittävän +puun käsittelemällä kaaret käänteisessä järjestyksessä. + +\section{Kruskalin algoritmi} + +\index{Kruskalin algoritmi@Kruskalin algoritmi} + +\key{Kruskalin algoritmi} aloittaa pienimmän +virittävän +puun muodostamisen tilanteesta, +jossa puussa ei ole yhtään kaaria. +Sitten algoritmi alkaa lisätä +puuhun kaaria järjestyksessä +kevyimmästä raskaimpaan. +Kunkin kaaren kohdalla +algoritmi ottaa kaaren mukaan puuhun, +jos tämä ei aiheuta sykliä. + +Kruskalin algoritmi pitää yllä +tietoa verkon komponenteista. +Aluksi jokainen solmu on omassa +komponentissaan, +ja komponentit yhdistyvät pikkuhiljaa +algoritmin aikana puuhun tulevista kaarista. +Lopulta kaikki solmut ovat samassa +komponentissa, jolloin pienin virittävä puu on valmis. + +\subsubsection{Esimerkki} + +\begin{samepage} +Tarkastellaan Kruskalin algoritmin toimintaa +seuraavassa verkossa: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} +\end{samepage} + +\begin{samepage} +Algoritmin ensimmäinen vaihe on +järjestää verkon kaaret niiden painon mukaan. +Tuloksena on seuraava lista: + +\begin{tabular}{ll} +\\ +kaari & paino \\ +\hline +5--6 & 2 \\ +1--2 & 3 \\ +3--6 & 3 \\ +1--5 & 5 \\ +2--3 & 5 \\ +2--5 & 6 \\ +4--6 & 7 \\ +3--4 & 9 \\ +\\ +\end{tabular} +\end{samepage} + +Tämän jälkeen algoritmi käy listan läpi +ja lisää kaaren puuhun, +jos se yhdistää kaksi erillistä komponenttia. + +Aluksi jokainen solmu on omassa komponentissaan: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; +%\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +%\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +%\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +%\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +%\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +%\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +%\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +%\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} +Ensimmäinen virittävään puuhun lisättävä +kaari on 5--6, joka yhdistää +komponentit $\{5\}$ ja $\{6\}$ komponentiksi $\{5,6\}$: + +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; + +%\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +%\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +%\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +%\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +%\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +%\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +%\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} +Tämän jälkeen algoritmi lisää puuhun vastaavasti +kaaret 1--2, 3--6 ja 1--5: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +%\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +%\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +%\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +%\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} + +Näiden lisäysten jälkeen monet +komponentit ovat yhdistyneet ja verkossa on kaksi +komponenttia: $\{1,2,3,5,6\}$ ja $\{4\}$. + +Seuraavaksi käsiteltävä kaari on 2--3, +mutta tämä kaari ei tule mukaan puuhun, +koska solmut 2 ja 3 ovat jo samassa komponentissa. +Vastaavasta syystä myöskään kaari 2--5 ei tule mukaan puuhun. + +\begin{samepage} +Lopuksi puuhun tulee kaari 4--6, +joka luo yhden komponentin: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +%\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +%\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +%\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} +\end{samepage} + +Tämän lisäyksen jälkeen algoritmi päättyy, +koska kaikki solmut on kytketty toisiinsa kaarilla +ja verkko on yhtenäinen. +Tuloksena on verkon pienin virittävä puu, +jonka paino on $2+3+3+5+7=20$. + +\subsubsection{Miksi algoritmi toimii?} + +On hyvä kysymys, miksi Kruskalin algoritmi +toimii aina eli miksi ahne strategia tuottaa +varmasti pienimmän mahdollisen virittävän puun. + +Voimme perustella algoritmin toimivuuden +tekemällä vastaoletuksen, että pienimmässä +virittävässä puussa ei olisi verkon keveintä kaarta. +Oletetaan esimerkiksi, että äskeisen verkon +pienimmässä virittävässä puussa ei olisi +2:n painoista kaarta solmujen 5 ja 6 välillä. +Emme tiedä tarkalleen, millainen uusi pienin +virittävä puu olisi, mutta siinä täytyy olla +kuitenkin joukko kaaria. +Oletetaan, että virittävä puu olisi +vaikkapa seuraavanlainen: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; + +\path[draw,thick,-,dashed] (1) -- (2); +\path[draw,thick,-,dashed] (2) -- (5); +\path[draw,thick,-,dashed] (2) -- (3); +\path[draw,thick,-,dashed] (3) -- (4); +\path[draw,thick,-,dashed] (4) -- (6); +\end{tikzpicture} +\end{center} + +Ei ole kuitenkaan mahdollista, +että yllä oleva virittävä puu olisi todellisuudessa +verkon pienin virittävä puu. +Tämä johtuu siitä, että voimme poistaa siitä +jonkin kaaren ja korvata sen 2:n painoisella kaarella. +Tuloksena on virittävä puu, jonka paino on \emph{pienempi}: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; + +\path[draw,thick,-,dashed] (1) -- (2); +\path[draw,thick,-,dashed] (2) -- (5); +\path[draw,thick,-,dashed] (3) -- (4); +\path[draw,thick,-,dashed] (4) -- (6); +\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +\end{tikzpicture} +\end{center} + +Niinpä on aina optimaalinen ratkaisu valita pienimpään +virittävään puuhun verkon kevein kaari. +Vastaavalla tavalla voimme perustella +seuraavaksi keveimmän kaaren valinnan, jne. +Niinpä Kruskalin algoritmi toimii oikein ja +tuottaa aina pienimmän virittävän puun. + +\subsubsection{Toteutus} + +Kruskalin algoritmi on mukavinta toteuttaa +kaarilistan avulla. Algoritmin ensimmäinen vaihe +on järjestää kaaret painojärjestykseen, +missä kuluu aikaa $O(m \log m)$. +Tämän jälkeen seuraa algoritmin toinen vaihe, +jossa listalta valitaan kaaret mukaan puuhun. + +Algoritmin toinen vaihe rakentuu seuraavanlaisen silmukan ympärille: + +\begin{lstlisting} +for (...) { + if (!sama(a,b)) liita(a,b); +} +\end{lstlisting} + +Silmukka käy läpi kaikki listan kaaret +niin, että muuttujat $a$ ja $b$ ovat kulloinkin kaaren +päissä olevat solmut. +Koodi käyttää kahta funktiota: +funktio \texttt{sama} tutkii, +ovatko solmut samassa komponentissa, +ja funktio \texttt{liita} +yhdistää kaksi komponenttia toisiinsa. + +Ongelmana on, kuinka toteuttaa tehokkaasti +funktiot \texttt{sama} ja \texttt{liita}. +Yksi mahdollisuus on pitää yllä verkkoa tavallisesti +ja toteuttaa funktio \texttt{sama} verkon läpikäyntinä. +Tällöin kuitenkin funktion \texttt{sama} +suoritus veisi aikaa $O(n+m)$, +mikä on hidasta, koska funktiota kutsutaan +jokaisen kaaren kohdalla. + +Seuraavaksi esiteltävä union-find-rakenne +ratkaisee asian. +Se toteuttaa molemmat funktiot +ajassa $O(\log n)$, +jolloin Kruskalin algoritmin +aikavaativuus on vain $O(m \log n)$ +kaarilistan järjestämisen jälkeen. + +\section{Union-find-rakenne} + +\index{union-find-rakenne} + +\key{Union-find-rakenne} pitää yllä +alkiojoukkoja. +Joukot ovat erillisiä, +eli tietty alkio on tarkalleen +yhdessä joukossa. +Rakenne tarjoaa kaksi operaatiota, +jotka toimivat ajassa $O(\log n)$. +Ensimmäinen operaatio tarkistaa, +ovatko kaksi alkiota samassa joukossa. +Toinen operaatio yhdistää kaksi +joukkoa toisiinsa. + +\subsubsection{Rakenne} + +Union-find-rakenteessa jokaisella +joukolla on edustaja-alkio. +Kaikki muut joukon alkiot osoittavat +edustajaan joko suoraan tai +muiden alkioiden kautta. + +Esimerkiksi jos joukot ovat +$\{1,4,7\}$, $\{5\}$ ja $\{2,3,6,8\}$, +tilanne voisi olla: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (0,-1) {$1$}; +\node[draw, circle] (2) at (7,0) {$2$}; +\node[draw, circle] (3) at (7,-1.5) {$3$}; +\node[draw, circle] (4) at (1,0) {$4$}; +\node[draw, circle] (5) at (4,0) {$5$}; +\node[draw, circle] (6) at (6,-2.5) {$6$}; +\node[draw, circle] (7) at (2,-1) {$7$}; +\node[draw, circle] (8) at (8,-2.5) {$8$}; + +\path[draw,thick,->] (1) -- (4); +\path[draw,thick,->] (7) -- (4); + +\path[draw,thick,->] (3) -- (2); +\path[draw,thick,->] (6) -- (3); +\path[draw,thick,->] (8) -- (3); + +\end{tikzpicture} +\end{center} +Tässä tapauksessa alkiot 4, 5 ja 2 +ovat joukkojen edustajat. +Minkä tahansa alkion edustaja +löytyy kulkemalla alkiosta lähtevää polkua +eteenpäin niin kauan, kunnes polku päättyy. +Esimerkiksi alkion 6 edustaja on 2, +koska alkiosta 6 lähtevä +polku on $6 \rightarrow 3 \rightarrow 2$. +Tämän avulla voi selvittää, +ovatko kaksi alkiota samassa joukossa: +jos kummankin alkion edustaja on sama, +alkiot ovat samassa joukossa, +ja muuten ne ovat eri joukoissa. + +Kahden joukon yhdistäminen tapahtuu +valitsemalla toinen edustaja +joukkojen yhteiseksi edustajaksi +ja kytkemällä toinen edustaja siihen. +Esimerkiksi joukot $\{1,4,7\}$ ja $\{2,3,6,8\}$ +voi yhdistää näin joukoksi $\{1,2,3,4,6,7,8\}$: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (2,-1) {$1$}; +\node[draw, circle] (2) at (7,0) {$2$}; +\node[draw, circle] (3) at (7,-1.5) {$3$}; +\node[draw, circle] (4) at (3,0) {$4$}; +\node[draw, circle] (6) at (6,-2.5) {$6$}; +\node[draw, circle] (7) at (4,-1) {$7$}; +\node[draw, circle] (8) at (8,-2.5) {$8$}; + +\path[draw,thick,->] (1) -- (4); +\path[draw,thick,->] (7) -- (4); + +\path[draw,thick,->] (3) -- (2); +\path[draw,thick,->] (6) -- (3); +\path[draw,thick,->] (8) -- (3); + +\path[draw,thick,->] (4) -- (2); +\end{tikzpicture} +\end{center} + +Joukkojen yhteiseksi edustajaksi valitaan alkio 2, +minkä vuoksi alkio 4 yhdistetään siihen. +Tästä lähtien alkio 2 edustaa kaikkia joukon alkioita. + +Tehokkuuden kannalta oleellista on, +miten yhdistäminen tapahtuu. +Osoittautuu, että ratkaisu on yksinkertainen: +riittää yhdistää aina pienempi joukko suurempaan, +tai kummin päin tahansa, +jos joukot ovat yhtä suuret. +Tällöin pisin ketju +alkiosta edustajaan on aina luokkaa $O(\log n)$, +koska jokainen askel eteenpäin +ketjussa kaksinkertaistaa +vastaavan joukon koon. + +\subsubsection{Toteutus} + +Union-find-rakenne on kätevää toteuttaa +taulukoiden avulla. +Seuraavassa toteutuksessa taulukko \texttt{k} +viittaa seuraavaan alkioon ketjussa +tai alkioon itseensä, jos alkio on edustaja. +Taulukko \texttt{s} taas kertoo jokaiselle edustajalle, +kuinka monta alkiota niiden joukossa on. + +Aluksi jokainen alkio on omassa joukossaan, +jonka koko on 1: + +\begin{lstlisting} +for (int i = 1; i <= n; i++) k[i] = i; +for (int i = 1; i <= n; i++) s[i] = 1; +\end{lstlisting} + +Funktio \texttt{id} kertoo alkion $x$ +joukon edustajan. Alkion edustaja löytyy +käymällä ketju läpi alkiosta $x$ alkaen. + +\begin{lstlisting} +int id(int x) { + while (x != k[x]) x = k[x]; + return x; +} +\end{lstlisting} + +Funktio \texttt{sama} kertoo, +ovatko alkiot $a$ ja $b$ samassa joukossa. +Tämä onnistuu helposti funktion +\texttt{id} avulla. + +\begin{lstlisting} +bool sama(int a, int b) { + return id(a) == id(b); +} +\end{lstlisting} + +\begin{samepage} +Funktio \texttt{liita} yhdistää +puolestaan alkioiden $a$ ja $b$ osoittamat +joukot yhdeksi joukoksi. +Funktio etsii ensin joukkojen edustajat +ja yhdistää sitten pienemmän joukon suurempaan. + +\begin{lstlisting} +void liita(int a, int b) { + a = id(a); + b = id(b); + if (s[b] > s[a]) swap(a,b); + s[a] += s[b]; + k[b] = a; +} +\end{lstlisting} +\end{samepage} + +Funktion \texttt{id} aikavaativuus on $O(\log n)$ +olettaen, että ketjun pituus on luokkaa $O(\log n)$. +Niinpä myös funktioiden \texttt{sama} ja \texttt{liita} +aikavaativuus on $O(\log n)$. +Funktio \texttt{liita} varmistaa, +että ketjun pituus on luokkaa $O(\log n)$ +yhdistämällä pienemmän joukon suurempaan. + +% Funktiota \texttt{id} on mahdollista vielä tehostaa +% seuraavasti: +% +% \begin{lstlisting} +% int id(int x) { +% if (x == k[x]) return x; +% return k[x] = id(x); +% } +% \end{lstlisting} +% +% Nyt joukon edustajan etsimisen yhteydessä kaikki ketjun +% alkiot laitetaan osoittamaan suoraan edustajaan. +% On mahdollista osoittaa, että tämän avulla +% funktioiden \texttt{sama} ja \texttt{liita} +% aikavaativuus on tasoitetusti +% vain $O(\alpha(n))$, missä $\alpha(n)$ on +% hyvin hitaasti kasvava käänteinen Ackermannin funktio. + +\section{Primin algoritmi} + +\index{Primin algoritmi@Primin algoritmi} + +\key{Primin algoritmi} on vaihtoehtoinen menetelmä +verkon pienimmän virittävän puun muodostamiseen. +Algoritmi aloittaa puun muodostamisen jostakin +verkon solmusta ja lisää puuhun aina kaaren, +joka on mahdollisimman kevyt ja joka +liittää puuhun uuden solmun. +Lopulta kaikki solmut on lisätty puuhun +ja pienin virittävä puu on valmis. + +Primin algoritmin toiminta on lähellä +Dijkstran algoritmia. +Erona on, että Dijkstran algoritmissa valitaan +kaari, jonka kautta syntyy lyhin polku alkusolmusta +uuteen solmuun, mutta Primin algoritmissa +valitaan vain kevein kaari, joka johtaa uuteen solmuun. + +\subsubsection{Esimerkki} + +Tarkastellaan Primin algoritmin toimintaa +seuraavassa verkossa: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); + +%\path[draw=red,thick,-,line width=2pt] (5) -- (6); +\end{tikzpicture} +\end{center} + +Aluksi solmujen välillä ei ole mitään kaaria: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; +%\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +%\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +%\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +%\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +%\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +%\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +%\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +%\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} + +Puun muodostuksen voi aloittaa mistä tahansa solmusta, +ja aloitetaan se nyt solmusta 1. +Kevein kaari on painoltaan 3 ja se johtaa solmuun 2: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +%\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +%\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +%\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +%\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +%\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +%\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +%\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} + +Nyt kevein uuteen solmuun johtavan +kaaren paino on 5, +ja voimme laajentaa joko solmuun 3 tai 5. +Valitaan solmu 3: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +%\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +%\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +%\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +%\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +%\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +%\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} + +\begin{samepage} +Sama jatkuu, kunnes kaikki solmut ovat mukana puussa: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1.5,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (6.5,2) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; +\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (3); +%\path[draw,thick,-] (3) -- node[font=\small,label=above:9] {} (4); +%\path[draw,thick,-] (1) -- node[font=\small,label=below:5] {} (5); +\path[draw,thick,-] (5) -- node[font=\small,label=below:2] {} (6); +\path[draw,thick,-] (6) -- node[font=\small,label=below:7] {} (4); +%\path[draw,thick,-] (2) -- node[font=\small,label=left:6] {} (5); +\path[draw,thick,-] (3) -- node[font=\small,label=left:3] {} (6); +\end{tikzpicture} +\end{center} +\end{samepage} + +\subsubsection{Toteutus} + +Dijkstran algoritmin tavoin Primin algoritmin voi toteuttaa +tehokkaasti käyttämällä prioriteettijonoa. +Primin algoritmin tapauksessa jono sisältää kaikki solmut, +jotka voi yhdistää nykyiseen komponentiin kaarella, +järjestyksessä kaaren painon mukaan kevyimmästä raskaimpaan. + +Primin algoritmin aikavaativuus on $O(n + m \log m)$ +eli sama kuin Dijkstran algoritmissa. +Käytännössä Primin algoritmi on suunnilleen +yhtä nopea kuin Kruskalin algoritmi, +ja onkin makuasia, kumpaa algoritmia käyttää. +Useimmat kisakoodarit käyttävät kuitenkin Kruskalin algoritmia. + + diff --git a/luku16.tex b/luku16.tex new file mode 100644 index 0000000..6743e34 --- /dev/null +++ b/luku16.tex @@ -0,0 +1,753 @@ +\chapter{Directed graphs} + +Tässä luvussa tutustumme kahteen suunnattujen +verkkojen alaluokkaan: + +\begin{itemize} +\item \key{Syklitön verkko}: +Tällaisessa verkossa ei ole yhtään sykliä, +eli mistään solmusta ei ole polkua takaisin +solmuun itseensä. +\item \key{Seuraajaverkko}: +Tällaisessa verkossa jokaisen solmun lähtöaste on 1 +eli kullakin solmulla on yksikäsitteinen seuraaja. +\end{itemize} +Osoittautuu, että kummassakin tapauksessa +verkon käsittely on yleistä verkkoa helpompaa +ja voimme käyttää tehokkaita algoritmeja, jotka +hyödyntävät oletuksia verkon rakenteesta. + +\section{Topologinen järjestys} + +\index{topologinen jxrjestys@topologinen järjestys} +\index{sykli@sykli} + +\key{Topologinen järjestys} on tapa +järjestää suunnatun verkon solmut niin, +että jos solmusta $a$ pääsee solmuun $b$, +niin $a$ on ennen $b$:tä järjestyksessä. +Esimerkiksi verkon +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,5) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; +\node[draw, circle] (6) at (5,3) {$6$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (4) -- (1); +\path[draw,thick,->,>=latex] (4) -- (5); +\path[draw,thick,->,>=latex] (5) -- (2); +\path[draw,thick,->,>=latex] (5) -- (3); +\path[draw,thick,->,>=latex] (3) -- (6); +\end{tikzpicture} +\end{center} +yksi topologinen järjestys on +$[4,1,5,2,3,6]$: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (-6,0) {$1$}; +\node[draw, circle] (2) at (-3,0) {$2$}; +\node[draw, circle] (3) at (-1.5,0) {$3$}; +\node[draw, circle] (4) at (-7.5,0) {$4$}; +\node[draw, circle] (5) at (-4.5,0) {$5$}; +\node[draw, circle] (6) at (-0,0) {$6$}; + +\path[draw,thick,->,>=latex] (1) edge [bend right=30] (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (4) -- (1); +\path[draw,thick,->,>=latex] (4) edge [bend left=30] (5); +\path[draw,thick,->,>=latex] (5) -- (2); +\path[draw,thick,->,>=latex] (5) edge [bend left=30] (3); +\path[draw,thick,->,>=latex] (3) -- (6); +\end{tikzpicture} +\end{center} + +Topologinen järjestys on olemassa +aina silloin, kun verkko on syklitön. +Jos taas verkossa on sykli, +topologista järjestystä ei voi muodostaa, +koska mitään syklissä olevaa solmua ei voi +valita ensimmäisenä topologiseen järjestykseen. +Seuraavaksi näemme, miten syvyyshaun avulla +voi muodostaa topologisen järjestyksen tai +todeta, että tämä ei ole mahdollista syklin takia. + +\subsubsection{Algoritmi} + +Ideana on käydä läpi verkon solmut ja aloittaa +solmusta uusi syvyyshaku aina silloin, kun solmua +ei ole vielä käsitelty. +Kunkin syvyyshaun aikana solmuilla on +kolme mahdollista tilaa: + +\begin{itemize} +\item tila 0: solmua ei ole käsitelty (valkoinen) +\item tila 1: solmun käsittely on alkanut (vaaleanharmaa) +\item tila 2: solmu on käsitelty (tummanharmaa) +\end{itemize} + +Aluksi jokaisen verkon solmun tila on 0. +Kun syvyyshaku saapuu solmuun, sen tilaksi tulee 1. +Lopuksi kun syvyyshaku on käsitellyt kaikki +solmun naapurit, solmun tilaksi tulee 2. + +Jos verkossa on sykli, tämä selviää syvyyshaun aikana siitä, +että jossain vaiheessa haku saapuu solmuun, +jonka tila on 1. Tässä tapauksessa topologista +järjestystä ei voi muodostaa. + +Jos verkossa ei ole sykliä, topologinen järjestys +saadaan muodostamalla lista, johon kukin solmu lisätään +silloin, kun sen tilaksi tulee 2. +Tämä lista käänteisenä on yksi verkon +topologinen järjestys. + +\subsubsection{Esimerkki 1} + +Esimerkkiverkossa syvyyshaku etenee ensin solmusta 1 +solmuun 6 asti: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle,fill=gray!20] (1) at (1,5) {$1$}; +\node[draw, circle,fill=gray!20] (2) at (3,5) {$2$}; +\node[draw, circle,fill=gray!20] (3) at (5,5) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; +\node[draw, circle,fill=gray!80] (6) at (5,3) {$6$}; + +\path[draw,thick,->,>=latex] (4) -- (1); +\path[draw,thick,->,>=latex] (4) -- (5); +\path[draw,thick,->,>=latex] (5) -- (2); +\path[draw,thick,->,>=latex] (5) -- (3); +%\path[draw,thick,->,>=latex] (3) -- (6); + +\path[draw=red,thick,->,line width=2pt] (1) -- (2); +\path[draw=red,thick,->,line width=2pt] (2) -- (3); +\path[draw=red,thick,->,line width=2pt] (3) -- (6); +\end{tikzpicture} +\end{center} + +Tässä vaiheessa solmu 6 on käsitelty, joten se lisätään listalle. +Sen jälkeen haku palaa takaisinpäin: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle,fill=gray!80] (1) at (1,5) {$1$}; +\node[draw, circle,fill=gray!80] (2) at (3,5) {$2$}; +\node[draw, circle,fill=gray!80] (3) at (5,5) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; +\node[draw, circle,fill=gray!80] (6) at (5,3) {$6$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (4) -- (1); +\path[draw,thick,->,>=latex] (4) -- (5); +\path[draw,thick,->,>=latex] (5) -- (2); +\path[draw,thick,->,>=latex] (5) -- (3); +\path[draw,thick,->,>=latex] (3) -- (6); +\end{tikzpicture} +\end{center} + +Tämän jälkeen listan sisältönä on $[6,3,2,1]$. +Sitten seuraava syvyyshaku alkaa solmusta 4: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle,fill=gray!80] (1) at (1,5) {$1$}; +\node[draw, circle,fill=gray!80] (2) at (3,5) {$2$}; +\node[draw, circle,fill=gray!80] (3) at (5,5) {$3$}; +\node[draw, circle,fill=gray!20] (4) at (1,3) {$4$}; +\node[draw, circle,fill=gray!80] (5) at (3,3) {$5$}; +\node[draw, circle,fill=gray!80] (6) at (5,3) {$6$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (4) -- (1); +%\path[draw,thick,->,>=latex] (4) -- (5); +\path[draw,thick,->,>=latex] (5) -- (2); +\path[draw,thick,->,>=latex] (5) -- (3); +\path[draw,thick,->,>=latex] (3) -- (6); + +\path[draw=red,thick,->,line width=2pt] (4) -- (5); +\end{tikzpicture} +\end{center} + +Tämän seurauksena listaksi tulee $[6,3,2,1,5,4]$. +Kaikki solmut on käyty läpi, joten topologinen järjestys on valmis. +Topologinen järjestys on lista käänteisenä eli $[4,5,1,2,3,6]$: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (3,0) {$1$}; +\node[draw, circle] (2) at (4.5,0) {$2$}; +\node[draw, circle] (3) at (6,0) {$3$}; +\node[draw, circle] (4) at (0,0) {$4$}; +\node[draw, circle] (5) at (1.5,0) {$5$}; +\node[draw, circle] (6) at (7.5,0) {$6$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (4) edge [bend left=30] (1); +\path[draw,thick,->,>=latex] (4) -- (5); +\path[draw,thick,->,>=latex] (5) edge [bend right=30] (2); +\path[draw,thick,->,>=latex] (5) edge [bend right=40] (3); +\path[draw,thick,->,>=latex] (3) -- (6); +\end{tikzpicture} +\end{center} + +Huomaa, että topologinen järjestys ei ole yksikäsitteinen, +vaan verkolla voi olla useita topologisia järjestyksiä. + +\subsubsection{Esimerkki 2} + +Tarkastellaan sitten tilannetta, jossa topologista järjestystä +ei voi muodostaa syklin takia. Näin on seuraavassa verkossa: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,5) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; +\node[draw, circle] (6) at (5,3) {$6$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (4) -- (1); +\path[draw,thick,->,>=latex] (4) -- (5); +\path[draw,thick,->,>=latex] (5) -- (2); +\path[draw,thick,->,>=latex] (3) -- (5); +\path[draw,thick,->,>=latex] (3) -- (6); +\end{tikzpicture} +\end{center} +Nyt syvyyshaun aikana tapahtuu näin: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle,fill=gray!20] (1) at (1,5) {$1$}; +\node[draw, circle,fill=gray!20] (2) at (3,5) {$2$}; +\node[draw, circle,fill=gray!20] (3) at (5,5) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle,fill=gray!20] (5) at (3,3) {$5$}; +\node[draw, circle] (6) at (5,3) {$6$}; + +\path[draw,thick,->,>=latex] (4) -- (1); +\path[draw,thick,->,>=latex] (4) -- (5); +\path[draw,thick,->,>=latex] (3) -- (6); + +\path[draw=red,thick,->,line width=2pt] (1) -- (2); +\path[draw=red,thick,->,line width=2pt] (2) -- (3); +\path[draw=red,thick,->,line width=2pt] (3) -- (5); +\path[draw=red,thick,->,line width=2pt] (5) -- (2); +\end{tikzpicture} +\end{center} +Syvyyshaku saapuu tilassa 1 olevaan solmuun 2, +mikä tarkoittaa, että verkossa on sykli. +Tässä tapauksessa sykli on $2 \rightarrow 3 \rightarrow 5 \rightarrow 2$. + +\section{Dynaaminen ohjelmointi} + +Jos suunnattu verkko on syklitön, +siihen voi soveltaa dynaamista ohjelmointia. +Tämän avulla voi ratkaista tehokkaasti +ajassa $O(n+m)$ esimerkiksi seuraavat +ongelmat koskien verkossa olevia polkuja +alkusolmusta loppusolmuun: +\begin{itemize} +\item montako erilaista polkua on olemassa? +\item mikä on lyhin/pisin polku? +\item mikä on pienin/suurin määrä kaaria polulla? +\item mitkä solmut esiintyvät varmasti polulla? +\end{itemize} + +\subsubsection{Polkujen määrän laskeminen} + +Lasketaan esimerkkinä polkujen määrä +alkusolmusta loppusolmuun suunnatussa, +syklittömässä verkossa. +Esimerkiksi verkossa +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,5) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; +\node[draw, circle] (6) at (5,3) {$6$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (4) -- (1); +\path[draw,thick,->,>=latex] (4) -- (5); +\path[draw,thick,->,>=latex] (5) -- (2); +\path[draw,thick,->,>=latex] (5) -- (3); +\path[draw,thick,->,>=latex] (3) -- (6); +\end{tikzpicture} +\end{center} +on 3 polkua solmusta 4 solmuun 6: +\begin{itemize} +\item $4 \rightarrow 1 \rightarrow 2 \rightarrow 3 \rightarrow 6$ +\item $4 \rightarrow 5 \rightarrow 2 \rightarrow 3 \rightarrow 6$ +\item $4 \rightarrow 5 \rightarrow 3 \rightarrow 6$ +\end{itemize} +Ideana on käydä läpi verkon solmut topologisessa järjestyksessä +ja laskea kunkin solmun kohdalla yhteen eri suunnista +tulevien polkujen määrät. +Verkon topologinen järjestys on seuraava: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (3,0) {$1$}; +\node[draw, circle] (2) at (4.5,0) {$2$}; +\node[draw, circle] (3) at (6,0) {$3$}; +\node[draw, circle] (4) at (0,0) {$4$}; +\node[draw, circle] (5) at (1.5,0) {$5$}; +\node[draw, circle] (6) at (7.5,0) {$6$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (4) edge [bend left=30] (1); +\path[draw,thick,->,>=latex] (4) -- (5); +\path[draw,thick,->,>=latex] (5) edge [bend right=30] (2); +\path[draw,thick,->,>=latex] (5) edge [bend right=40] (3); +\path[draw,thick,->,>=latex] (3) -- (6); +\end{tikzpicture} +\end{center} +Tuloksena ovat seuraavat lukumäärät: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,5) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; +\node[draw, circle] (6) at (5,3) {$6$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (4) -- (1); +\path[draw,thick,->,>=latex] (4) -- (5); +\path[draw,thick,->,>=latex] (5) -- (2); +\path[draw,thick,->,>=latex] (5) -- (3); +\path[draw,thick,->,>=latex] (3) -- (6); + +\node[color=red] at (1,2.3) {$1$}; +\node[color=red] at (3,2.3) {$1$}; +\node[color=red] at (5,2.3) {$3$}; +\node[color=red] at (1,5.7) {$1$}; +\node[color=red] at (3,5.7) {$2$}; +\node[color=red] at (5,5.7) {$3$}; +\end{tikzpicture} +\end{center} + +Esimerkiksi solmuun 2 pääsee solmuista 1 ja 5. +Kumpaankin solmuun päättyy yksi polku solmusta 4 alkaen, +joten solmuun 2 päättyy kaksi polkua solmusta 4 alkaen. +Vastaavasti solmuun 3 pääsee solmuista 2 ja 5, +joiden kautta tulee kaksi ja yksi polkua solmusta 4 alkaen. + +\subsubsection{Dijkstran algoritmin sovellukset} + +\index{Dijkstran algoritmi@Dijkstran algoritmi} + +Dijkstran algoritmin sivutuotteena syntyy suunnattu, syklitön verkko, +joka kertoo jokaiselle alkuperäisen verkon solmulle, +mitä tapoja alkusolmusta on päästä kyseiseen solmuun lyhintä +polkua käyttäen. +Tähän verkkoon voi soveltaa edelleen dynaamista ohjelmointia. +Esimerkiksi verkossa +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (0,0) {$1$}; +\node[draw, circle] (2) at (2,0) {$2$}; +\node[draw, circle] (3) at (0,-2) {$3$}; +\node[draw, circle] (4) at (2,-2) {$4$}; +\node[draw, circle] (5) at (4,-1) {$5$}; + +\path[draw,thick,-] (1) -- node[font=\small,label=above:3] {} (2); +\path[draw,thick,-] (1) -- node[font=\small,label=left:5] {} (3); +\path[draw,thick,-] (2) -- node[font=\small,label=right:4] {} (4); +\path[draw,thick,-] (2) -- node[font=\small,label=above:8] {} (5); +\path[draw,thick,-] (3) -- node[font=\small,label=below:2] {} (4); +\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5); +\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (3); +\end{tikzpicture} +\end{center} +solmusta 1 lähteviin lyhimpiin polkuihin kuuluvat +seuraavat kaaret: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (0,0) {$1$}; +\node[draw, circle] (2) at (2,0) {$2$}; +\node[draw, circle] (3) at (0,-2) {$3$}; +\node[draw, circle] (4) at (2,-2) {$4$}; +\node[draw, circle] (5) at (4,-1) {$5$}; + +\path[draw,thick,->] (1) -- node[font=\small,label=above:3] {} (2); +\path[draw,thick,->] (1) -- node[font=\small,label=left:5] {} (3); +\path[draw,thick,->] (2) -- node[font=\small,label=right:4] {} (4); +\path[draw,thick,->] (3) -- node[font=\small,label=below:2] {} (4); +\path[draw,thick,->] (4) -- node[font=\small,label=below:1] {} (5); +\path[draw,thick,->] (2) -- node[font=\small,label=above:2] {} (3); +\end{tikzpicture} +\end{center} + +Koska kyseessä on suunnaton, syklitön verkko, +siihen voi soveltaa dynaamista ohjelmointia. +Niinpä voi esimerkiksi laskea, montako lyhintä polkua +on olemassa solmusta 1 solmuun 5: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (0,0) {$1$}; +\node[draw, circle] (2) at (2,0) {$2$}; +\node[draw, circle] (3) at (0,-2) {$3$}; +\node[draw, circle] (4) at (2,-2) {$4$}; +\node[draw, circle] (5) at (4,-1) {$5$}; + +\path[draw,thick,->] (1) -- node[font=\small,label=above:3] {} (2); +\path[draw,thick,->] (1) -- node[font=\small,label=left:5] {} (3); +\path[draw,thick,->] (2) -- node[font=\small,label=right:4] {} (4); +\path[draw,thick,->] (3) -- node[font=\small,label=below:2] {} (4); +\path[draw,thick,->] (4) -- node[font=\small,label=below:1] {} (5); +\path[draw,thick,->] (2) -- node[font=\small,label=above:2] {} (3); + +\node[color=red] at (0,0.7) {$1$}; +\node[color=red] at (2,0.7) {$1$}; +\node[color=red] at (0,-2.7) {$2$}; +\node[color=red] at (2,-2.7) {$3$}; +\node[color=red] at (4,-1.7) {$3$}; +\end{tikzpicture} +\end{center} + +\subsubsection{Ongelman esitys verkkona} + +Itse asiassa mikä tahansa dynaamisen ohjelmoinnin +ongelma voidaan esittää suunnattuna, syklittömänä verkkona. +Tällaisessa verkossa solmuja ovat dynaamisen +ohjelmoinnin tilat ja kaaret kuvaavat, +miten tilat riippuvat toisistaan. + +Tarkastellaan esimerkkinä tuttua tehtävää, +jossa annettuna on rahamäärä $x$ +ja tehtävänä on muodostaa se kolikoista +$\{c_1,c_2,\ldots,c_k\}$. +Tässä tapauksessa voimme muodostaa verkon, jonka solmut +vastaavat rahamääriä ja kaaret kuvaavat +kolikkojen valintoja. +Esimerkiksi jos kolikot ovat $\{1,3,4\}$ +ja $x=6$, niin verkosta tulee seuraava: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (0) at (0,0) {$0$}; +\node[draw, circle] (1) at (2,0) {$1$}; +\node[draw, circle] (2) at (4,0) {$2$}; +\node[draw, circle] (3) at (6,0) {$3$}; +\node[draw, circle] (4) at (8,0) {$4$}; +\node[draw, circle] (5) at (10,0) {$5$}; +\node[draw, circle] (6) at (12,0) {$6$}; + +\path[draw,thick,->] (0) -- (1); +\path[draw,thick,->] (1) -- (2); +\path[draw,thick,->] (2) -- (3); +\path[draw,thick,->] (3) -- (4); +\path[draw,thick,->] (4) -- (5); +\path[draw,thick,->] (5) -- (6); + +\path[draw,thick,->] (0) edge [bend right=30] (3); +\path[draw,thick,->] (1) edge [bend right=30] (4); +\path[draw,thick,->] (2) edge [bend right=30] (5); +\path[draw,thick,->] (3) edge [bend right=30] (6); + +\path[draw,thick,->] (0) edge [bend left=30] (4); +\path[draw,thick,->] (1) edge [bend left=30] (5); +\path[draw,thick,->] (2) edge [bend left=30] (6); +\end{tikzpicture} +\end{center} + +Tätä verkkoesitystä käyttäen +lyhin polku solmusta 0 solmuun $x$ +vastaa ratkaisua, jossa kolikoita on +mahdollisimman vähän, ja polkujen yhteismäärä +solmusta 0 solmuun $x$ +on sama kuin ratkaisujen yhteismäärä. + +\section{Tehokas eteneminen} + +\index{seuraajaverkko@seuraajaverkko} +\index{funktionaalinen verkko@funktionaalinen verkko} + +Seuraavaksi oletamme, että +suunnaton verkko on \key{seuraajaverkko}, +jolloin jokaisen solmun lähtöaste on 1 +eli siitä lähtee tasan yksi kaari ulospäin. +Niinpä verkko muodostuu yhdestä tai useammasta +komponentista, joista jokaisessa on yksi sykli +ja joukko siihen johtavia polkuja. + +Seuraajaverkosta käytetään joskus nimeä +\key{funktionaalinen verkko}. +Tämä johtuu siitä, että jokaista seuraajaverkkoa +vastaa funktio $f$, joka määrittelee verkon kaaret. +Funktion parametrina on verkon solmu ja +se palauttaa solmusta lähtevän kaaren kohdesolmun. + +\begin{samepage} +Esimerkiksi funktio +\begin{center} +\begin{tabular}{r|rrrrrrrrr} +$x$ & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ +\hline +$f(x)$ & 3 & 5 & 7 & 6 & 2 & 2 & 1 & 6 & 3 \\ +\end{tabular} +\end{center} +\end{samepage} +määrittelee seuraavan verkon: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {$1$}; +\node[draw, circle] (2) at (2,0) {$2$}; +\node[draw, circle] (3) at (-2,0) {$3$}; +\node[draw, circle] (4) at (1,-3) {$4$}; +\node[draw, circle] (5) at (4,0) {$5$}; +\node[draw, circle] (6) at (2,-1.5) {$6$}; +\node[draw, circle] (7) at (-2,-1.5) {$7$}; +\node[draw, circle] (8) at (3,-3) {$8$}; +\node[draw, circle] (9) at (-4,0) {$9$}; + +\path[draw,thick,->] (1) -- (3); +\path[draw,thick,->] (2) edge [bend left=40] (5); +\path[draw,thick,->] (3) -- (7); +\path[draw,thick,->] (4) -- (6); +\path[draw,thick,->] (5) edge [bend left=40] (2); +\path[draw,thick,->] (6) -- (2); +\path[draw,thick,->] (7) -- (1); +\path[draw,thick,->] (8) -- (6); +\path[draw,thick,->] (9) -- (3); +\end{tikzpicture} +\end{center} + +Koska seuraajaverkon jokaisella solmulla +on yksikäsitteinen seuraaja, voimme määritellä funktion $f(x,k)$, +joka kertoo solmun, johon päätyy solmusta $x$ +kulkemalla $k$ askelta. +Esimerkiksi yllä olevassa verkossa $f(4,6)=2$, +koska solmusta 4 päätyy solmuun 2 kulkemalla 6 askelta: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {$4$}; +\node[draw, circle] (2) at (1.5,0) {$6$}; +\node[draw, circle] (3) at (3,0) {$2$}; +\node[draw, circle] (4) at (4.5,0) {$5$}; +\node[draw, circle] (5) at (6,0) {$2$}; +\node[draw, circle] (6) at (7.5,0) {$5$}; +\node[draw, circle] (7) at (9,0) {$2$}; + +\path[draw,thick,->] (1) -- (2); +\path[draw,thick,->] (2) -- (3); +\path[draw,thick,->] (3) -- (4); +\path[draw,thick,->] (4) -- (5); +\path[draw,thick,->] (5) -- (6); +\path[draw,thick,->] (6) -- (7); +\end{tikzpicture} +\end{center} + +Suoraviivainen tapa laskea arvo $f(x,k)$ +on käydä läpi polku askel askeleelta, mihin kuluu aikaa $O(k)$. +Sopivan esikäsittelyn avulla voimme laskea kuitenkin +minkä tahansa arvon $f(x,k)$ ajassa $O(\log k)$. + +Ideana on laskea etukäteen kaikki arvot $f(x,k)$, kun $k$ on 2:n potenssi +ja enintään $u$, missä $u$ on suurin mahdollinen määrä +askeleita, joista olemme kiinnostuneita. +Tämä onnistuu tehokkaasti, koska voimme käyttää rekursiota + +\begin{equation*} + f(x,k) = \begin{cases} + f(x) & k = 1\\ + f(f(x,k/2),k/2) & k > 1\\ + \end{cases} +\end{equation*} + +Arvojen $f(x,k)$ esilaskenta vie aikaa $O(n \log u)$, +koska jokaisesta solmusta lasketaan $O(\log u)$ arvoa. +Esimerkin tapauksessa taulukko alkaa muodostua seuraavasti: + +\begin{center} +\begin{tabular}{r|rrrrrrrrr} +$x$ & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ +\hline +$f(x,1)$ & 3 & 5 & 7 & 6 & 2 & 2 & 1 & 6 & 3 \\ +$f(x,2)$ & 7 & 2 & 1 & 2 & 5 & 5 & 3 & 2 & 7 \\ +$f(x,4)$ & 3 & 2 & 7 & 2 & 5 & 5 & 1 & 2 & 3 \\ +$f(x,8)$ & 7 & 2 & 1 & 2 & 5 & 5 & 3 & 2 & 7 \\ +$\cdots$ \\ +\end{tabular} +\end{center} + +Tämän jälkeen funktion $f(x,k)$ arvon saa laskettua +esittämällä luvun $k$ summana 2:n potensseja. +Esimerkiksi jos haluamme laskea arvon $f(x,11)$, +muodostamme ensin esityksen $11=8+2+1$. +Tämän ansiosta +\[f(x,11)=f(f(f(x,8),2),1).\] +Esimerkiksi yllä olevassa verkossa +\[f(4,11)=f(f(f(4,8),2),1)=5.\] + +Tällaisessa esityksessä on aina +$O(\log k)$ osaa, joten arvon $f(x,k)$ laskemiseen +kuluu aikaa $O(\log k)$. + +\section{Syklin tunnistaminen} + +\index{sykli@sykli} +\index{syklin tunnistaminen@syklin tunnistaminen} + +Kiinnostavia kysymyksiä seuraajaverkossa ovat, +minkä solmun kohdalla saavutaan sykliin +solmusta $x$ lähdettäessä +ja montako solmua kyseiseen sykliin kuuluu. +Esimerkiksi verkossa + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (5) at (0,0) {$5$}; +\node[draw, circle] (4) at (-2,0) {$4$}; +\node[draw, circle] (6) at (-1,1.5) {$6$}; +\node[draw, circle] (3) at (-4,0) {$3$}; +\node[draw, circle] (2) at (-6,0) {$2$}; +\node[draw, circle] (1) at (-8,0) {$1$}; + +\path[draw,thick,->] (1) -- (2); +\path[draw,thick,->] (2) -- (3); +\path[draw,thick,->] (3) -- (4); +\path[draw,thick,->] (4) -- (5); +\path[draw,thick,->] (5) -- (6); +\path[draw,thick,->] (6) -- (4); +\end{tikzpicture} +\end{center} +solmusta 1 lähdettäessä ensimmäinen sykliin kuuluva +solmu on solmu 4 ja syklissä on kolme solmua +(solmut 4, 5 ja 6). + +Helppo tapa tunnistaa sykli on alkaa kulkea verkossa +solmusta $x$ alkaen ja pitää kirjaa kaikista vastaan tulevista +solmuista. Kun jokin solmu tulee vastaan toista kertaa, +sykli on löytynyt. Tämän menetelmän aikavaativuus on $O(n)$ +ja muistia kuluu myös $O(n)$. + +Osoittautuu kuitenkin, että syklin tunnistamiseen on +olemassa parempia algoritmeja. +Niissä aikavaativuus on edelleen $O(n)$, +mutta muistia kuluu vain $O(1)$. +Tästä on merkittävää hyötyä, jos $n$ on suuri. +Tutustumme seuraavaksi Floydin algoritmiin, +joka saavuttaa nämä ominaisuudet. + +\subsubsection{Floydin algoritmi} + +\index{Floydin algoritmi@Floydin algoritmi} + +\key{Floydin algoritmi} kulkee verkossa eteenpäin rinnakkain +kahdesta kohdasta. +Algoritmissa on kaksi osoitinta $a$ ja $b$, +jotka molemmat ovat ensin alkusolmussa. +Osoitin $a$ liikkuu joka vuorolla askeleen eteenpäin, +kun taas osoitin $b$ liikkuu kaksi askelta eteenpäin. +Haku jatkuu, kunnes osoittimet kohtaavat: + +\begin{lstlisting} +a = f(x); +b = f(f(x)); +while (a != b) { + a = f(a); + b = f(f(b)); +} +\end{lstlisting} + +Tässä vaiheessa osoitin $a$ on kulkenut $k$ askelta +ja osoitin $b$ on kulkenut $2k$ askelta, +missä $k$ on jaollinen syklin pituudella. +Niinpä ensimmäinen sykliin kuuluva solmu löytyy siirtämällä +osoitin $a$ alkuun ja liikuttamalla osoittimia +rinnakkain eteenpäin, kunnes ne kohtaavat: + +\begin{lstlisting} +a = x; +while (a != b) { + a = f(a); + b = f(b); +} +\end{lstlisting} + +Nyt $a$ ja $b$ osoittavat ensimmäiseen syklin +solmuun, joka tulee vastaan solmusta $x$ lähdettäessä. +Lopuksi syklin pituus $c$ voidaan laskea näin: + +\begin{lstlisting} +b = f(a); +c = 1; +while (a != b) { + b = f(b); + c++; +} +\end{lstlisting} +% +% \subsubsection{Algoritmi 2 (Brent)} +% +% \index{Brentin algoritmi@Brentin algoritmi} +% +% Brentin algoritmi +% muodostuu peräkkäisistä vaiheista, joissa osoitin $a$ pysyy +% paikallaan ja osoitin $b$ liikkuu $k$ askelta +% Alussa $k=1$ ja $k$:n arvo kaksinkertaistuu +% joka vaiheen alussa. +% Lisäksi $a$ siirretään $b$:n kohdalle vaiheen alussa. +% Näin jatketaan, kunnes osoittimet kohtaavat. +% +% \begin{lstlisting} +% a = x; +% b = f(x); +% c = k = 1; +% while (a != b) { +% if (c == k) { +% a = b; +% c = 0; +% k *= 2; +% } +% b = f(b); +% c++; +% } +% \end{lstlisting} +% +% Nyt tiedossa on, että syklin pituus on $c$. +% Tämän jälkeen ensimmäinen sykliin kuuluva solmu löytyy +% palauttamalla ensin osoittimet alkuun, +% siirtämällä sitten osoitinta $b$ eteenpäin $c$ askelta +% ja liikuttamalla lopuksi osoittimia rinnakkain, +% kunnes ne osoittavat samaan solmuun. +% +% \begin{lstlisting} +% a = b = x; +% for (int i = 0; i < c; i++) b = f(b); +% while (a != b) { +% a = f(a); +% b = f(b); +% } +% \end{lstlisting} +% +% Nyt $a$ ja $b$ osoittavat ensimmäiseen sykliin kuuluvaan solmuun. +% +% Brentin algoritmin etuna Floydin algoritmiin verrattuna on, +% että se kutsuu funktiota $f$ harvemmin. +% Floydin algoritmi kutsuu funktiota $f$ ensimmäisessä silmukassa +% kolmesti joka kierroksella, kun taas Brentin algoritmi +% kutsuu funktiota $f$ vain kerran kierrosta kohden. + diff --git a/luku17.tex b/luku17.tex new file mode 100644 index 0000000..f9d1e2a --- /dev/null +++ b/luku17.tex @@ -0,0 +1,548 @@ +\chapter{Strongly connectivity} + +\index{vahvasti yhtenxinen verkko@vahvasti yhtenäinen verkko} + +Suunnatussa verkossa yhtenäisyys ei takaa sitä, +että kahden solmun välillä olisi olemassa polkua, +koska kaarten suunnat rajoittavat liikkumista. +Niinpä suunnattuja verkkoja varten on mielekästä +määritellä uusi käsite, +joka vaatii enemmän verkon yhtenäisyydeltä. + +Verkko on \key{vahvasti yhtenäinen}, +jos mistä tahansa solmusta on olemassa polku +kaikkiin muihin solmuihin. +Esimerkiksi seuraavassa kuvassa vasen +verkko on vahvasti yhtenäinen, +kun taas oikea verkko ei ole. + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,1) {$1$}; +\node[draw, circle] (2) at (3,1) {$2$}; +\node[draw, circle] (3) at (1,-1) {$3$}; +\node[draw, circle] (4) at (3,-1) {$4$}; + +\path[draw,thick,->] (1) -- (2); +\path[draw,thick,->] (2) -- (4); +\path[draw,thick,->] (4) -- (3); +\path[draw,thick,->] (3) -- (1); + +\node[draw, circle] (1b) at (6,1) {$1$}; +\node[draw, circle] (2b) at (8,1) {$2$}; +\node[draw, circle] (3b) at (6,-1) {$3$}; +\node[draw, circle] (4b) at (8,-1) {$4$}; + +\path[draw,thick,->] (1b) -- (2b); +\path[draw,thick,->] (2b) -- (4b); +\path[draw,thick,->] (4b) -- (3b); +\path[draw,thick,->] (1b) -- (3b); +\end{tikzpicture} +\end{center} + +Oikea verkko ei ole vahvasti yhtenäinen, +koska esimerkiksi solmusta 2 ei ole +polkua solmuun 1. + +\index{vahvasti yhtenxinen komponentti@vahvasti yhtenäinen komponentti} +\index{komponenttiverkko@komponenttiverkko} + +Verkon \key{vahvasti yhtenäiset komponentit} +jakavat verkon solmut mahdollisimman +suuriin vahvasti yhtenäisiin osiin. +Verkon vahvasti yhtenäiset komponentit +muodostavat syklittömän \key{komponenttiverkon}, +joka kuvaa alkuperäisen verkon syvärakennetta. + +Esimerkiksi verkon +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle] (1) at (-1,1) {$7$}; +\node[draw, circle] (2) at (-3,2) {$3$}; +\node[draw, circle] (4) at (-5,2) {$2$}; +\node[draw, circle] (6) at (-7,2) {$1$}; +\node[draw, circle] (3) at (-3,0) {$6$}; +\node[draw, circle] (5) at (-5,0) {$5$}; +\node[draw, circle] (7) at (-7,0) {$4$}; + +\path[draw,thick,->] (2) -- (1); +\path[draw,thick,->] (1) -- (3); +\path[draw,thick,->] (3) -- (2); +\path[draw,thick,->] (2) -- (4); +\path[draw,thick,->] (3) -- (5); +\path[draw,thick,->] (4) edge [bend left] (6); +\path[draw,thick,->] (6) edge [bend left] (4); +\path[draw,thick,->] (4) -- (5); +\path[draw,thick,->] (5) -- (7); +\path[draw,thick,->] (6) -- (7); +\end{tikzpicture} +\end{center} +vahvasti yhtenäiset komponentit ovat +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (-1,1) {$7$}; +\node[draw, circle] (2) at (-3,2) {$3$}; +\node[draw, circle] (4) at (-5,2) {$2$}; +\node[draw, circle] (6) at (-7,2) {$1$}; +\node[draw, circle] (3) at (-3,0) {$6$}; +\node[draw, circle] (5) at (-5,0) {$5$}; +\node[draw, circle] (7) at (-7,0) {$4$}; + +\path[draw,thick,->] (2) -- (1); +\path[draw,thick,->] (1) -- (3); +\path[draw,thick,->] (3) -- (2); +\path[draw,thick,->] (2) -- (4); +\path[draw,thick,->] (3) -- (5); +\path[draw,thick,->] (4) edge [bend left] (6); +\path[draw,thick,->] (6) edge [bend left] (4); +\path[draw,thick,->] (4) -- (5); +\path[draw,thick,->] (5) -- (7); +\path[draw,thick,->] (6) -- (7); + +\draw [red,thick,dashed,line width=2pt] (-0.5,2.5) rectangle (-3.5,-0.5); +\draw [red,thick,dashed,line width=2pt] (-4.5,2.5) rectangle (-7.5,1.5); +\draw [red,thick,dashed,line width=2pt] (-4.5,0.5) rectangle (-5.5,-0.5); +\draw [red,thick,dashed,line width=2pt] (-6.5,0.5) rectangle (-7.5,-0.5); +\end{tikzpicture} +\end{center} +ja ne muodostavat seuraavan komponenttiverkon: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (-3,1) {$B$}; +\node[draw, circle] (2) at (-6,2) {$A$}; +\node[draw, circle] (3) at (-5,0) {$D$}; +\node[draw, circle] (4) at (-7,0) {$C$}; + +\path[draw,thick,->] (1) -- (2); +\path[draw,thick,->] (1) -- (3); +\path[draw,thick,->] (2) -- (3); +\path[draw,thick,->] (2) -- (4); +\path[draw,thick,->] (3) -- (4); +\end{tikzpicture} +\end{center} +Komponentit ovat $A=\{1,2\}$, +$B=\{3,6,7\}$, $C=\{4\}$ sekä $D=\{5\}$. + +Komponenttiverkko on syklitön, suunnattu verkko, +jonka käsittely on alkuperäistä verkkoa +helpompaa, koska siinä ei ole syklejä. +Niinpä komponenttiverkolle voi muodostaa +luvun 16 tapaan topologisen järjestyksen +ja soveltaa sen jälkeen dynaamista ohjelmintia +verkon käsittelyyn. + +\section{Kosarajun algoritmi} + +\index{Kosarajun algoritmi@Kosarajun algoritmi} + +\key{Kosarajun algoritmi} on tehokas +menetelmä verkon +vahvasti yhtenäisten komponenttien etsimiseen. +Se suorittaa verkkoon +kaksi syvyyshakua, joista ensimmäinen +kerää solmut listaan verkon rakenteen perusteella +ja toinen muodostaa vahvasti yhtenäiset komponentit. + +\subsubsection{Syvyyshaku 1} + +Algoritmin ensimmäinen vaihe muodostaa listan solmuista +syvyyshaun käsittelyjärjestyksessä. +Algoritmi käy solmut läpi yksi kerrallaan, +ja jos solmua ei ole vielä käsitelty, algoritmi suorittaa +solmusta alkaen syvyyshaun. +Solmu lisätään listalle, kun syvyyshaku on +käsitellyt kaikki siitä lähtevät kaaret. + +Esimerkkiverkossa solmujen käsittelyjärjestys on: +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle] (1) at (-1,1) {$7$}; +\node[draw, circle] (2) at (-3,2) {$3$}; +\node[draw, circle] (4) at (-5,2) {$2$}; +\node[draw, circle] (6) at (-7,2) {$1$}; +\node[draw, circle] (3) at (-3,0) {$6$}; +\node[draw, circle] (5) at (-5,0) {$5$}; +\node[draw, circle] (7) at (-7,0) {$4$}; + +\node at (-7,2.75) {$1/8$}; +\node at (-5,2.75) {$2/7$}; +\node at (-3,2.75) {$9/14$}; +\node at (-7,-0.75) {$4/5$}; +\node at (-5,-0.75) {$3/6$}; +\node at (-3,-0.75) {$11/12$}; +\node at (-1,1.75) {$10/13$}; + +\path[draw,thick,->] (2) -- (1); +\path[draw,thick,->] (1) -- (3); +\path[draw,thick,->] (3) -- (2); +\path[draw,thick,->] (2) -- (4); +\path[draw,thick,->] (3) -- (5); +\path[draw,thick,->] (4) edge [bend left] (6); +\path[draw,thick,->] (6) edge [bend left] (4); +\path[draw,thick,->] (4) -- (5); +\path[draw,thick,->] (5) -- (7); +\path[draw,thick,->] (6) -- (7); +\end{tikzpicture} +\end{center} + +Solmun kohdalla oleva merkintä $x/y$ tarkoittaa, että solmun +käsittely syvyyshaussa alkoi hetkellä $x$ ja päättyi hetkellä $y$. +Kun solmut järjestetään käsittelyn päättymisajan +mukaan, tuloksena on seuraava järjestys: + +\begin{tabular}{ll} +\\ +solmu & päättymisaika \\ +\hline +4 & 5 \\ +5 & 6 \\ +2 & 7 \\ +1 & 8 \\ +6 & 12 \\ +7 & 13 \\ +3 & 14 \\ +\\ +\end{tabular} + +Solmujen käsittelyjärjestys algoritmin seuraavassa vaiheessa +tulee olemaan tämä järjestys käänteisenä eli $[3,7,6,1,2,5,4]$. + +\subsubsection{Syvyyshaku 2} + +Algoritmin toinen vaihe muodostaa verkon vahvasti +yhtenäiset komponentit. +Ennen toista syvyyshakua algoritmi muuttaa +jokaisen kaaren suunnan käänteiseksi. +Tämä varmistaa, +että toisen syvyyshaun aikana löydetään +joka kerta vahvasti yhtenäinen komponentti, +johon ei kuulu ylimääräisiä solmuja. + +Esimerkkiverkko on käännettynä seuraava: + +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle] (1) at (-1,1) {$7$}; +\node[draw, circle] (2) at (-3,2) {$3$}; +\node[draw, circle] (4) at (-5,2) {$2$}; +\node[draw, circle] (6) at (-7,2) {$1$}; +\node[draw, circle] (3) at (-3,0) {$6$}; +\node[draw, circle] (5) at (-5,0) {$5$}; +\node[draw, circle] (7) at (-7,0) {$4$}; + +\path[draw,thick,<-] (2) -- (1); +\path[draw,thick,<-] (1) -- (3); +\path[draw,thick,<-] (3) -- (2); +\path[draw,thick,<-] (2) -- (4); +\path[draw,thick,<-] (3) -- (5); +\path[draw,thick,<-] (4) edge [bend left] (6); +\path[draw,thick,<-] (6) edge [bend left] (4); +\path[draw,thick,<-] (4) -- (5); +\path[draw,thick,<-] (5) -- (7); +\path[draw,thick,<-] (6) -- (7); +\end{tikzpicture} +\end{center} + +Tämän jälkeen algoritmi käy läpi +solmut käänteisessä ensimmäisen syvyyshaun +tuottamassa järjestyksessä. +Jos solmu ei kuulu vielä komponenttiin, +siitä alkaa uusi syvyyshaku. +Solmun komponenttiin liitetään kaikki aiemmin +käsittelemättömät solmut, +joihin syvyyshaku pääsee solmusta. + +Esimerkkiverkossa muodostuu ensin komponentti +solmusta 3 alkaen: + +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle] (1) at (-1,1) {$7$}; +\node[draw, circle] (2) at (-3,2) {$3$}; +\node[draw, circle] (4) at (-5,2) {$2$}; +\node[draw, circle] (6) at (-7,2) {$1$}; +\node[draw, circle] (3) at (-3,0) {$6$}; +\node[draw, circle] (5) at (-5,0) {$5$}; +\node[draw, circle] (7) at (-7,0) {$4$}; + +\path[draw,thick,<-] (2) -- (1); +\path[draw,thick,<-] (1) -- (3); +\path[draw,thick,<-] (3) -- (2); +\path[draw,thick,<-] (2) -- (4); +\path[draw,thick,<-] (3) -- (5); +\path[draw,thick,<-] (4) edge [bend left] (6); +\path[draw,thick,<-] (6) edge [bend left] (4); +\path[draw,thick,<-] (4) -- (5); +\path[draw,thick,<-] (5) -- (7); +\path[draw,thick,<-] (6) -- (7); + +\draw [red,thick,dashed,line width=2pt] (-0.5,2.5) rectangle (-3.5,-0.5); +%\draw [red,thick,dashed,line width=2pt] (-4.5,2.5) rectangle (-7.5,1.5); +%\draw [red,thick,dashed,line width=2pt] (-4.5,0.5) rectangle (-5.5,-0.5); +%\draw [red,thick,dashed,line width=2pt] (-6.5,0.5) rectangle (-7.5,-0.5); +\end{tikzpicture} +\end{center} + +Huomaa, että kaarten kääntämisen ansiosta komponentti +ei pääse ''vuotamaan'' muihin verkon osiin. + +\begin{samepage} +Sitten listalla ovat solmut 7 ja 6, mutta ne on jo liitetty +komponenttiin. +Seuraava uusi solmu on 1, josta muodostuu uusi komponentti: + +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle] (1) at (-1,1) {$7$}; +\node[draw, circle] (2) at (-3,2) {$3$}; +\node[draw, circle] (4) at (-5,2) {$2$}; +\node[draw, circle] (6) at (-7,2) {$1$}; +\node[draw, circle] (3) at (-3,0) {$6$}; +\node[draw, circle] (5) at (-5,0) {$5$}; +\node[draw, circle] (7) at (-7,0) {$4$}; + +\path[draw,thick,<-] (2) -- (1); +\path[draw,thick,<-] (1) -- (3); +\path[draw,thick,<-] (3) -- (2); +\path[draw,thick,<-] (2) -- (4); +\path[draw,thick,<-] (3) -- (5); +\path[draw,thick,<-] (4) edge [bend left] (6); +\path[draw,thick,<-] (6) edge [bend left] (4); +\path[draw,thick,<-] (4) -- (5); +\path[draw,thick,<-] (5) -- (7); +\path[draw,thick,<-] (6) -- (7); + +\draw [red,thick,dashed,line width=2pt] (-0.5,2.5) rectangle (-3.5,-0.5); +\draw [red,thick,dashed,line width=2pt] (-4.5,2.5) rectangle (-7.5,1.5); +%\draw [red,thick,dashed,line width=2pt] (-4.5,0.5) rectangle (-5.5,-0.5); +%\draw [red,thick,dashed,line width=2pt] (-6.5,0.5) rectangle (-7.5,-0.5); +\end{tikzpicture} +\end{center} +\end{samepage} + +Viimeisenä algoritmi käsittelee solmut 5 ja 4, +jotka tuottavat loput vahvasti yhtenäiset komponentit: + +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle] (1) at (-1,1) {$7$}; +\node[draw, circle] (2) at (-3,2) {$3$}; +\node[draw, circle] (4) at (-5,2) {$2$}; +\node[draw, circle] (6) at (-7,2) {$1$}; +\node[draw, circle] (3) at (-3,0) {$6$}; +\node[draw, circle] (5) at (-5,0) {$5$}; +\node[draw, circle] (7) at (-7,0) {$4$}; + +\path[draw,thick,<-] (2) -- (1); +\path[draw,thick,<-] (1) -- (3); +\path[draw,thick,<-] (3) -- (2); +\path[draw,thick,<-] (2) -- (4); +\path[draw,thick,<-] (3) -- (5); +\path[draw,thick,<-] (4) edge [bend left] (6); +\path[draw,thick,<-] (6) edge [bend left] (4); +\path[draw,thick,<-] (4) -- (5); +\path[draw,thick,<-] (5) -- (7); +\path[draw,thick,<-] (6) -- (7); + +\draw [red,thick,dashed,line width=2pt] (-0.5,2.5) rectangle (-3.5,-0.5); +\draw [red,thick,dashed,line width=2pt] (-4.5,2.5) rectangle (-7.5,1.5); +\draw [red,thick,dashed,line width=2pt] (-4.5,0.5) rectangle (-5.5,-0.5); +\draw [red,thick,dashed,line width=2pt] (-6.5,0.5) rectangle (-7.5,-0.5); +\end{tikzpicture} +\end{center} + +Algoritmin aikavaativuus on $O(n+m)$, +missä $n$ on solmujen määrä ja $m$ on kaarten määrä. +Tämä johtuu siitä, +että algoritmi suorittaa kaksi syvyyshakua ja +kummankin haun aikavaativuus on $O(n+m)$. + +\section{2SAT-ongelma} + +\index{2SAT-ongelma} + +Vahvasti yhtenäisyys liittyy myös \key{2SAT-ongelman} ratkaisemiseen. +Siinä annettuna on looginen lauseke muotoa +\[ +(a_1 \lor b_1) \land (a_2 \lor b_2) \land \cdots \land (a_m \lor b_m), +\] +jossa jokainen $a_i$ ja $b_i$ on joko looginen muuttuja +($x_1,x_2,\ldots,x_n$) +tai loogisen muuttujan negaatio ($\lnot x_1, \lnot x_2, \ldots, \lnot x_n$). +Merkit ''$\land$'' ja ''$\lor$'' tarkoittavat +loogisia operaatioita ''ja'' ja ''tai''. +Tehtävänä on valita muuttujille arvot niin, +että lauseke on tosi, +tai todeta, että tämä ei ole mahdollista. + +Esimerkiksi lauseke +\[ +L_1 = (x_2 \lor \lnot x_1) \land + (\lnot x_1 \lor \lnot x_2) \land + (x_1 \lor x_3) \land + (\lnot x_2 \lor \lnot x_3) \land + (x_1 \lor x_4) +\] +on tosi, kun $x_1$ ja $x_2$ ovat epätosia +ja $x_3$ ja $x_4$ ovat tosia. +Vastaavasti lauseke +\[ +L_2 = (x_1 \lor x_2) \land + (x_1 \lor \lnot x_2) \land + (\lnot x_1 \lor x_3) \land + (\lnot x_1 \lor \lnot x_3) +\] +on epätosi riippumatta muuttujien valinnasta. +Tämän näkee siitä, että muuttujalle $x_1$ +ei ole mahdollista arvoa, joka ei tuottaisi ristiriitaa. +Jos $x_1$ on epätosi, pitäisi päteä sekä $x_2$ että $\lnot x_2$, +mikä on mahdotonta. +Jos taas $x_1$ on tosi, +pitäisi päteä sekä $x_3$ että $\lnot x_3$, +mikä on myös mahdotonta. + +2SAT-ongelman saa muutettua verkoksi niin, +että jokainen muuttuja $x_i$ ja negaatio $\lnot x_i$ +on yksi verkon solmuista +ja muuttujien riippuvuudet ovat kaaria. +Jokaisesta parista $(a_i \lor b_i)$ tulee kaksi +kaarta: $\lnot a_i \to b_i$ sekä $\lnot b_i \to a_i$. +Nämä tarkoittavat, että jos $a_i$ ei päde, +niin $b_i$:n on pakko päteä, ja päinvastoin. + +Lausekkeen $L_1$ verkosta tulee nyt: +\\ +\begin{center} +\begin{tikzpicture}[scale=1.0,minimum size=2pt] +\node[draw, circle, inner sep=1.3pt] (1) at (1,2) {$\lnot x_3$}; +\node[draw, circle] (2) at (3,2) {$x_2$}; +\node[draw, circle, inner sep=1.3pt] (3) at (1,0) {$\lnot x_4$}; +\node[draw, circle] (4) at (3,0) {$x_1$}; +\node[draw, circle, inner sep=1.3pt] (5) at (5,2) {$\lnot x_1$}; +\node[draw, circle] (6) at (7,2) {$x_4$}; +\node[draw, circle, inner sep=1.3pt] (7) at (5,0) {$\lnot x_2$}; +\node[draw, circle] (8) at (7,0) {$x_3$}; + +\path[draw,thick,->] (1) -- (4); +\path[draw,thick,->] (4) -- (2); +\path[draw,thick,->] (2) -- (1); +\path[draw,thick,->] (3) -- (4); +\path[draw,thick,->] (2) -- (5); +\path[draw,thick,->] (4) -- (7); +\path[draw,thick,->] (5) -- (6); +\path[draw,thick,->] (5) -- (8); +\path[draw,thick,->] (8) -- (7); +\path[draw,thick,->] (7) -- (5); +\end{tikzpicture} +\end{center} + +Lausekkeen $L_2$ verkosta taas tulee: +\\ +\begin{center} +\begin{tikzpicture}[scale=1.0,minimum size=2pt] +\node[draw, circle] (1) at (1,2) {$x_3$}; +\node[draw, circle] (2) at (3,2) {$x_2$}; +\node[draw, circle, inner sep=1.3pt] (3) at (5,2) {$\lnot x_2$}; +\node[draw, circle, inner sep=1.3pt] (4) at (7,2) {$\lnot x_3$}; +\node[draw, circle, inner sep=1.3pt] (5) at (4,3.5) {$\lnot x_1$}; +\node[draw, circle] (6) at (4,0.5) {$x_1$}; + +\path[draw,thick,->] (1) -- (5); +\path[draw,thick,->] (4) -- (5); +\path[draw,thick,->] (6) -- (1); +\path[draw,thick,->] (6) -- (4); +\path[draw,thick,->] (5) -- (2); +\path[draw,thick,->] (5) -- (3); +\path[draw,thick,->] (2) -- (6); +\path[draw,thick,->] (3) -- (6); +\end{tikzpicture} +\end{center} + +Verkon rakenne kertoo, onko 2SAT-ongelmalla ratkaisua. +Jos on jokin muuttuja $x_i$ niin, +että $x_i$ ja $\lnot x_i$ ovat samassa +vahvasti yhtenäisessä komponentissa, +niin ratkaisua ei ole olemassa. +Tällöin verkossa on polku sekä $x_i$:stä +$\lnot x_i$:ään että $\lnot x_i$:stä $x_i$:ään, +eli kumman tahansa arvon valitseminen +muuttujalle $x_i$ pakottaisi myös valitsemaan +vastakkaisen arvon, mikä on ristiriita. +Jos taas verkossa ei ole tällaista +solmua $x_i$, ratkaisu on olemassa. + +Lausekkeen $L_1$ verkossa +mitkään solmut $x_i$ ja $\lnot x_i$ +eivät ole samassa vahvasti yhtenäisessä komponentissa, +mikä tarkoittaa, että ratkaisu on olemassa. +Lausekkeen $L_2$ verkossa taas kaikki solmut +ovat samassa vahvasti yhtenäisessä komponentissa, +eli ratkaisua ei ole olemassa. + +Jos ratkaisu on olemassa, muuttujien arvot saa selville +käymällä komponenttiverkko läpi käänteisessä +topologisessa järjestyksessä. +Verkosta otetaan käsittelyyn ja poistetaan +joka vaiheessa komponentti, +josta ei lähde kaaria muihin jäljellä +oleviin komponentteihin. +Jos komponentin muuttujille ei ole vielä valittu arvoja, +ne saavat komponentin mukaiset arvot. +Jos taas arvot on jo valittu, niitä ei muuteta. +Näin jatketaan, kunnes jokainen lausekkeen muuttuja on saanut arvon. + +Lausekkeen $L_1$ verkon komponenttiverkko on seuraava: +\begin{center} +\begin{tikzpicture}[scale=1.0] +\node[draw, circle] (1) at (0,0) {$A$}; +\node[draw, circle] (2) at (2,0) {$B$}; +\node[draw, circle] (3) at (4,0) {$C$}; +\node[draw, circle] (4) at (6,0) {$D$}; + +\path[draw,thick,->] (1) -- (2); +\path[draw,thick,->] (2) -- (3); +\path[draw,thick,->] (3) -- (4); +\end{tikzpicture} +\end{center} + +Komponentit ovat +$A = \{\lnot x_4\}$, +$B = \{x_1, x_2, \lnot x_3\}$, +$C = \{\lnot x_1, \lnot x_2, x_3\}$ sekä +$D = \{x_4\}$. +Ratkaisun muodostuksessa +käsitellään ensin komponentti $D$, +josta $x_4$ saa arvon tosi. +Sitten käsitellään komponentti $C$, +josta $x_1$ ja $x_2$ tulevat epätodeksi +ja $x_3$ tulee todeksi. +Kaikki muuttujat ovat saaneet arvon, +joten myöhemmin käsiteltävät +komponentit $B$ ja $A$ eivät vaikuta enää ratkaisuun. + +Huomaa, että tämän menetelmän toiminta +perustuu verkon erityiseen rakenteeseen. +Jos solmusta $x_i$ pääsee +solmuun $x_j$, +josta pääsee solmuun $\lnot x_j$, +niin $x_i$ ei saa koskaan arvoa tosi. +Tämä johtuu siitä, että +solmusta $\lnot x_j$ täytyy +päästä myös solmuun $\lnot x_i$, +koska kaikki riippuvuudet +ovat verkossa molempiin suuntiin. +Niinpä sekä $x_i$ että $x_j$ +saavat varmasti arvokseen epätosi. + +\index{3SAT-ongelma} + +2SAT-ongelman vaikeampi versio on \key{3SAT-ongelma}, +jossa jokainen lausekkeen osa on muotoa +$(a_i \lor b_i \lor c_i)$. +Tämän ongelman ratkaisemiseen \textit{ei} +tunneta tehokasta menetelmää, +vaan kyseessä on NP-vaikea ongelma. + + + + diff --git a/luku18.tex b/luku18.tex new file mode 100644 index 0000000..77723c6 --- /dev/null +++ b/luku18.tex @@ -0,0 +1,912 @@ +\chapter{Tree queries} + +\index{puukysely@puukysely} + +Tässä luvussa tutustumme algoritmeihin, +joiden avulla voi toteuttaa tehokkaasti kyselyitä +juurelliseen puuhun. +Kyselyt liittyvät puussa oleviin polkuihin +ja alipuihin. +Esimerkkejä kyselyistä ovat: + +\begin{itemize} +\item mikä solmu on $k$ askelta ylempänä solmua $x$? +\item mikä on solmun $x$ alipuun arvojen summa? +\item mikä on solmujen $a$ ja $b$ välisen polun arvojen summa? +\item mikä on solmujen $a$ ja $b$ alin yhteinen esivanhempi? +\end{itemize} + +\section{Tehokas nouseminen} + +Tehokas nouseminen puussa tarkoittaa, +että voimme selvittää nopeasti, +mihin solmuun päätyy kulkemalla +$k$ askelta ylöspäin solmusta $x$ alkaen. +Merkitään $f(x,k)$ solmua, +joka on $k$ askelta ylempänä solmua $x$. +Esimerkiksi seuraavassa puussa +$f(2,1)=1$ ja $f(8,2)=4$. + +\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$}; +\node[draw, circle] (8) at (-1,-3) {$8$}; +\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,-] (7) -- (8); + +\path[draw=red,thick,->,line width=2pt] (8) edge [bend left] (3); +\path[draw=red,thick,->,line width=2pt] (2) edge [bend right] (1); +\end{tikzpicture} +\end{center} + +Suoraviivainen tapa laskea funktion $f(x,k)$ +arvo on kulkea puussa $k$ askelta ylöspäin +solmusta $x$ alkaen. +Tämän aikavaativuus on kuitenkin $O(n)$, +koska on mahdollista, että puussa on +ketju, jossa on $O(n)$ solmua. + +Kuten luvussa 16.3, funktion $f(x,k)$ +arvo on mahdollista laskea tehokkaasti ajassa +$O(\log k)$ sopivan esikäsittelyn avulla. +Ideana on laskea etukäteen kaikki arvot +$f(x,k)$, joissa $k=1,2,4,8,\ldots$ eli 2:n potenssi. +Esimerkiksi yllä olevassa puussa muodostuu seuraava taulukko: + +\begin{center} +\begin{tabular}{r|rrrrrrrrr} +$x$ & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 \\ +\hline +$f(x,1)$ & 0 & 1 & 4 & 1 & 1 & 2 & 4 & 7 \\ +$f(x,2)$ & 0 & 0 & 1 & 0 & 0 & 1 & 1 & 4 \\ +$f(x,4)$ & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ +$\cdots$ \\ +\end{tabular} +\end{center} + +Taulukossa arvo 0 tarkoittaa, että nousemalla $k$ +askelta päätyy puun ulkopuolelle juurisolmun yläpuolelle. + +Esilaskenta vie aikaa $O(n \log n)$, koska jokaisesta +solmusta voi nousta korkeintaan $n$ askelta ylöspäin. +Tämän jälkeen minkä tahansa funktion $f(x,k)$ arvon saa +laskettua ajassa $O(\log k)$ jakamalla nousun 2:n +potenssin osiin. + +\section{Solmutaulukko} + +\index{solmutaulukko@solmutaulukko} + +\key{Solmutaulukko} sisältää juurellisen puun solmut siinä +järjestyksessä kuin juuresta alkava syvyyshaku +vierailee solmuissa. +Esimerkiksi puussa +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,3) {$1$}; +\node[draw, circle] (2) at (-3,1) {$2$}; +\node[draw, circle] (3) at (-1,1) {$3$}; +\node[draw, circle] (4) at (1,1) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (-3,-1) {$6$}; +\node[draw, circle] (7) at (-0.5,-1) {$7$}; +\node[draw, circle] (8) at (1,-1) {$8$}; +\node[draw, circle] (9) at (2.5,-1) {$9$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (1) -- (5); +\path[draw,thick,-] (2) -- (6); +\path[draw,thick,-] (4) -- (7); +\path[draw,thick,-] (4) -- (8); +\path[draw,thick,-] (4) -- (9); +\end{tikzpicture} +\end{center} +syvyyshaku etenee +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,3) {$1$}; +\node[draw, circle] (2) at (-3,1) {$2$}; +\node[draw, circle] (3) at (-1,1) {$3$}; +\node[draw, circle] (4) at (1,1) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (-3,-1) {$6$}; +\node[draw, circle] (7) at (-0.5,-1) {$7$}; +\node[draw, circle] (8) at (1,-1) {$8$}; +\node[draw, circle] (9) at (2.5,-1) {$9$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (1) -- (5); +\path[draw,thick,-] (2) -- (6); +\path[draw,thick,-] (4) -- (7); +\path[draw,thick,-] (4) -- (8); +\path[draw,thick,-] (4) -- (9); + + +\path[draw=red,thick,->,line width=2pt] (1) edge [bend right=15] (2); +\path[draw=red,thick,->,line width=2pt] (2) edge [bend right=15] (6); +\path[draw=red,thick,->,line width=2pt] (6) edge [bend right=15] (2); +\path[draw=red,thick,->,line width=2pt] (2) edge [bend right=15] (1); +\path[draw=red,thick,->,line width=2pt] (1) edge [bend right=15] (3); +\path[draw=red,thick,->,line width=2pt] (3) edge [bend right=15] (1); +\path[draw=red,thick,->,line width=2pt] (1) edge [bend right=15] (4); +\path[draw=red,thick,->,line width=2pt] (4) edge [bend right=15] (7); +\path[draw=red,thick,->,line width=2pt] (7) edge [bend right=15] (4); +\path[draw=red,thick,->,line width=2pt] (4) edge [bend right=15] (8); +\path[draw=red,thick,->,line width=2pt] (8) edge [bend right=15] (4); +\path[draw=red,thick,->,line width=2pt] (4) edge [bend right=15] (9); +\path[draw=red,thick,->,line width=2pt] (9) edge [bend right=15] (4); +\path[draw=red,thick,->,line width=2pt] (4) edge [bend right=15] (1); +\path[draw=red,thick,->,line width=2pt] (1) edge [bend right=15] (5); +\path[draw=red,thick,->,line width=2pt] (5) edge [bend right=15] (1); + +\end{tikzpicture} +\end{center} +ja solmutaulukoksi tulee: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (9,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$6$}; +\node at (3.5,0.5) {$3$}; +\node at (4.5,0.5) {$4$}; +\node at (5.5,0.5) {$7$}; +\node at (6.5,0.5) {$8$}; +\node at (7.5,0.5) {$9$}; +\node at (8.5,0.5) {$5$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\node at (8.5,1.4) {$9$}; +\end{tikzpicture} +\end{center} + +\subsubsection{Alipuiden käsittely} + +Solmutaulukossa jokaisen alipuun kaikki solmut ovat peräkkäin +niin, että ensin on alipuun juurisolmu ja sitten +kaikki muut alipuun solmut. +Esimerkiksi äskeisessä taulukossa solmun $4$ +alipuuta vastaa seuraava taulukon osa: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (4,0) rectangle (8,1); +\draw (0,0) grid (9,1); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$6$}; +\node at (3.5,0.5) {$3$}; +\node at (4.5,0.5) {$4$}; +\node at (5.5,0.5) {$7$}; +\node at (6.5,0.5) {$8$}; +\node at (7.5,0.5) {$9$}; +\node at (8.5,0.5) {$5$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\node at (8.5,1.4) {$9$}; +\end{tikzpicture} +\end{center} +Tämän ansiosta solmutaulukon avulla voi käsitellä tehokkaasti +puun alipuihin liittyviä kyselyitä. +Ratkaistaan esimerkkinä tehtävä, +jossa kuhunkin puun solmuun liittyy +arvo ja toteutettavana on seuraavat kyselyt: +\begin{itemize} +\item muuta solmun $x$ arvoa +\item laske arvojen summa solmun $x$ alipuussa +\end{itemize} + +Tarkastellaan seuraavaa puuta, +jossa siniset luvut ovat solmujen arvoja. +Esimerkiksi solmun $4$ alipuun arvojen summa on $3+4+3+1=11$. + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,3) {$1$}; +\node[draw, circle] (2) at (-3,1) {$2$}; +\node[draw, circle] (3) at (-1,1) {$3$}; +\node[draw, circle] (4) at (1,1) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (-3,-1) {$6$}; +\node[draw, circle] (7) at (-0.5,-1) {$7$}; +\node[draw, circle] (8) at (1,-1) {$8$}; +\node[draw, circle] (9) at (2.5,-1) {$9$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (1) -- (5); +\path[draw,thick,-] (2) -- (6); +\path[draw,thick,-] (4) -- (7); +\path[draw,thick,-] (4) -- (8); +\path[draw,thick,-] (4) -- (9); + +\node[color=blue] at (0,3+0.65) {2}; +\node[color=blue] at (-3-0.65,1) {3}; +\node[color=blue] at (-1-0.65,1) {5}; +\node[color=blue] at (1+0.65,1) {3}; +\node[color=blue] at (3+0.65,1) {1}; +\node[color=blue] at (-3,-1-0.65) {4}; +\node[color=blue] at (-0.5,-1-0.65) {4}; +\node[color=blue] at (1,-1-0.65) {3}; +\node[color=blue] at (2.5,-1-0.65) {1}; +\end{tikzpicture} +\end{center} + +Ideana on luoda solmutaulukko, joka sisältää jokaisesta solmusta +kolme tietoa: (1) solmun tunnus, (2) alipuun koko ja (3) solmun arvo. +Esimerkiksi yllä olevasta puusta syntyy seuraava taulukko: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,1) grid (9,-2); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$6$}; +\node at (3.5,0.5) {$3$}; +\node at (4.5,0.5) {$4$}; +\node at (5.5,0.5) {$7$}; +\node at (6.5,0.5) {$8$}; +\node at (7.5,0.5) {$9$}; +\node at (8.5,0.5) {$5$}; + +\node at (0.5,-0.5) {$9$}; +\node at (1.5,-0.5) {$2$}; +\node at (2.5,-0.5) {$1$}; +\node at (3.5,-0.5) {$1$}; +\node at (4.5,-0.5) {$4$}; +\node at (5.5,-0.5) {$1$}; +\node at (6.5,-0.5) {$1$}; +\node at (7.5,-0.5) {$1$}; +\node at (8.5,-0.5) {$1$}; + +\node at (0.5,-1.5) {$2$}; +\node at (1.5,-1.5) {$3$}; +\node at (2.5,-1.5) {$4$}; +\node at (3.5,-1.5) {$5$}; +\node at (4.5,-1.5) {$3$}; +\node at (5.5,-1.5) {$4$}; +\node at (6.5,-1.5) {$3$}; +\node at (7.5,-1.5) {$1$}; +\node at (8.5,-1.5) {$1$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\node at (8.5,1.4) {$9$}; +\end{tikzpicture} +\end{center} + +Tästä taulukosta alipuun solmujen arvojen summa selviää +lukemalla ensin alipuun koko ja sitten sitä vastaavat solmut. +Esimerkiksi solmun $4$ alipuun arvojen summa selviää näin: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (4,1) rectangle (5,0); +\fill[color=lightgray] (4,0) rectangle (5,-1); +\fill[color=lightgray] (4,-1) rectangle (8,-2); +\draw (0,1) grid (9,-2); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$6$}; +\node at (3.5,0.5) {$3$}; +\node at (4.5,0.5) {$4$}; +\node at (5.5,0.5) {$7$}; +\node at (6.5,0.5) {$8$}; +\node at (7.5,0.5) {$9$}; +\node at (8.5,0.5) {$5$}; + +\node at (0.5,-0.5) {$9$}; +\node at (1.5,-0.5) {$2$}; +\node at (2.5,-0.5) {$1$}; +\node at (3.5,-0.5) {$1$}; +\node at (4.5,-0.5) {$4$}; +\node at (5.5,-0.5) {$1$}; +\node at (6.5,-0.5) {$1$}; +\node at (7.5,-0.5) {$1$}; +\node at (8.5,-0.5) {$1$}; + +\node at (0.5,-1.5) {$2$}; +\node at (1.5,-1.5) {$3$}; +\node at (2.5,-1.5) {$4$}; +\node at (3.5,-1.5) {$5$}; +\node at (4.5,-1.5) {$3$}; +\node at (5.5,-1.5) {$4$}; +\node at (6.5,-1.5) {$3$}; +\node at (7.5,-1.5) {$1$}; +\node at (8.5,-1.5) {$1$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\node at (8.5,1.4) {$9$}; +\end{tikzpicture} +\end{center} + +Viimeinen tarvittava askel on tallentaa solmujen arvot +binääri-indeksi\-puuhun tai segmenttipuuhun. +Tällöin sekä alipuun arvojen summan laskeminen +että solmun arvon muuttaminen onnistuvat ajassa $O(\log n)$, +eli pystymme vastaamaan kyselyihin tehokkaasti. + +\subsubsection{Polkujen käsittely} + +Solmutaulukon avulla voi myös käsitellä tehokkaasti +polkuja, jotka kulkevat juuresta tiettyyn solmuun puussa. +Ratkaistaan seuraavaksi tehtävä, +jossa toteutettavana on seuraavat kyselyt: +\begin{itemize} +\item muuta solmun $x$ arvoa +\item laske arvojen summa juuresta solmuun $x$ +\end{itemize} + +Esimerkiksi seuraavassa puussa polulla solmusta 1 +solmuun 8 arvojen summa on $4+5+3=12$. + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,3) {$1$}; +\node[draw, circle] (2) at (-3,1) {$2$}; +\node[draw, circle] (3) at (-1,1) {$3$}; +\node[draw, circle] (4) at (1,1) {$4$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (-3,-1) {$6$}; +\node[draw, circle] (7) at (-0.5,-1) {$7$}; +\node[draw, circle] (8) at (1,-1) {$8$}; +\node[draw, circle] (9) at (2.5,-1) {$9$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (1) -- (5); +\path[draw,thick,-] (2) -- (6); +\path[draw,thick,-] (4) -- (7); +\path[draw,thick,-] (4) -- (8); +\path[draw,thick,-] (4) -- (9); + +\node[color=blue] at (0,3+0.65) {4}; +\node[color=blue] at (-3-0.65,1) {5}; +\node[color=blue] at (-1-0.65,1) {3}; +\node[color=blue] at (1+0.65,1) {5}; +\node[color=blue] at (3+0.65,1) {2}; +\node[color=blue] at (-3,-1-0.65) {3}; +\node[color=blue] at (-0.5,-1-0.65) {5}; +\node[color=blue] at (1,-1-0.65) {3}; +\node[color=blue] at (2.5,-1-0.65) {1}; +\end{tikzpicture} +\end{center} + +Ideana on muodostaa samanlainen taulukko kuin +alipuiden käsittelyssä mutta tallentaa +solmujen arvot erikoisella tavalla: +kun taulukon kohdassa $k$ olevan solmun arvo on $a$, +kohdan $k$ arvoon lisätään $a$ ja kohdan $k+c$ arvosta +vähennetään $a$, missä $c$ on alipuun koko. + +\begin{samepage} +Esimerkiksi yllä olevaa puuta vastaa seuraava taulukko: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,1) grid (10,-2); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$6$}; +\node at (3.5,0.5) {$3$}; +\node at (4.5,0.5) {$4$}; +\node at (5.5,0.5) {$7$}; +\node at (6.5,0.5) {$8$}; +\node at (7.5,0.5) {$9$}; +\node at (8.5,0.5) {$5$}; +\node at (9.5,0.5) {--}; + +\node at (0.5,-0.5) {$9$}; +\node at (1.5,-0.5) {$2$}; +\node at (2.5,-0.5) {$1$}; +\node at (3.5,-0.5) {$1$}; +\node at (4.5,-0.5) {$4$}; +\node at (5.5,-0.5) {$1$}; +\node at (6.5,-0.5) {$1$}; +\node at (7.5,-0.5) {$1$}; +\node at (8.5,-0.5) {$1$}; +\node at (9.5,-0.5) {--}; + +\node at (0.5,-1.5) {$4$}; +\node at (1.5,-1.5) {$5$}; +\node at (2.5,-1.5) {$3$}; +\node at (3.5,-1.5) {$-5$}; +\node at (4.5,-1.5) {$2$}; +\node at (5.5,-1.5) {$5$}; +\node at (6.5,-1.5) {$-2$}; +\node at (7.5,-1.5) {$-2$}; +\node at (8.5,-1.5) {$-4$}; +\node at (9.5,-1.5) {$-4$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\node at (8.5,1.4) {$9$}; +\node at (9.5,1.4) {$10$}; +\end{tikzpicture} +\end{center} +\end{samepage} + +Esimerkiksi solmun $3$ arvona on $-5$, koska +se on solmujen $2$ ja $6$ alipuiden jälkeinen solmu, +mistä tulee arvoa $-5-3$, ja sen oma arvo on $3$. +Yhteensä solmun 3 arvo on siis $-5-3+3=-5$. +Huomaa, että taulukossa on ylimääräinen kohta 10, +johon on tallennettu vain juuren arvon vastaluku. + +Nyt solmujen arvojen summa polulla juuresta alkaen +selviää laskemalla kaikkien taulukon arvojen summa +taulukon alusta solmuun asti. +Esimerkiksi summa solmusta $1$ solmuun $8$ selviää näin: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (6,1) rectangle (7,0); +\fill[color=lightgray] (0,-1) rectangle (7,-2); +\draw (0,1) grid (10,-2); + +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$6$}; +\node at (3.5,0.5) {$3$}; +\node at (4.5,0.5) {$4$}; +\node at (5.5,0.5) {$7$}; +\node at (6.5,0.5) {$8$}; +\node at (7.5,0.5) {$9$}; +\node at (8.5,0.5) {$5$}; +\node at (9.5,0.5) {--}; + +\node at (0.5,-0.5) {$9$}; +\node at (1.5,-0.5) {$2$}; +\node at (2.5,-0.5) {$1$}; +\node at (3.5,-0.5) {$1$}; +\node at (4.5,-0.5) {$4$}; +\node at (5.5,-0.5) {$1$}; +\node at (6.5,-0.5) {$1$}; +\node at (7.5,-0.5) {$1$}; +\node at (8.5,-0.5) {$1$}; +\node at (9.5,-0.5) {--}; + +\node at (0.5,-1.5) {$4$}; +\node at (1.5,-1.5) {$5$}; +\node at (2.5,-1.5) {$3$}; +\node at (3.5,-1.5) {$-5$}; +\node at (4.5,-1.5) {$2$}; +\node at (5.5,-1.5) {$5$}; +\node at (6.5,-1.5) {$-2$}; +\node at (7.5,-1.5) {$-2$}; +\node at (8.5,-1.5) {$-4$}; +\node at (9.5,-1.5) {$-4$}; + +\footnotesize +\node at (0.5,1.4) {$1$}; +\node at (1.5,1.4) {$2$}; +\node at (2.5,1.4) {$3$}; +\node at (3.5,1.4) {$4$}; +\node at (4.5,1.4) {$5$}; +\node at (5.5,1.4) {$6$}; +\node at (6.5,1.4) {$7$}; +\node at (7.5,1.4) {$8$}; +\node at (8.5,1.4) {$9$}; +\node at (9.5,1.4) {$10$}; +\end{tikzpicture} +\end{center} + +Summaksi tulee +\[4+5+3-5+2+5-2=12,\] +mikä vastaa polun summaa $4+5+3=12$. +Tämä laskentatapa toimii, koska jokaisen solmun arvo +lisätään summaan, kun se tulee vastaan syvyyshaussa, +ja vähennetään summasta, kun sen käsittely päättyy. + +Alipuiden käsittelyä vastaavasti voimme tallentaa +arvot binääri-indeksi\-puuhun tai segmenttipuuhun ja +sekä polun summan laskeminen että arvon muuttaminen +onnistuvat ajassa $O(\log n)$. + +\section{Alin yhteinen esivanhempi} + +\index{alin yhteinen esivanhempi@alin yhteinen esivanhempi} + +Kahden puun solmun +\key{alin yhteinen esivanhempi} +on mahdollisimman matalalla puussa oleva solmu, +jonka alipuuhun kumpikin solmu kuuluu. +Tyypillinen tehtävä on vastata tehokkaasti +joukkoon kyselyitä, jossa selvitettävänä on +kahden solmun alin yhteinen esivanhempi. +Esimerkiksi puussa +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,3) {$1$}; +\node[draw, circle] (2) at (2,1) {$4$}; +\node[draw, circle] (3) at (-2,1) {$2$}; +\node[draw, circle] (4) at (0,1) {$3$}; +\node[draw, circle] (5) at (2,-1) {$7$}; +\node[draw, circle] (6) at (-3,-1) {$5$}; +\node[draw, circle] (7) at (-1,-1) {$6$}; +\node[draw, circle] (8) at (-1,-3) {$8$}; +\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,-] (7) -- (8); +\end{tikzpicture} +\end{center} +solmujen 5 ja 8 alin yhteinen esivanhempi on solmu 2 +ja solmujen 3 ja 4 alin yhteinen esivanhempi on solmu 1. + +Tutustumme seuraavaksi kahteen tehokkaaseen menetelmään +alimman yhteisen esivanhemman selvittämiseen. + +\subsubsection{Menetelmä 1} + +Yksi tapa ratkaista tehtävä on hyödyntää +tehokasta nousemista puussa. +Tällöin alimman yhteisen esivanhemman etsiminen +muodostuu kahdesta vaiheesta. +Ensin noustaan alemmasta solmusta samalle tasolle +ylemmän solmun kanssa, +sitten noustaan rinnakkain kohti +alinta yhteistä esivanhempaa. + +Tarkastellaan esimerkkinä solmujen 5 ja 8 +alimman yhteisen esivanhemman etsimistä: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,3) {$1$}; +\node[draw, circle] (2) at (2,1) {$4$}; +\node[draw, circle] (3) at (-2,1) {$2$}; +\node[draw, circle] (4) at (0,1) {$3$}; +\node[draw, circle] (5) at (2,-1) {$7$}; +\node[draw, circle,fill=lightgray] (6) at (-3,-1) {$5$}; +\node[draw, circle] (7) at (-1,-1) {$6$}; +\node[draw, circle,fill=lightgray] (8) at (-1,-3) {$8$}; +\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,-] (7) -- (8); +\end{tikzpicture} +\end{center} + +Solmu 5 on tasolla 3, kun taas solmu 8 on tasolla 4. +Niinpä nousemme ensin solmusta 8 yhden tason ylemmäs solmuun 6. +Tämän jälkeen nousemme rinnakkain solmuista 5 ja 6 +lähtien yhden tason, jolloin päädymme solmuun 2: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,3) {$1$}; +\node[draw, circle] (2) at (2,1) {$4$}; +\node[draw, circle] (3) at (-2,1) {$2$}; +\node[draw, circle] (4) at (0,1) {$3$}; +\node[draw, circle] (5) at (2,-1) {$7$}; +\node[draw, circle,fill=lightgray] (6) at (-3,-1) {$5$}; +\node[draw, circle] (7) at (-1,-1) {$6$}; +\node[draw, circle,fill=lightgray] (8) at (-1,-3) {$8$}; +\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,-] (7) -- (8); + +\path[draw=red,thick,->,line width=2pt] (6) edge [bend left] (3); +\path[draw=red,thick,->,line width=2pt] (8) edge [bend right] (7); +\path[draw=red,thick,->,line width=2pt] (7) edge [bend right] (3); +\end{tikzpicture} +\end{center} + +Menetelmä vaatii $O(n \log n)$-aikaisen esikäsittelyn, +jonka jälkeen minkä tahansa kahden solmun alin yhteinen +esivanhempi selviää ajassa $O(\log n)$, +koska kumpikin vaihe nousussa vie aikaa $O(\log n)$. + +\subsubsection{Menetelmä 2} + +Toinen tapa ratkaista tehtävä perustuu solmutaulukon +käyttämiseen. +Ideana on jälleen järjestää solmut syvyyshaun mukaan: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,3) {$1$}; +\node[draw, circle] (2) at (2,1) {$4$}; +\node[draw, circle] (3) at (-2,1) {$2$}; +\node[draw, circle] (4) at (0,1) {$3$}; +\node[draw, circle] (5) at (2,-1) {$7$}; +\node[draw, circle] (6) at (-3,-1) {$5$}; +\node[draw, circle] (7) at (-1,-1) {$6$}; +\node[draw, circle] (8) at (-1,-3) {$8$}; +\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,-] (7) -- (8); + +\path[draw=red,thick,->,line width=2pt] (1) edge [bend right=15] (3); +\path[draw=red,thick,->,line width=2pt] (3) edge [bend right=15] (6); +\path[draw=red,thick,->,line width=2pt] (6) edge [bend right=15] (3); +\path[draw=red,thick,->,line width=2pt] (3) edge [bend right=15] (7); +\path[draw=red,thick,->,line width=2pt] (7) edge [bend right=15] (8); +\path[draw=red,thick,->,line width=2pt] (8) edge [bend right=15] (7); +\path[draw=red,thick,->,line width=2pt] (7) edge [bend right=15] (3); +\path[draw=red,thick,->,line width=2pt] (3) edge [bend right=15] (1); +\path[draw=red,thick,->,line width=2pt] (1) edge [bend right=15] (4); +\path[draw=red,thick,->,line width=2pt] (4) edge [bend right=15] (1); +\path[draw=red,thick,->,line width=2pt] (1) edge [bend right=15] (2); +\path[draw=red,thick,->,line width=2pt] (2) edge [bend right=15] (5); +\path[draw=red,thick,->,line width=2pt] (5) edge [bend right=15] (2); +\path[draw=red,thick,->,line width=2pt] (2) edge [bend right=15] (1); +\end{tikzpicture} +\end{center} + +Erona aiempaan solmu lisätään kuitenkin solmutaulukkoon +mukaan \textit{aina}, kun syvyyshaku käy solmussa, +eikä vain ensimmäisellä kerralla. +Niinpä solmu esiintyy solmutaulukossa $k+1$ kertaa, +missä $k$ on solmun lasten määrä, +ja solmutaulukossa on yhteensä $2n-1$ solmua. + +Tallennamme solmutaulukkoon kaksi tietoa: +(1) solmun tunnus ja (2) solmun taso puussa. +Esimerkkipuuta vastaavat taulukot ovat: + +\begin{center} +\begin{tikzpicture}[scale=0.7] + +\draw (0,1) grid (15,2); +%\node at (-1.1,1.5) {\texttt{node}}; +\node at (0.5,1.5) {$1$}; +\node at (1.5,1.5) {$2$}; +\node at (2.5,1.5) {$5$}; +\node at (3.5,1.5) {$2$}; +\node at (4.5,1.5) {$6$}; +\node at (5.5,1.5) {$8$}; +\node at (6.5,1.5) {$6$}; +\node at (7.5,1.5) {$2$}; +\node at (8.5,1.5) {$1$}; +\node at (9.5,1.5) {$3$}; +\node at (10.5,1.5) {$1$}; +\node at (11.5,1.5) {$4$}; +\node at (12.5,1.5) {$7$}; +\node at (13.5,1.5) {$4$}; +\node at (14.5,1.5) {$1$}; + + +\draw (0,0) grid (15,1); +%\node at (-1.1,0.5) {\texttt{depth}}; +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$3$}; +\node at (3.5,0.5) {$2$}; +\node at (4.5,0.5) {$3$}; +\node at (5.5,0.5) {$4$}; +\node at (6.5,0.5) {$3$}; +\node at (7.5,0.5) {$2$}; +\node at (8.5,0.5) {$1$}; +\node at (9.5,0.5) {$2$}; +\node at (10.5,0.5) {$1$}; +\node at (11.5,0.5) {$2$}; +\node at (12.5,0.5) {$3$}; +\node at (13.5,0.5) {$2$}; +\node at (14.5,0.5) {$1$}; + +\footnotesize +\node at (0.5,2.5) {$1$}; +\node at (1.5,2.5) {$2$}; +\node at (2.5,2.5) {$3$}; +\node at (3.5,2.5) {$4$}; +\node at (4.5,2.5) {$5$}; +\node at (5.5,2.5) {$6$}; +\node at (6.5,2.5) {$7$}; +\node at (7.5,2.5) {$8$}; +\node at (8.5,2.5) {$9$}; +\node at (9.5,2.5) {$10$}; +\node at (10.5,2.5) {$11$}; +\node at (11.5,2.5) {$12$}; +\node at (12.5,2.5) {$13$}; +\node at (13.5,2.5) {$14$}; +\node at (14.5,2.5) {$15$}; +\end{tikzpicture} +\end{center} + +Tämän taulukon avulla solmujen $a$ ja $b$ alin yhteinen esivanhempi +selviää etsimällä taulukosta alimman tason solmu +solmujen $a$ ja $b$ välissä. +Esimerkiksi solmujen 5 ja 8 alin yhteinen esivanhempi +löytyy seuraavasti: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (2,1) rectangle (3,2); +\fill[color=lightgray] (5,1) rectangle (6,2); +\fill[color=lightgray] (2,0) rectangle (6,1); + +\node at (3.5,-0.5) {$\uparrow$}; + +\draw (0,1) grid (15,2); +\node at (0.5,1.5) {$1$}; +\node at (1.5,1.5) {$2$}; +\node at (2.5,1.5) {$5$}; +\node at (3.5,1.5) {$2$}; +\node at (4.5,1.5) {$6$}; +\node at (5.5,1.5) {$8$}; +\node at (6.5,1.5) {$6$}; +\node at (7.5,1.5) {$2$}; +\node at (8.5,1.5) {$1$}; +\node at (9.5,1.5) {$3$}; +\node at (10.5,1.5) {$1$}; +\node at (11.5,1.5) {$4$}; +\node at (12.5,1.5) {$7$}; +\node at (13.5,1.5) {$4$}; +\node at (14.5,1.5) {$1$}; + + +\draw (0,0) grid (15,1); +\node at (0.5,0.5) {$1$}; +\node at (1.5,0.5) {$2$}; +\node at (2.5,0.5) {$3$}; +\node at (3.5,0.5) {$2$}; +\node at (4.5,0.5) {$3$}; +\node at (5.5,0.5) {$4$}; +\node at (6.5,0.5) {$3$}; +\node at (7.5,0.5) {$2$}; +\node at (8.5,0.5) {$1$}; +\node at (9.5,0.5) {$2$}; +\node at (10.5,0.5) {$1$}; +\node at (11.5,0.5) {$2$}; +\node at (12.5,0.5) {$3$}; +\node at (13.5,0.5) {$2$}; +\node at (14.5,0.5) {$1$}; + +\footnotesize +\node at (0.5,2.5) {$1$}; +\node at (1.5,2.5) {$2$}; +\node at (2.5,2.5) {$3$}; +\node at (3.5,2.5) {$4$}; +\node at (4.5,2.5) {$5$}; +\node at (5.5,2.5) {$6$}; +\node at (6.5,2.5) {$7$}; +\node at (7.5,2.5) {$8$}; +\node at (8.5,2.5) {$9$}; +\node at (9.5,2.5) {$10$}; +\node at (10.5,2.5) {$11$}; +\node at (11.5,2.5) {$12$}; +\node at (12.5,2.5) {$13$}; +\node at (13.5,2.5) {$14$}; +\node at (14.5,2.5) {$15$}; +\end{tikzpicture} +\end{center} + +Solmu 5 on taulukossa kohdassa 3, +solmu 8 on taulukossa kohdassa 6 +ja alimman tason solmu välillä $3 \ldots 6$ +on kohdassa 4 oleva solmu 2, +jonka taso on 2. +Niinpä solmujen 5 ja 8 alin yhteinen esivanhempi +on solmu 2. + +Alimman tason solmu välillä selviää +ajassa $O(\log n)$, kun taulukon sisältö on +tallennettu segmenttipuuhun. +Myös aikavaativuus $O(1)$ on mahdollinen, +koska taulukko on staattinen, mutta tälle on harvoin tarvetta. +Kummassakin tapauksessa esikäsittely vie aikaa $O(n \log n)$. + +\subsubsection{Solmujen etäisyydet} + +Tarkastellaan lopuksi tehtävää, +jossa kyselyissä tulee laskea tehokkaasti +kahden solmun etäisyys eli solmujen välisen polun pituus puussa. +Osoittautuu, että tämä tehtävä +palautuu alimman yhteisen esivanhemman etsimiseen. + +Valitaan ensin mikä tahansa +solmu puun juureksi. +Tämän jälkeen solmujen $a$ ja $b$ +etäisyys on $d(a)+d(b)-2 \cdot d(c)$, +missä $c$ on solmujen alin yhteinen esivanhempi +ja $d(s)$ on etäisyys puun juuresta solmuun $s$. +Esimerkiksi puussa + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,3) {$1$}; +\node[draw, circle] (2) at (2,1) {$4$}; +\node[draw, circle] (3) at (-2,1) {$2$}; +\node[draw, circle] (4) at (0,1) {$3$}; +\node[draw, circle] (5) at (2,-1) {$7$}; +\node[draw, circle] (6) at (-3,-1) {$5$}; +\node[draw, circle] (7) at (-1,-1) {$6$}; +\node[draw, circle] (8) at (-1,-3) {$8$}; +\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,-] (7) -- (8); + +\path[draw=red,thick,-,line width=2pt] (8) -- node[font=\small] {} (7); +\path[draw=red,thick,-,line width=2pt] (7) -- node[font=\small] {} (3); +\path[draw=red,thick,-,line width=2pt] (6) -- node[font=\small] {} (3); +\end{tikzpicture} +\end{center} +solmujen 5 ja 8 alin yhteinen esivanhempi on 2. +Polku solmusta 5 solmuun 8 +kulkee ensin ylöspäin solmusta 5 +solmuun 2 ja sitten alaspäin +solmusta 2 solmuun 8. +Solmujen etäisyydet juuresta ovat $d(5)=3$, +$d(8)=4$ ja $d(2)=2$, +joten solmujen 5 ja 8 etäisyys +on $3+4-2\cdot2=3$. + + diff --git a/luku19.tex b/luku19.tex new file mode 100644 index 0000000..1b84aae --- /dev/null +++ b/luku19.tex @@ -0,0 +1,664 @@ +\chapter{Paths and circuits} + +Tämä luku käsittelee kahdenlaisia polkuja verkossa: +\begin{itemize} +\item \key{Eulerin polku} on verkossa oleva +polku, joka kulkee tasan kerran jokaista +verkon kaarta pitkin. +\item \key{Hamiltonin polku} on verkossa +oleva polku, joka käy tasan kerran +jokaisessa verkon solmussa. +\end{itemize} +Vaikka Eulerin ja Hamiltonin polut +näyttävät päältä päin +samantapaisilta käsitteiltä, +niihin liittyy hyvin erilaisia laskennallisia ongelmia. +Osoittautuu, että yksinkertainen verkon solmujen +asteisiin liittyvä sääntö ratkaisee, onko verkossa +Eulerin polkua, ja polun muodostamiseen on myös +olemassa tehokas algoritmi. +Sen sijaan Hamiltonin polun etsimiseen ei tunneta +mitään tehokasta algoritmia, vaan kyseessä on +NP-vaikea ongelma. + +\section{Eulerin polku} + +\index{Eulerin polku@Eulerin polku} + +\key{Eulerin polku} on verkossa oleva +polku, joka kulkee tarkalleen kerran jokaista kaarta pitkin. +Esimerkiksi verkossa +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (4) -- (5); +\end{tikzpicture} +\end{center} +on Eulerin polku solmusta 2 solmuun 5: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (4) -- (5); + +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]north:1.}] {} (1); +\path[draw=red,thick,->,line width=2pt] (1) -- node[font=\small,label={[red]left:2.}] {} (4); +\path[draw=red,thick,->,line width=2pt] (4) -- node[font=\small,label={[red]south:3.}] {} (5); +\path[draw=red,thick,->,line width=2pt] (5) -- node[font=\small,label={[red]left:4.}] {} (2); +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]north:5.}] {} (3); +\path[draw=red,thick,->,line width=2pt] (3) -- node[font=\small,label={[red]south:6.}] {} (5); +\end{tikzpicture} +\end{center} +\index{Eulerin kierros@Eulerin kierros} +\key{Eulerin kierros} +on puolestaan Eulerin polku, +jonka alku- ja loppusolmu ovat samat. +Esimerkiksi verkossa +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (2) -- (4); +\end{tikzpicture} +\end{center} +on Eulerin kierros, jonka alku- ja loppusolmu on 1: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (2) -- (4); + +\path[draw=red,thick,->,line width=2pt] (1) -- node[font=\small,label={[red]left:1.}] {} (4); +\path[draw=red,thick,->,line width=2pt] (4) -- node[font=\small,label={[red]south:2.}] {} (2); +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]right:3.}] {} (5); +\path[draw=red,thick,->,line width=2pt] (5) -- node[font=\small,label={[red]south:4.}] {} (3); +\path[draw=red,thick,->,line width=2pt] (3) -- node[font=\small,label={[red]north:5.}] {} (2); +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]north:6.}] {} (1); +\end{tikzpicture} +\end{center} + +\subsubsection{Olemassaolo} + +Osoittautuu, että Eulerin polun ja kierroksen olemassaolo +riippuu verkon solmujen asteista. +Solmun aste on sen naapurien määrä eli niiden solmujen määrä, +jotka ovat yhteydessä solmuun kaarella. + +Suuntaamattomassa verkossa on Eulerin polku, +jos kaikki kaaret ovat samassa yhtenäisessä komponentissa ja +\begin{itemize} +\item jokaisen solmun aste on parillinen \textit{tai} +\item tarkalleen kahden solmun aste on pariton ja kaikkien +muiden solmujen aste on parillinen. +\end{itemize} + +Ensimmäisessä tapauksessa Eulerin polku on samalla myös Eulerin kierros. +Jälkimmäisessä tapauksessa Eulerin polun alku- ja loppusolmu ovat +paritonasteiset solmut ja se ei ole Eulerin kierros. + +\begin{samepage} +Esimerkiksi verkossa +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (4) -- (5); +\end{tikzpicture} +\end{center} +\end{samepage} +solmujen 1, 3 ja 4 aste on 2 ja solmujen 2 ja 5 aste on 3. +Tarkalleen kahden solmun aste on pariton, +joten verkossa on Eulerin polku solmujen 2 ja 5 välillä, +mutta verkossa ei ole Eulerin kierrosta. + +Jos verkko on suunnattu, tilanne on hieman hankalampi. +Silloin Eulerin polun ja kierroksen olemassaoloon +vaikuttavat solmujen lähtö- ja tuloasteet. +Solmun lähtöaste on solmusta lähtevien kaarten määrä, +ja vastaavasti solmun tuloaste on solmuun tulevien kaarten määrä. + +Suunnatussa verkossa on Eulerin polku, jos +kaikki kaaret ovat samassa vahvasti yhtenäisessä +komponentissa ja +\begin{itemize} +\item jokaisen solmun lähtö- ja tuloaste on sama \textit{tai} +\item yhdessä solmussa lähtöaste on yhden suurempi kuin tuloaste, +toisessa solmussa tuloaste on yhden suurempi kuin lähtöaste +ja kaikissa muissa solmuissa lähtö- ja tuloaste on sama. +\end{itemize} + +Tilanne on vastaava kuin suuntaamattomassa verkossa: +ensimmäisessä tapauksessa Eulerin polku on myös Eulerin kierros, +ja toisessa tapauksessa verkossa on vain Eulerin polku, +jonka lähtösolmussa lähtöaste on suurempi ja +päätesolmussa tuloaste on suurempi. + +Esimerkiksi verkossa +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (4) -- (1); +\path[draw,thick,->,>=latex] (3) -- (5); +\path[draw,thick,->,>=latex] (2) -- (5); +\path[draw,thick,->,>=latex] (5) -- (4); +\end{tikzpicture} +\end{center} +solmuissa 1, 3 ja 4 sekä lähtöaste että tuloaste on 1. +Solmussa 2 tuloaste on 1 ja lähtöaste on 2, +kun taas solmussa 5 tulosate on 2 ja lähtöaste on 1. +Niinpä verkossa on Eulerin polku solmusta 2 solmuun 5: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (4) -- (5); + +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]north:1.}] {} (3); +\path[draw=red,thick,->,line width=2pt] (3) -- node[font=\small,label={[red]south:2.}] {} (5); +\path[draw=red,thick,->,line width=2pt] (5) -- node[font=\small,label={[red]south:3.}] {} (4); +\path[draw=red,thick,->,line width=2pt] (4) -- node[font=\small,label={[red]left:4.}] {} (1); +\path[draw=red,thick,->,line width=2pt] (1) -- node[font=\small,label={[red]north:5.}] {} (2); +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]left:6.}] {} (5); +\end{tikzpicture} +\end{center} + +\subsubsection{Hierholzerin algoritmi} + +\index{Hierholzerin algoritmi@Hierholzerin algoritmi} + +\key{Hierholzerin algoritmi} muodostaa Eulerin kierroksen +suuntaamattomassa verkossa. +Algoritmi olettaa, että kaikki kaaret ovat samassa +komponentissa ja jokaisen solmun aste on parillinen. +Algoritmi on mahdollista toteuttaa niin, että sen +aikavaativuus on $O(n+m)$. + +Hierholzerin algoritmi muodostaa ensin verkkoon jonkin kierroksen, +johon kuuluu osa verkon kaarista. +Sen jälkeen algoritmi alkaa laajentaa kierrosta +lisäämällä sen osaksi uusia alikierroksia. +Tämä jatkuu niin kauan, kunnes kaikki kaaret kuuluvat +kierrokseen ja siitä on tullut Eulerin kierros. + +Algoritmi laajentaa kierrosta valitsemalla jonkin +kierrokseen kuuluvan solmun $x$, +jonka kaikki kaaret eivät ole vielä mukana kierroksessa. +Algoritmi muodostaa solmusta $x$ alkaen uuden polun +kulkien vain sellaisia kaaria, jotka eivät ole +mukana kierroksessa. +Koska jokaisen solmun aste on parillinen, +ennemmin tai myöhemmin polku palaa takaisin lähtösolmuun $x$. + +Jos verkossa on kaksi paritonasteista solmua, +Hierholzerin algoritmilla voi myös muodostaa +Eulerin polun lisäämällä kaaren +paritonasteisten solmujen välille. +Tämän jälkeen verkosta voi etsiä ensin +Eulerin kierroksen ja poistaa siitä sitten +ylimääräisen kaaren, jolloin tuloksena on Eulerin polku. + +\subsubsection{Esimerkki} + +\begin{samepage} +Tarkastellaan algoritmin toimintaa seuraavassa verkossa: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (3,5) {$1$}; +\node[draw, circle] (2) at (1,3) {$2$}; +\node[draw, circle] (3) at (3,3) {$3$}; +\node[draw, circle] (4) at (5,3) {$4$}; +\node[draw, circle] (5) at (1,1) {$5$}; +\node[draw, circle] (6) at (3,1) {$6$}; +\node[draw, circle] (7) at (5,1) {$7$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (2) -- (6); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (4) -- (7); +\path[draw,thick,-] (5) -- (6); +\path[draw,thick,-] (6) -- (7); +\end{tikzpicture} +\end{center} +\end{samepage} + +\begin{samepage} +Oletetaan, että algoritmi aloittaa +ensimmäisen kierroksen solmusta 1. +Siitä syntyy kierros $1 \rightarrow 2 \rightarrow 3 \rightarrow 1$: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (3,5) {$1$}; +\node[draw, circle] (2) at (1,3) {$2$}; +\node[draw, circle] (3) at (3,3) {$3$}; +\node[draw, circle] (4) at (5,3) {$4$}; +\node[draw, circle] (5) at (1,1) {$5$}; +\node[draw, circle] (6) at (3,1) {$6$}; +\node[draw, circle] (7) at (5,1) {$7$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (2) -- (6); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (4) -- (7); +\path[draw,thick,-] (5) -- (6); +\path[draw,thick,-] (6) -- (7); + +\path[draw=red,thick,->,line width=2pt] (1) -- node[font=\small,label={[red]north:1.}] {} (2); +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]north:2.}] {} (3); +\path[draw=red,thick,->,line width=2pt] (3) -- node[font=\small,label={[red]east:3.}] {} (1); +\end{tikzpicture} +\end{center} +\end{samepage} + +Seuraavaksi algoritmi lisää mukaan kierroksen +$2 \rightarrow 5 \rightarrow 6 \rightarrow 2$: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (3,5) {$1$}; +\node[draw, circle] (2) at (1,3) {$2$}; +\node[draw, circle] (3) at (3,3) {$3$}; +\node[draw, circle] (4) at (5,3) {$4$}; +\node[draw, circle] (5) at (1,1) {$5$}; +\node[draw, circle] (6) at (3,1) {$6$}; +\node[draw, circle] (7) at (5,1) {$7$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (2) -- (6); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (4) -- (7); +\path[draw,thick,-] (5) -- (6); +\path[draw,thick,-] (6) -- (7); + +\path[draw=red,thick,->,line width=2pt] (1) -- node[font=\small,label={[red]north:1.}] {} (2); +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]west:2.}] {} (5); +\path[draw=red,thick,->,line width=2pt] (5) -- node[font=\small,label={[red]south:3.}] {} (6); +\path[draw=red,thick,->,line width=2pt] (6) -- node[font=\small,label={[red]north:4.}] {} (2); +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]north:5.}] {} (3); +\path[draw=red,thick,->,line width=2pt] (3) -- node[font=\small,label={[red]east:6.}] {} (1); +\end{tikzpicture} +\end{center} + +Lopuksi algoritmi lisää mukaan kierroksen +$6 \rightarrow 3 \rightarrow 4 \rightarrow 7 \rightarrow 6$: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (3,5) {$1$}; +\node[draw, circle] (2) at (1,3) {$2$}; +\node[draw, circle] (3) at (3,3) {$3$}; +\node[draw, circle] (4) at (5,3) {$4$}; +\node[draw, circle] (5) at (1,1) {$5$}; +\node[draw, circle] (6) at (3,1) {$6$}; +\node[draw, circle] (7) at (5,1) {$7$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (2) -- (6); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (4) -- (7); +\path[draw,thick,-] (5) -- (6); +\path[draw,thick,-] (6) -- (7); + +\path[draw=red,thick,->,line width=2pt] (1) -- node[font=\small,label={[red]north:1.}] {} (2); +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]west:2.}] {} (5); +\path[draw=red,thick,->,line width=2pt] (5) -- node[font=\small,label={[red]south:3.}] {} (6); +\path[draw=red,thick,->,line width=2pt] (6) -- node[font=\small,label={[red]east:4.}] {} (3); +\path[draw=red,thick,->,line width=2pt] (3) -- node[font=\small,label={[red]north:5.}] {} (4); +\path[draw=red,thick,->,line width=2pt] (4) -- node[font=\small,label={[red]east:6.}] {} (7); +\path[draw=red,thick,->,line width=2pt] (7) -- node[font=\small,label={[red]south:7.}] {} (6); +\path[draw=red,thick,->,line width=2pt] (6) -- node[font=\small,label={[red]right:8.}] {} (2); +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]north:9.}] {} (3); +\path[draw=red,thick,->,line width=2pt] (3) -- node[font=\small,label={[red]east:10.}] {} (1); +\end{tikzpicture} +\end{center} + +Nyt kaikki kaaret ovat kierroksessa, +joten Eulerin kierros on valmis. + +\section{Hamiltonin polku} + +\index{Hamiltonin polku@Hamiltonin polku} +\key{Hamiltonin polku} +on verkossa oleva polku, +joka kulkee tarkalleen kerran jokaisen solmun kautta. +Esimerkiksi verkossa +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (4) -- (5); +\end{tikzpicture} +\end{center} +on Hamiltonin polku solmusta 1 solmuun 3: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (4) -- (5); + +\path[draw=red,thick,->,line width=2pt] (1) -- node[font=\small,label={[red]left:1.}] {} (4); +\path[draw=red,thick,->,line width=2pt] (4) -- node[font=\small,label={[red]south:2.}] {} (5); +\path[draw=red,thick,->,line width=2pt] (5) -- node[font=\small,label={[red]left:3.}] {} (2); +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]north:4.}] {} (3); +\end{tikzpicture} +\end{center} + +\index{Hamiltonin kierros@Hamiltonin kierros} +Jos Hamiltonin polun alku- ja loppusolmu on sama, +kyseessä on \key{Hamiltonin kierros}. +Äskeisessä verkossa on myös +Hamiltonin kierros, jonka alku- ja loppusolmu on solmu 1: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,5) {$1$}; +\node[draw, circle] (2) at (3,5) {$2$}; +\node[draw, circle] (3) at (5,4) {$3$}; +\node[draw, circle] (4) at (1,3) {$4$}; +\node[draw, circle] (5) at (3,3) {$5$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (2) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (2) -- (5); +\path[draw,thick,-] (4) -- (5); + +\path[draw=red,thick,->,line width=2pt] (1) -- node[font=\small,label={[red]north:1.}] {} (2); +\path[draw=red,thick,->,line width=2pt] (2) -- node[font=\small,label={[red]north:2.}] {} (3); +\path[draw=red,thick,->,line width=2pt] (3) -- node[font=\small,label={[red]south:3.}] {} (5); +\path[draw=red,thick,->,line width=2pt] (5) -- node[font=\small,label={[red]south:4.}] {} (4); +\path[draw=red,thick,->,line width=2pt] (4) -- node[font=\small,label={[red]left:5.}] {} (1); +\end{tikzpicture} +\end{center} + +\subsubsection{Olemassaolo} + +Hamiltonin polun olemassaoloon ei tiedetä +mitään verkon rakenteeseen liittyvää ehtoa, +jonka voisi tarkistaa tehokkaasti. +Joissakin erikoistapauksissa voidaan silti sanoa +varmasti, että verkossa on Hamiltonin polku. + +Yksinkertainen havainto on, että jos verkko on täydellinen +eli jokaisen solmun välillä on kaari, +niin siinä on Hamiltonin polku. +Myös vahvempia tuloksia on saatu aikaan: + +\begin{itemize} +\item +\index{Diracin lause@Diracin lause} +\key{Diracin lause}: +Jos jokaisen verkon solmun aste on $n/2$ tai suurempi, +niin verkossa on Hamiltonin polku. +\item +\index{Oren lause@Oren lause} +\key{Oren lause}: +Jos jokaisen ei-vierekkäisen solmuparin asteiden summa +on $n$ tai suurempi, +niin verkossa on Hamiltonin polku. +\end{itemize} + +Yhteistä näissä ja muissa tuloksissa on, +että ne takaavat Hamiltonin polun olemassaolon, +jos verkossa on \textit{paljon} kaaria. +Tämä on ymmärrettävää, koska mitä enemmän +kaaria verkossa on, sitä enemmän mahdollisuuksia +Hamiltonin polun muodostamiseen on olemassa. + +\subsubsection{Muodostaminen} + +Koska Hamiltonin polun olemassaoloa ei voi tarkastaa tehokkaasti, +on selvää, että polkua ei voi myöskään muodostaa tehokkaasti, +koska muuten polun olemassaolon voisi selvittää yrittämällä +muodostaa sen. + +Yksinkertaisin tapa etsiä Hamiltonin polkua on käyttää +peruuttavaa hakua, joka käy läpi kaikki vaihtoehdot +polun muodostamiseen. +Tällaisen algoritmin aikavaativuus on ainakin luokkaa $O(n!)$, +koska $n$ solmusta voidaan muodostaa $n!$ järjestystä, +jossa ne voivat esiintyä polulla. + +Tehokkaampi tapa perustuu dynaamiseen ohjelmointiin +luvun 10.4 tapaan. +Ideana on määritellä funktio $f(s,x)$, +jossa $s$ on verkon solmujen osajoukko ja +$x$ on yksi osajoukon solmuista. +Funktio kertoo, onko olemassa Hamiltonin polkua, +joka käy läpi joukon $s$ solmut päätyen solmuun $x$. +Tällainen ratkaisu on mahdollista toteuttaa ajassa $O(2^n n^2)$. + +\section{De Bruijnin jono} + +\index{de Bruijnin jono@de Bruijnin jono} + +\key{De Bruijnin jono} +on merkkijono, jonka osajonona on tarkalleen +kerran jokainen $k$-merkkisen aakkoston +$n$ merkin yhdistelmä. +Tällaisen merkkijonon pituus on +$k^n+n-1$ merkkiä. +Esimerkiksi kun $k=2$ ja $n=3$, +niin yksi mahdollinen de Bruijnin jono on +\[0001011100.\] +Tämän merkkijono osajonot ovat kaikki +kolmen bitin yhdistelmät +000, 001, 010, 011, 100, 101, 110 ja 111. + +Osoittautuu, että de Bruijnin jono +vastaa Eulerin kierrosta sopivasti +muodostetussa verkossa. +Ideana on muodostaa verkko niin, +että jokaisessa solmussa on $n-1$ +merkin yhdistelmä ja liikkuminen +kaarta pitkin muodostaa uuden +$n$ merkin yhdistelmän. +Esimerkin tapauksessa verkosta tulee seuraava: + +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (00) at (-3,0) {00}; +\node[draw, circle] (11) at (3,0) {11}; +\node[draw, circle] (01) at (0,2) {01}; +\node[draw, circle] (10) at (0,-2) {10}; + +\path[draw,thick,->] (00) edge [bend left=20] node[font=\small,label=1] {} (01); +\path[draw,thick,->] (01) edge [bend left=20] node[font=\small,label=1] {} (11); +\path[draw,thick,->] (11) edge [bend left=20] node[font=\small,label=below:0] {} (10); +\path[draw,thick,->] (10) edge [bend left=20] node[font=\small,label=below:0] {} (00); + +\path[draw,thick,->] (01) edge [bend left=30] node[font=\small,label=right:0] {} (10); +\path[draw,thick,->] (10) edge [bend left=30] node[font=\small,label=left:1] {} (01); + +\path[draw,thick,-] (00) edge [loop left] node[font=\small,label=below:0] {} (00); +\path[draw,thick,-] (11) edge [loop right] node[font=\small,label=below:1] {} (11); +\end{tikzpicture} +\end{center} + +Eulerin kierros tässä verkossa tuottaa merkkijonon, +joka sisältää kaikki $n$ merkin yhdistelmät, +kun mukaan otetaan aloitussolmun merkit sekä +kussakin kaaressa olevat merkit. +Alkusolmussa on $n-1$ merkkiä ja kaarissa +on $k^n$ merkkiä, joten tuloksena on +lyhin mahdollinen merkkijono. + +\section{Ratsun kierros} + +\index{ratsun kierros@ratsun kierros} + +\key{Ratsun kierros} on tapa liikuttaa ratsua +shakin sääntöjen mukaisesti $n \times n$ -kokoisella +shakkilaudalla niin, +että ratsu käy tarkalleen kerran jokaisessa ruudussa. +Ratsun kierros on \key{suljettu}, jos ratsu palaa lopuksi alkuruutuun, +ja muussa tapauksessa kierros on \key{avoin}. + +Esimerkiksi tapauksessa $5 \times 5$ yksi ratsun kierros on seuraava: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (5,5); +\node at (0.5,4.5) {$1$}; +\node at (1.5,4.5) {$4$}; +\node at (2.5,4.5) {$11$}; +\node at (3.5,4.5) {$16$}; +\node at (4.5,4.5) {$25$}; +\node at (0.5,3.5) {$12$}; +\node at (1.5,3.5) {$17$}; +\node at (2.5,3.5) {$2$}; +\node at (3.5,3.5) {$5$}; +\node at (4.5,3.5) {$10$}; +\node at (0.5,2.5) {$3$}; +\node at (1.5,2.5) {$20$}; +\node at (2.5,2.5) {$7$}; +\node at (3.5,2.5) {$24$}; +\node at (4.5,2.5) {$15$}; +\node at (0.5,1.5) {$18$}; +\node at (1.5,1.5) {$13$}; +\node at (2.5,1.5) {$22$}; +\node at (3.5,1.5) {$9$}; +\node at (4.5,1.5) {$6$}; +\node at (0.5,0.5) {$21$}; +\node at (1.5,0.5) {$8$}; +\node at (2.5,0.5) {$19$}; +\node at (3.5,0.5) {$14$}; +\node at (4.5,0.5) {$23$}; +\end{tikzpicture} +\end{center} + +Ratsun kierros shakkilaudalla vastaa Hamiltonin polkua verkossa, +jonka solmut ovat ruutuja ja kahden solmun välillä on kaari, +jos ratsu pystyy siirtymään solmusta toiseen shakin sääntöjen mukaisesti. + +Peruuttava haku on luonteva menetelmä ratsun kierroksen muodostamiseen. +Hakua voi tehostaa erilaisilla \key{heuristiikoilla}, +jotka pyrkivät ohjaamaan ratsua niin, että kokonainen kierros +tulee valmiiksi nopeasti. + +\subsubsection{Warnsdorffin sääntö} + +\index{heuristiikka@heuristiikka} +\index{Warnsdorffin sxxntz@Warnsdorffin sääntö} + +\key{Warnsdorffin sääntö} on yksinkertainen +ja hyvä heuristiikka +ratsun kierroksen etsimiseen. +Sen avulla on mahdollista löytää nopeasti ratsun kierros +suurestakin ruudukosta. +Ideana on siirtää ratsua aina niin, +että se päätyy ruutuun, josta on mahdollisimman \emph{vähän} +mahdollisuuksia jatkaa kierrosta. + +Esimerkiksi seuraavassa tilanteessa on valittavana +viisi ruutua, joihin ratsu voi siirtyä: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (5,5); +\node at (0.5,4.5) {$1$}; +\node at (2.5,3.5) {$2$}; +\node at (4.5,4.5) {$a$}; +\node at (0.5,2.5) {$b$}; +\node at (4.5,2.5) {$e$}; +\node at (1.5,1.5) {$c$}; +\node at (3.5,1.5) {$d$}; +\end{tikzpicture} +\end{center} +Tässä tapauksessa Warnsdorffin sääntö valitsee ruudun $a$, +koska tämän valinnan jälkeen on vain yksi mahdollisuus +jatkaa kierrosta. Muissa valinnoissa mahdollisuuksia olisi kolme. + + diff --git a/luku20.tex b/luku20.tex new file mode 100644 index 0000000..4158899 --- /dev/null +++ b/luku20.tex @@ -0,0 +1,1360 @@ +\chapter{Network flows} + +Annettuna on suunnattu, painotettu verkko, +josta on valittu tietty alkusolmu ja loppusolmu. +Tarkastelemme seuraavia ongelmia: + +\begin{itemize} +\item \key{Maksimivirtauksen etsiminen}: +Kuinka paljon virtausta on mahdollista kuljettaa +verkon alkusolmusta loppusolmuun kaaria pitkin? +\item \key{Minimileikkauksen etsiminen}: +Mikä on yhteispainoltaan pienin joukko kaaria, +joiden poistaminen erottaa alkusolmun loppusolmusta? +\end{itemize} + +Osoittautuu, että nämä ongelmat vastaavat toisiaan +ja ne on mahdollista ratkaista samanaikaisesti +toistensa avulla. + +Käytämme esimerkkinä seuraavaa verkkoa, +jossa solmu 1 on alkusolmu ja solmu 6 on loppusolmu: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (7,2) {$6$}; +\node[draw, circle] (5) at (3,1) {$4$}; +\node[draw, circle] (6) at (5,1) {$5$}; +\path[draw,thick,->] (1) -- node[font=\small,label=5] {} (2); +\path[draw,thick,->] (2) -- node[font=\small,label=6] {} (3); +\path[draw,thick,->] (3) -- node[font=\small,label=5] {} (4); +\path[draw,thick,->] (1) -- node[font=\small,label=below:4] {} (5); +\path[draw,thick,->] (5) -- node[font=\small,label=below:1] {} (6); +\path[draw,thick,->] (6) -- node[font=\small,label=below:2] {} (4); +\path[draw,thick,<-] (2) -- node[font=\small,label=left:3] {} (5); +\path[draw,thick,->] (3) -- node[font=\small,label=left:8] {} (6); +\end{tikzpicture} +\end{center} + +\subsubsection{Maksimivirtaus} + +\index{virtaus@virtaus} +\index{maksimivirtaus@maksimivirtaus} + +\key{Maksimivirtaus} on suurin +mahdollinen verkossa kulkeva virtaus, +joka lähtee liikkeelle alkusolmusta ja +päätyy loppusolmuun. +Kunkin kaaren paino on kapasiteetti, +joka ilmaisee, kuinka paljon virtausta kaaren +kautta voi kulkea. +Kaikissa verkon solmuissa alku- +ja loppusolmua lukuun ottamatta +lähtevän ja tulevan virtauksen on oltava yhtä suuri. + +Esimerkkiverkon maksimivirtaus on seuraava: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (7,2) {$6$}; +\node[draw, circle] (5) at (3,1) {$4$}; +\node[draw, circle] (6) at (5,1) {$5$}; +\path[draw,thick,->] (1) -- node[font=\small,label=3/5] {} (2); +\path[draw,thick,->] (2) -- node[font=\small,label=6/6] {} (3); +\path[draw,thick,->] (3) -- node[font=\small,label=5/5] {} (4); +\path[draw,thick,->] (1) -- node[font=\small,label=below:4/4] {} (5); +\path[draw,thick,->] (5) -- node[font=\small,label=below:1/1] {} (6); +\path[draw,thick,->] (6) -- node[font=\small,label=below:2/2] {} (4); +\path[draw,thick,<-] (2) -- node[font=\small,label=left:3/3] {} (5); +\path[draw,thick,->] (3) -- node[font=\small,label=left:1/8] {} (6); +\end{tikzpicture} +\end{center} + +Merkintä $v/k$ kaaressa tarkoittaa, +että kaaressa kulkee virtausta $v$ +ja kaaren kapasiteetti on $k$. +Jokaiselle kaarelle tulee päteä $v \le k$. +Tässä verkossa +maksimivirtauksen suuruus on 7, koska alkusolmusta +lähtevä virtaus on $3+4=7$ ja loppusolmuun +tuleva virtaus on $5+2=7$. + +Huomaa, että jokaisessa välisolmussa tulevan ja +lähtevän virtauksen määrä on yhtä suuri. +Esimerkiksi solmuun 2 tulee virtausta $3+3=6$ yksikköä solmuista 1 ja 4 +ja siitä lähtee virtausta $6$ yksikköä solmuun 3. + +\subsubsection{Minimileikkaus} + +\index{leikkaus@leikkaus} +\index{minimileikkaus@minimileikkaus} + +\key{Minimileikkaus} on yhteispainoltaan +pienin mahdollinen joukko verkon kaaria, +joiden poistaminen estää kulkemisen +alkusolmusta loppusolmuun. +Leikkauksen jälkeen alkuosaan kuuluvat +solmut, joihin pääsee alkusolmusta, +ja loppuosaan kuuluvat muut verkon solmut, +mukaan lukien loppusolmu. + +Esimerkkiverkon minimileikkaus on seuraava: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (7,2) {$6$}; +\node[draw, circle] (5) at (3,1) {$4$}; +\node[draw, circle] (6) at (5,1) {$5$}; +\path[draw,thick,->] (1) -- node[font=\small,label=5] {} (2); +\path[draw,thick,->] (2) -- node[font=\small,label=6] {} (3); +\path[draw,thick,->] (3) -- node[font=\small,label=5] {} (4); +\path[draw,thick,->] (1) -- node[font=\small,label=below:4] {} (5); +\path[draw,thick,->] (5) -- node[font=\small,label=below:1] {} (6); +\path[draw,thick,->] (6) -- node[font=\small,label=below:2] {} (4); +\path[draw,thick,<-] (2) -- node[font=\small,label=left:3] {} (5); +\path[draw,thick,->] (3) -- node[font=\small,label=left:8] {} (6); + +\path[draw=red,thick,-,line width=2pt] (4-.3,3-.3) -- (4+.3,3+.3); +\path[draw=red,thick,-,line width=2pt] (4-.3,3+.3) -- (4+.3,3-.3); +\path[draw=red,thick,-,line width=2pt] (4-.3,1-.3) -- (4+.3,1+.3); +\path[draw=red,thick,-,line width=2pt] (4-.3,1+.3) -- (4+.3,1-.3); +\end{tikzpicture} +\end{center} + +Tässä leikkauksessa alkuosassa ovat solmut $\{1,2,4\}$ +ja loppuosassa ovat solmut $\{3,5,6\}$. +Minimileikkauksen paino on 7, +koska alkuosasta loppuosaan kulkevat +kaaret $2 \rightarrow 3$ ja $4 \rightarrow 5$, +joiden yhteispaino on $6+1=7$. +\\\\ +Ei ole sattumaa, että esimerkkiverkossa +sekä maksimivirtauksen suuruus +että minimileikkauksen paino on 7. +Virtauslaskennan keskeinen tulos on, +että verkon maksimivirtaus ja +minimileikkaus +ovat \textit{aina} yhtä suuret, +eli käsitteet kuvaavat saman asian +kahta eri puolta. + +Seuraavaksi tutustumme Ford–Fulkersonin +algoritmiin, jolla voi etsiä verkon +maksimivirtauksen ja +minimileikkauksen. +Algoritmi auttaa myös ymmärtämään, +\textit{miksi} maksimivirtaus ja +minimileikkaus ovat yhtä suuret. + +\section{Ford–Fulkersonin algoritmi} + +\index{Ford–Fulkersonin algoritmi} + +\key{Ford–Fulkersonin algoritmi} etsii verkon maksimivirtauksen. +Algoritmi aloittaa tilanteesta, +jossa virtaus on 0, ja alkaa sitten etsiä verkosta polkuja, +jotka tuottavat siihen lisää virtausta. +Kun mitään polkua ei enää pysty muodostamaan, +maksimivirtaus on tullut valmiiksi. + +Algoritmi käsittelee verkkoa muodossa, +jossa jokaiselle kaarelle on vastakkaiseen +suuntaan kulkeva pari. +Kaaren paino kuvastaa, miten paljon +lisää virtausta sen kautta pystyy vielä kulkemaan. +Aluksi alkuperäisen verkon kaarilla on +painona niiden kapasiteetti +ja käänteisillä kaarilla on painona 0. + +\begin{samepage} +Esimerkkiverkosta syntyy seuraava verkko: + +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle] (1) at (1,1.3) {$1$}; +\node[draw, circle] (2) at (3,2.6) {$2$}; +\node[draw, circle] (3) at (5,2.6) {$3$}; +\node[draw, circle] (4) at (7,1.3) {$6$}; +\node[draw, circle] (5) at (3,0) {$4$}; +\node[draw, circle] (6) at (5,0) {$5$}; + +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=5] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=below:0] {} (1); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=6] {} (3); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=below:0] {} (2); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=5] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:0] {} (3); +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=4] {} (5); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=below:0] {} (1); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=1] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=below:0] {} (5); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=2] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:0] {} (6); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=left:3] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=right:0] {} (5); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=right:8] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=left:0] {} (3); +\end{tikzpicture} +\end{center} +\end{samepage} + +\subsubsection{Algoritmin toiminta} + +Ford–Fulkersonin algoritmi etsii verkosta joka vaiheessa polun, +joka alkaa alkusolmusta, +päättyy loppusolmuun ja jossa jokaisen kaaren +paino on positiivinen. +Jos vaihtoehtoja on useita, mikä tahansa valinta kelpaa. + +Esimerkkiverkossa voimme valita vaikkapa seuraavan polun: + +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle] (1) at (1,1.3) {$1$}; +\node[draw, circle] (2) at (3,2.6) {$2$}; +\node[draw, circle] (3) at (5,2.6) {$3$}; +\node[draw, circle] (4) at (7,1.3) {$6$}; +\node[draw, circle] (5) at (3,0) {$4$}; +\node[draw, circle] (6) at (5,0) {$5$}; + +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=5] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=below:0] {} (1); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=6] {} (3); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=below:0] {} (2); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=5] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:0] {} (3); +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=4] {} (5); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=below:0] {} (1); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=1] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=below:0] {} (5); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=2] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:0] {} (6); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=left:3] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=right:0] {} (5); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=right:8] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=left:0] {} (3); + +\path[draw=red,thick,->,line width=2pt] (1) edge [bend left=10] (2); +\path[draw=red,thick,->,line width=2pt] (2) edge [bend left=10] (3); +\path[draw=red,thick,->,line width=2pt] (3) edge [bend left=10] (6); +\path[draw=red,thick,->,line width=2pt] (6) edge [bend left=10] (4); +\end{tikzpicture} +\end{center} + +Polun valinnan jälkeen virtaus lisääntyy $x$ yksikköä, +jossa $x$ on pienin kaaren kapasiteetti polulla. +Samalla jokaisen polulla olevan kaaren kapasiteetti +vähenee $x$:llä ja jokaisen käänteisen kaaren kapasiteetti kasvaa $x$:llä. + +Yllä valitussa polussa +kaarten kapasiteetit ovat 5, 6, 8 ja 2. +Pienin kapasiteetti on 2, +joten virtaus kasvaa 2:lla +ja verkko muuttuu seuraavasti: + +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle] (1) at (1,1.3) {$1$}; +\node[draw, circle] (2) at (3,2.6) {$2$}; +\node[draw, circle] (3) at (5,2.6) {$3$}; +\node[draw, circle] (4) at (7,1.3) {$6$}; +\node[draw, circle] (5) at (3,0) {$4$}; +\node[draw, circle] (6) at (5,0) {$5$}; + +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=3] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=below:2] {} (1); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=4] {} (3); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=below:2] {} (2); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=5] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:0] {} (3); +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=4] {} (5); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=below:0] {} (1); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=1] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=below:0] {} (5); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=0] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:2] {} (6); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=left:3] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=right:0] {} (5); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=right:6] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=left:2] {} (3); +\end{tikzpicture} +\end{center} + +Muutoksessa on ideana, että virtauksen lisääminen +vähentää polkuun kuuluvien kaarten kykyä välittää virtausta. +Toisaalta virtausta on mahdollista peruuttaa myöhemmin +käyttämällä käänteisiä kaaria, jos osoittautuu, että +virtausta on järkevää reitittää verkossa toisella tavalla. + +Algoritmi kasvattaa virtausta +niin kauan, kuin verkossa on olemassa polku +alkusolmusta loppusolmuun positiivisia kaaria pitkin. +Tässä tapauksessa +voimme valita seuraavan polun vaikkapa näin: + +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle] (1) at (1,1.3) {$1$}; +\node[draw, circle] (2) at (3,2.6) {$2$}; +\node[draw, circle] (3) at (5,2.6) {$3$}; +\node[draw, circle] (4) at (7,1.3) {$6$}; +\node[draw, circle] (5) at (3,0) {$4$}; +\node[draw, circle] (6) at (5,0) {$5$}; + +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=3] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=below:2] {} (1); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=4] {} (3); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=below:2] {} (2); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=5] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:0] {} (3); +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=4] {} (5); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=below:0] {} (1); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=1] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=below:0] {} (5); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=0] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:2] {} (6); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=left:3] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=right:0] {} (5); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=right:6] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=left:2] {} (3); + +\path[draw=red,thick,->,line width=2pt] (1) edge [bend left=10] (5); +\path[draw=red,thick,->,line width=2pt] (5) edge [bend left=10] (2); +\path[draw=red,thick,->,line width=2pt] (2) edge [bend left=10] (3); +\path[draw=red,thick,->,line width=2pt] (3) edge [bend left=10] (4); +\end{tikzpicture} +\end{center} + +Tämän polun pienin kapasiteetti on 3, +joten polku kasvattaa virtausta 3:lla +ja kokonaisvirtaus polun käsittelyn jälkeen on 5. + +\begin{samepage} +Nyt verkko muuttuu seuraavasti: + +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle] (1) at (1,1.3) {$1$}; +\node[draw, circle] (2) at (3,2.6) {$2$}; +\node[draw, circle] (3) at (5,2.6) {$3$}; +\node[draw, circle] (4) at (7,1.3) {$6$}; +\node[draw, circle] (5) at (3,0) {$4$}; +\node[draw, circle] (6) at (5,0) {$5$}; + +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=3] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=below:2] {} (1); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=1] {} (3); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=below:5] {} (2); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=2] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:3] {} (3); +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=1] {} (5); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=below:3] {} (1); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=1] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=below:0] {} (5); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=0] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:2] {} (6); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=left:0] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=right:3] {} (5); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=right:6] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=left:2] {} (3); +\end{tikzpicture} +\end{center} +\end{samepage} + +Maksimivirtaus tulee valmiiksi +lisäämällä virtausta vielä polkujen $1 \rightarrow 2 \rightarrow 3 \rightarrow 6$ ja +$1 \rightarrow 4 \rightarrow 5 \rightarrow 3 \rightarrow 6$ avulla. +Molemmat polut tuottavat 1 yksikön lisää virtausta, +ja lopullinen verkko on seuraava: + +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle] (1) at (1,1.3) {$1$}; +\node[draw, circle] (2) at (3,2.6) {$2$}; +\node[draw, circle] (3) at (5,2.6) {$3$}; +\node[draw, circle] (4) at (7,1.3) {$6$}; +\node[draw, circle] (5) at (3,0) {$4$}; +\node[draw, circle] (6) at (5,0) {$5$}; + +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=2] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=below:3] {} (1); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=0] {} (3); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=below:6] {} (2); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=0] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:5] {} (3); +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=0] {} (5); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=below:4] {} (1); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=0] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=below:1] {} (5); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=0] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:2] {} (6); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=left:0] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=right:3] {} (5); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=right:7] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=left:1] {} (3); +\end{tikzpicture} +\end{center} + +Nyt virtausta ei pysty enää kasvattamaan, +koska verkossa ei ole mitään polkua +alkusolmusta loppusolmuun, +jossa jokaisen kaaren paino olisi positiivinen. +Niinpä algoritmi pysähtyy ja verkon maksimivirtaus on 7. + +\subsubsection{Polun valinta} + +Ford–Fulkersonin algoritmi ei ota kantaa siihen, +millä tavoin virtausta kasvattava polku valitaan verkossa. +Valintatavasta riippumatta algoritmi pysähtyy +ja tuottaa maksimivirtauksen ennemmin tai myöhemmin, +mutta polun valinnalla on vaikutusta algoritmin tehokkuuteen. + +Yksinkertainen tapa on valita virtausta kasvattava +polku syvyyshaulla. +Tämä toimii usein hyvin, mutta pahin tapaus on, +että jokainen polku +kasvattaa virtausta vain 1:llä ja algoritmi toimii hitaasti. +Onneksi tämän ilmiön pystyy estämään käyttämällä +jompaakumpaa seuraavaa algoritmia: + +\index{Edmonds–Karpin algoritmi} + +\key{Edmonds–Karpin algoritmi} on +Ford–Fulkersonin algoritmin toteutus, +jossa virtausta kasvattava polku valitaan +aina niin, että siinä on mahdollisimman vähän kaaria. +Tämä onnistuu etsimällä polku syvyyshaun +sijasta leveyshaulla. +Osoittautuu, että tämä varmistaa virtauksen +kasvamisen nopeasti ja +maksimivirtauksen etsiminen vie aikaa $O(m^2 n)$. + +\index{skaalaava algoritmi@skaalaava algoritmi} + +\key{Skaalaava algoritmi} +asettaa minimiarvon, joka on ensin alkusolmusta +lähtevien kaarten kapasiteettien summa $c$. +Joka vaiheessa verkosta etsitään +syvyyshaulla polku, jonka jokaisen kaaren kapasiteetti +on vähintään minimiarvo. +Aina jos kelvollista polkua ei löydy, +minimiarvo jaetaan 2:lla, +kunnes lopuksi minimiarvo on 1. +Algoritmin aikavaativuus on $O(m^2 \log c)$. + +Käytännössä skaalaava algoritmi on mukavampi koodattava, +koska siinä riittää etsiä polku syvyyshaulla. +Molemmat algoritmit ovat yleensä aina riittävän +nopeita ohjelmointikisoissa esiintyviin tehtäviin. + +\subsubsection{Minimileikkaus} + +\index{minimileikkaus@minimileikkaus} + +Osoittautuu, että kun Ford–Fulkersonin algoritmi on saanut valmiiksi +maksimivirtauksen, se on tuottanut samalla minimileikkauksen. +Olkoon $A$ niiden solmujen joukko, +joihin verkossa pääsee +alkusolmusta positiivisia kaaria pitkin. +Esimerkkiverkossa $A$ sisältää solmut 1, 2 ja 4: + +\begin{center} +\begin{tikzpicture}[scale=0.9,label distance=-2mm] +\node[draw, circle,fill=lightgray] (1) at (1,1.3) {$1$}; +\node[draw, circle,fill=lightgray] (2) at (3,2.6) {$2$}; +\node[draw, circle] (3) at (5,2.6) {$3$}; +\node[draw, circle] (4) at (7,1.3) {$6$}; +\node[draw, circle,fill=lightgray] (5) at (3,0) {$4$}; +\node[draw, circle] (6) at (5,0) {$5$}; + +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=2] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=below:3] {} (1); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=0] {} (3); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=below:6] {} (2); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=0] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:5] {} (3); +\path[draw,thick,->] (1) edge [bend left=10] node[font=\small,label=0] {} (5); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=below:4] {} (1); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=0] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=below:1] {} (5); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=0] {} (4); +\path[draw,thick,->] (4) edge [bend left=10] node[font=\small,label=below:2] {} (6); +\path[draw,thick,->] (5) edge [bend left=10] node[font=\small,label=left:0] {} (2); +\path[draw,thick,->] (2) edge [bend left=10] node[font=\small,label=right:3] {} (5); +\path[draw,thick,->] (3) edge [bend left=10] node[font=\small,label=right:7] {} (6); +\path[draw,thick,->] (6) edge [bend left=10] node[font=\small,label=left:1] {} (3); +\end{tikzpicture} +\end{center} + +Nyt minimileikkauksen muodostavat ne alkuperäisen verkon kaaret, +jotka kulkevat joukosta $A$ joukon $A$ ulkopuolelle +ja joiden kapasiteetti on täysin käytetty +maksimivirtauksessa. +Tässä verkossa kyseiset kaaret ovat $2 \rightarrow 3$ +ja $4 \rightarrow 5$, jotka tuottavat minimileikkauksen $6+1=7$. + +Miksi sitten algoritmin tuottama virtaus ja leikkaus ovat +varmasti maksimivirtaus ja minimileikkaus? +Syynä tähän on, että verkossa ei voi olla virtausta, +jonka suuruus ylittäisi yhdenkään +verkossa olevan leikkauksen painoa. +Niinpä kun verkossa oleva +virtaus ja leikkaus ovat yhtä suuret, +ne ovat varmasti maksimivirtaus ja minimileikkaus. + +Tarkastellaan mitä tahansa verkon leikkausta, +jossa alkusolmu kuuluu osaan $A$, +loppusolmu kuuluu osaan $B$ ja osien välillä kulkee kaaria: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\draw[dashed] (-2,0) circle (1.5); +\draw[dashed] (2,0) circle (1.5); + +\node at (-2,-1) {$A$}; +\node at (2,-1) {$B$}; + +\node[draw, circle] (1) at (-1,0.5) {}; +\node[draw, circle] (2) at (-1,0) {}; +\node[draw, circle] (3) at (-1,-0.5) {}; +\node[draw, circle] (4) at (1,0.5) {}; +\node[draw, circle] (5) at (1,0) {}; +\node[draw, circle] (6) at (1,-0.5) {}; + +\path[draw,thick,->] (1) -- (4); +\path[draw,thick,->] (5) -- (2); +\path[draw,thick,->] (3) -- (6); + +\end{tikzpicture} +\end{center} + +Leikkauksen paino on niiden kaarten painojen summa, +jotka kulkevat osasta $A$ osaan $B$. +Tämä on yläraja sille, kuinka suuri verkossa oleva virtaus voi olla, +koska virtauksen täytyy edetä osasta $A$ osaan $B$. +Niinpä maksimivirtaus on pienempi tai yhtä suuri kuin +mikä tahansa verkon leikkaus. + +Toisaalta Ford–Fulkersonin algoritmi tuottaa virtauksen, +joka on \emph{tarkalleen} yhtä suuri kuin verkossa oleva leikkaus. +Niinpä tämän virtauksen on oltava maksimivirtaus ja +vastaavasti leikkauksen on oltava minimileikkaus. + +\section{Rinnakkaiset polut} + +Ensimmäisenä virtauslaskennan sovelluksena tarkastelemme +tehtävää, jossa tavoitteena on muodostaa mahdollisimman +monta rinnakkaista polkua verkon alkusolmusta loppusolmuun. +Vaatimuksena on, että jokainen verkon kaari esiintyy +enintään yhdellä polulla. + +Esimerkiksi verkossa +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (3,1) {$4$}; +\node[draw, circle] (5) at (5,1) {$5$}; +\node[draw, circle] (6) at (7,2) {$6$}; +\path[draw,thick,->] (1) -- (2); +\path[draw,thick,->] (1) -- (4); +\path[draw,thick,->] (2) -- (4); +\path[draw,thick,->] (3) -- (2); +\path[draw,thick,->] (3) -- (5); +\path[draw,thick,->] (3) -- (6); +\path[draw,thick,->] (4) -- (3); +\path[draw,thick,->] (4) -- (5); +\path[draw,thick,->] (5) -- (6); +\end{tikzpicture} +\end{center} +pystyy muodostamaan kaksi rinnakkaista polkua solmusta 1 solmuun 6. +Tämä toteutuu valitsemalla polut +$1 \rightarrow 2 \rightarrow 4 \rightarrow 3 \rightarrow 6$ +ja $1 \rightarrow 4 \rightarrow 5 \rightarrow 6$: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (3,1) {$4$}; +\node[draw, circle] (5) at (5,1) {$5$}; +\node[draw, circle] (6) at (7,2) {$6$}; +\path[draw,thick,->] (1) -- (2); +\path[draw,thick,->] (1) -- (4); +\path[draw,thick,->] (2) -- (4); +\path[draw,thick,->] (3) -- (2); +\path[draw,thick,->] (3) -- (5); +\path[draw,thick,->] (3) -- (6); +\path[draw,thick,->] (4) -- (3); +\path[draw,thick,->] (4) -- (5); +\path[draw,thick,->] (5) -- (6); + +\path[draw=green,thick,->,line width=2pt] (1) -- (2); +\path[draw=green,thick,->,line width=2pt] (2) -- (4); +\path[draw=green,thick,->,line width=2pt] (4) -- (3); +\path[draw=green,thick,->,line width=2pt] (3) -- (6); + +\path[draw=blue,thick,->,line width=2pt] (1) -- (4); +\path[draw=blue,thick,->,line width=2pt] (4) -- (5); +\path[draw=blue,thick,->,line width=2pt] (5) -- (6); +\end{tikzpicture} +\end{center} + +Osoittautuu, että suurin rinnakkaisten polkujen määrä +on yhtä suuri kuin maksimivirtaus verkossa, +jossa jokaisen kaaren kapasiteetti on 1. +Kun maksimivirtaus on muodostettu, +rinnakkaiset polut voi löytää +ahneesti etsimällä alkusolmusta loppusolmuun +kulkevia polkuja. + +Tarkastellaan sitten tehtävän muunnelmaa, +jossa jokainen solmu (alku- ja loppusolmua lukuun ottamatta) +saa esiintyä enintään yhdellä polulla. +Tämän rajoituksen seurauksena äskeisessä verkossa +voi muodostaa vain yhden polun, +koska solmu 4 ei voi esiintyä monella polulla: + +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (1,2) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (5,3) {$3$}; +\node[draw, circle] (4) at (3,1) {$4$}; +\node[draw, circle] (5) at (5,1) {$5$}; +\node[draw, circle] (6) at (7,2) {$6$}; +\path[draw,thick,->] (1) -- (2); +\path[draw,thick,->] (1) -- (4); +\path[draw,thick,->] (2) -- (4); +\path[draw,thick,->] (3) -- (2); +\path[draw,thick,->] (3) -- (5); +\path[draw,thick,->] (3) -- (6); +\path[draw,thick,->] (4) -- (3); +\path[draw,thick,->] (4) -- (5); +\path[draw,thick,->] (5) -- (6); + +\path[draw=green,thick,->,line width=2pt] (1) -- (2); +\path[draw=green,thick,->,line width=2pt] (2) -- (4); +\path[draw=green,thick,->,line width=2pt] (4) -- (3); +\path[draw=green,thick,->,line width=2pt] (3) -- (6); +\end{tikzpicture} +\end{center} + +Tavallinen keino rajoittaa solmun kautta kulkevaa +virtausta on jakaa solmu tulosolmuksi ja lähtösolmuksi. +Kaikki solmuun tulevat kaaret saapuvat tulosolmuun +ja kaikki solmusta lähtevät kaaret poistuvat lähtösolmusta. +Lisäksi tulosolmusta lähtösolmuun on kaari, +jossa on haluttu kapasiteetti. + +Tässä tapauksessa verkosta tulee seuraava: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (1,2) {$1$}; + +\node[draw, circle] (2a) at (3,3) {$2$}; +\node[draw, circle] (3a) at (6,3) {$3$}; +\node[draw, circle] (4a) at (3,1) {$4$}; +\node[draw, circle] (5a) at (6,1) {$5$}; + +\node[draw, circle] (2b) at (4,3) {$2$}; +\node[draw, circle] (3b) at (7,3) {$3$}; +\node[draw, circle] (4b) at (4,1) {$4$}; +\node[draw, circle] (5b) at (7,1) {$5$}; + +\node[draw, circle] (6) at (9,2) {$6$}; + +\path[draw,thick,->] (2a) -- (2b); +\path[draw,thick,->] (3a) -- (3b); +\path[draw,thick,->] (4a) -- (4b); +\path[draw,thick,->] (5a) -- (5b); + +\path[draw,thick,->] (1) -- (2a); +\path[draw,thick,->] (1) -- (4a); +\path[draw,thick,->] (2b) -- (4a); +\path[draw,thick,->] (3b) edge [bend right=30] (2a); +\path[draw,thick,->] (3b) -- (5a); +\path[draw,thick,->] (3b) -- (6); +\path[draw,thick,->] (4b) -- (3a); +\path[draw,thick,->] (4b) -- (5a); +\path[draw,thick,->] (5b) -- (6); +\end{tikzpicture} +\end{center} + +Tämän verkon maksimivirtaus on: +\begin{center} +\begin{tikzpicture} +\node[draw, circle] (1) at (1,2) {$1$}; + +\node[draw, circle] (2a) at (3,3) {$2$}; +\node[draw, circle] (3a) at (6,3) {$3$}; +\node[draw, circle] (4a) at (3,1) {$4$}; +\node[draw, circle] (5a) at (6,1) {$5$}; + +\node[draw, circle] (2b) at (4,3) {$2$}; +\node[draw, circle] (3b) at (7,3) {$3$}; +\node[draw, circle] (4b) at (4,1) {$4$}; +\node[draw, circle] (5b) at (7,1) {$5$}; + +\node[draw, circle] (6) at (9,2) {$6$}; + +\path[draw,thick,->] (2a) -- (2b); +\path[draw,thick,->] (3a) -- (3b); +\path[draw,thick,->] (4a) -- (4b); +\path[draw,thick,->] (5a) -- (5b); + +\path[draw,thick,->] (1) -- (2a); +\path[draw,thick,->] (1) -- (4a); +\path[draw,thick,->] (2b) -- (4a); +\path[draw,thick,->] (3b) edge [bend right=30] (2a); +\path[draw,thick,->] (3b) -- (5a); +\path[draw,thick,->] (3b) -- (6); +\path[draw,thick,->] (4b) -- (3a); +\path[draw,thick,->] (4b) -- (5a); +\path[draw,thick,->] (5b) -- (6); + +\path[draw=red,thick,->,line width=2pt] (1) -- (2a); +\path[draw=red,thick,->,line width=2pt] (2a) -- (2b); +\path[draw=red,thick,->,line width=2pt] (2b) -- (4a); +\path[draw=red,thick,->,line width=2pt] (4a) -- (4b); +\path[draw=red,thick,->,line width=2pt] (4b) -- (3a); +\path[draw=red,thick,->,line width=2pt] (3a) -- (3b); +\path[draw=red,thick,->,line width=2pt] (3b) -- (6); +\end{tikzpicture} +\end{center} + +Tämä tarkoittaa, että verkossa on mahdollista muodostaa +vain yksi polku alkusolmusta lähtösolmuun, +kun sama solmu ei saa esiintyä monessa polussa. + +\section{Maksimiparitus} + +\index{paritus@paritus} +\index{maksimiparitus@maksimiparitus} + +\key{Maksimiparitus} on suurin mahdollinen joukko +verkon solmuista muodostettuja pareja, +jolle pätee, +että jokaisen parin välillä on kaari verkossa +ja jokainen solmu kuuluu enintään yhteen pariin. + +Maksimiparituksen etsimiseen yleisessä +verkossa on olemassa polynominen algoritmi, +mutta se on hyvin monimutkainen. +Tässä luvussa keskitymmekin tilanteeseen, +jossa verkko on kaksijakoinen. +Tällöin maksimiparituksen pystyy etsimään +helposti virtauslaskennan avulla. + +\subsubsection{Maksimiparituksen etsiminen} + +Kaksijakoinen verkko voidaan esittää niin, +että se muodostuu vasemman ja oikean puolen +solmuista ja kaikki verkon kaaret kulkevat puolten välillä. +Tarkastellaan esimerkkinä seuraavaa verkkoa: + +\begin{center} +\begin{tikzpicture}[scale=0.60] +\node[draw, circle] (1) at (2,4.5) {1}; +\node[draw, circle] (2) at (2,3) {2}; +\node[draw, circle] (3) at (2,1.5) {3}; +\node[draw, circle] (4) at (2,0) {4}; +\node[draw, circle] (5) at (8,4.5) {5}; +\node[draw, circle] (6) at (8,3) {6}; +\node[draw, circle] (7) at (8,1.5) {7}; +\node[draw, circle] (8) at (8,0) {8}; + +\path[draw,thick,-] (1) -- (5); +\path[draw,thick,-] (2) -- (7); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (3) -- (8); +\path[draw,thick,-] (4) -- (7); +\end{tikzpicture} +\end{center} + +Tässä verkossa maksimiparituksen koko on 3: +\begin{center} +\begin{tikzpicture}[scale=0.60] +\node[draw, circle] (1) at (2,4.5) {1}; +\node[draw, circle] (2) at (2,3) {2}; +\node[draw, circle] (3) at (2,1.5) {3}; +\node[draw, circle] (4) at (2,0) {4}; +\node[draw, circle] (5) at (8,4.5) {5}; +\node[draw, circle] (6) at (8,3) {6}; +\node[draw, circle] (7) at (8,1.5) {7}; +\node[draw, circle] (8) at (8,0) {8}; + +\path[draw,thick,-] (1) -- (5); +\path[draw,thick,-] (2) -- (7); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (3) -- (8); +\path[draw,thick,-] (4) -- (7); + +\path[draw=red,thick,-,line width=2pt] (1) -- (5); +\path[draw=red,thick,-,line width=2pt] (2) -- (7); +\path[draw=red,thick,-,line width=2pt] (3) -- (6); +\end{tikzpicture} +\end{center} + +Kaksijakoisen verkon maksimiparitus +vastaa maksimivirtausta verkossa, +johon on lisätty alkusolmu ja loppusolmu. +Alkusolmusta on kaari jokaiseen vasemman +puolen solmuun, ja vastaavasti loppusolmuun +on kaari jokaisesta oikean puolen solmusta. +Jokaisen kaaren kapasiteettina on 1. + +Esimerkissä tuloksena on seuraava verkko: + +\begin{center} +\begin{tikzpicture}[scale=0.60] +\node[draw, circle] (1) at (2,4.5) {1}; +\node[draw, circle] (2) at (2,3) {2}; +\node[draw, circle] (3) at (2,1.5) {3}; +\node[draw, circle] (4) at (2,0) {4}; +\node[draw, circle] (5) at (8,4.5) {5}; +\node[draw, circle] (6) at (8,3) {6}; +\node[draw, circle] (7) at (8,1.5) {7}; +\node[draw, circle] (8) at (8,0) {8}; + +\node[draw, circle] (a) at (-2,2.25) {\phantom{0}}; +\node[draw, circle] (b) at (12,2.25) {\phantom{0}}; + +\path[draw,thick,->] (1) -- (5); +\path[draw,thick,->] (2) -- (7); +\path[draw,thick,->] (3) -- (5); +\path[draw,thick,->] (3) -- (6); +\path[draw,thick,->] (3) -- (8); +\path[draw,thick,->] (4) -- (7); + +\path[draw,thick,->] (a) -- (1); +\path[draw,thick,->] (a) -- (2); +\path[draw,thick,->] (a) -- (3); +\path[draw,thick,->] (a) -- (4); +\path[draw,thick,->] (5) -- (b); +\path[draw,thick,->] (6) -- (b); +\path[draw,thick,->] (7) -- (b); +\path[draw,thick,->] (8) -- (b); +\end{tikzpicture} +\end{center} + +Tämän verkon maksimivirtaus on yhtä suuri kuin +alkuperäisen verkon maksimiparitus, +koska virtaus muodostuu joukosta polkuja +alkusolmusta loppusolmuun ja jokainen +polku ottaa mukaan uuden kaaren paritukseen. +Tässä tapauksessa maksimivirtaus on 3, +joten maksimiparitus on myös 3. + +\subsubsection{Hallin lause} + +\index{Hallin lause@Hallin lause} +\index{txydellinen paritus@täydellinen paritus} + +\key{Hallin lause} antaa ehdon, milloin kaksijakoiseen +verkkoon voidaan muodostaa paritus, +joka sisältää kaikki toisen puolen solmut. +Jos kummallakin puolella on yhtä monta solmua, +Hallin lause kertoo, voidaanko muodostaa +\key{täydellinen paritus}, +jossa kaikki solmut paritetaan keskenään. + +Oletetaan, että haluamme muodostaa parituksen, +johon kuuluvat kaikki vasemman puolen solmut. +Olkoon $X$ jokin joukko vasemman puolen solmuja +ja $f(X)$ näiden solmujen naapurien joukko. +Hallin lauseen mukaan paritus on mahdollinen, +kun jokaiselle joukolle $X$ +pätee $|X| \le |f(X)|$. + +Tarkastellaan Hallin lauseen merkitystä esimerkkiverkossa. +Valitaan ensin $X=\{1,3\}$, jolloin $f(X)=\{5,6,8\}$: + +\begin{center} +\begin{tikzpicture}[scale=0.60] +\node[draw, circle, fill=lightgray] (1) at (2,4.5) {1}; +\node[draw, circle] (2) at (2,3) {2}; +\node[draw, circle, fill=lightgray] (3) at (2,1.5) {3}; +\node[draw, circle] (4) at (2,0) {4}; +\node[draw, circle, fill=lightgray] (5) at (8,4.5) {5}; +\node[draw, circle, fill=lightgray] (6) at (8,3) {6}; +\node[draw, circle] (7) at (8,1.5) {7}; +\node[draw, circle, fill=lightgray] (8) at (8,0) {8}; + +\path[draw,thick,-] (1) -- (5); +\path[draw,thick,-] (2) -- (7); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (3) -- (8); +\path[draw,thick,-] (4) -- (7); +\end{tikzpicture} +\end{center} + +Tämä täyttää Hallin lauseen ehdon, koska $|X|=2$ ja $|f(X)|=3$. +Valitaan sitten $X=\{2,4\}$, jolloin $f(X)=\{7\}$: + +\begin{center} +\begin{tikzpicture}[scale=0.60] +\node[draw, circle] (1) at (2,4.5) {1}; +\node[draw, circle, fill=lightgray] (2) at (2,3) {2}; +\node[draw, circle] (3) at (2,1.5) {3}; +\node[draw, circle, fill=lightgray] (4) at (2,0) {4}; +\node[draw, circle] (5) at (8,4.5) {5}; +\node[draw, circle] (6) at (8,3) {6}; +\node[draw, circle, fill=lightgray] (7) at (8,1.5) {7}; +\node[draw, circle] (8) at (8,0) {8}; + +\path[draw,thick,-] (1) -- (5); +\path[draw,thick,-] (2) -- (7); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (3) -- (8); +\path[draw,thick,-] (4) -- (7); +\end{tikzpicture} +\end{center} + +Tässä tapauksessa $|X|=2$ ja $|f(X)|=1$, joten Hallin lauseen ehto +ei ole voimassa. +Tämä tarkoittaa, että verkossa +ei ole mahdollista muodostaa täydellistä paritusta, +johon kuuluvat kaikki vasemman puolen solmut. +Tämä on myös odotettu tulos, koska verkon maksimiparitus on 3 eikä 4. + +Jos Hallin lauseen ehto ei päde, osajoukko $X$ +kertoo syyn sille, miksi paritusta ei voi muodostaa. +Koska $X$ sisältää enemmän solmuja kuin $f(X)$, +kaikille $X$:n solmuille ei riitä paria oikealta. +Esimerkiksi yllä molemmat solmut 2 ja 4 tulisi +yhdistää solmuun 7, mutta tämä ei ole mahdollista. + +\subsubsection{Kőnigin lause} + +\index{Kőnigin lause} +\index{solmupeite@solmupeite} +\index{pienin solmupeite@pienin solmupeite} + +\key{Kőnigin lause} tarjoaa tehokkaan +tavan muodostaa kaksijakoiselle verkolle +\key{pienin solmupeite} eli pienin sellainen +solmujen joukko, että jokaisesta verkon kaaresta ainakin +toinen päätesolmuista kuuluu joukkoon. + +Yleisessä verkossa pienimmän solmupeitteen +etsiminen on NP-vaikea ongelma. +Sen sijaan kaksijakoisessa verkossa +Kőnigin lauseen nojalla maksimiparitus ja +pienin solmupeite ovat aina yhtä suuria, +minkä ansiosta +pienimmän solmupeitteen voi +etsiä tehokkaasti virtauslaskennan avulla. + +Tarkastellaan taas seuraavaa verkkoa, +jonka maksimiparituksen koko on 3: +\begin{center} +\begin{tikzpicture}[scale=0.60] +\node[draw, circle] (1) at (2,4.5) {1}; +\node[draw, circle] (2) at (2,3) {2}; +\node[draw, circle] (3) at (2,1.5) {3}; +\node[draw, circle] (4) at (2,0) {4}; +\node[draw, circle] (5) at (8,4.5) {5}; +\node[draw, circle] (6) at (8,3) {6}; +\node[draw, circle] (7) at (8,1.5) {7}; +\node[draw, circle] (8) at (8,0) {8}; + +\path[draw,thick,-] (1) -- (5); +\path[draw,thick,-] (2) -- (7); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (3) -- (8); +\path[draw,thick,-] (4) -- (7); + +\path[draw=red,thick,-,line width=2pt] (1) -- (5); +\path[draw=red,thick,-,line width=2pt] (2) -- (7); +\path[draw=red,thick,-,line width=2pt] (3) -- (6); +\end{tikzpicture} +\end{center} +Kőnigin lauseen ansiosta tiedämme nyt, +että myös pienimmän solmupeitteen koko on 3. +Solmupeite voidaan muodostaa seuraavasti: + +\begin{center} +\begin{tikzpicture}[scale=0.60] +\node[draw, circle, fill=lightgray] (1) at (2,4.5) {1}; +\node[draw, circle] (2) at (2,3) {2}; +\node[draw, circle, fill=lightgray] (3) at (2,1.5) {3}; +\node[draw, circle] (4) at (2,0) {4}; +\node[draw, circle] (5) at (8,4.5) {5}; +\node[draw, circle] (6) at (8,3) {6}; +\node[draw, circle, fill=lightgray] (7) at (8,1.5) {7}; +\node[draw, circle] (8) at (8,0) {8}; + +\path[draw,thick,-] (1) -- (5); +\path[draw,thick,-] (2) -- (7); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (3) -- (8); +\path[draw,thick,-] (4) -- (7); +\end{tikzpicture} +\end{center} +Pienin solmupeite muodostuu aina niin, +että jokaisesta maksimiparituksen kaaresta +toinen kaaren päätesolmuista kuuluu peitteeseen. + +\index{riippumaton joukko@riippumaton joukko} +\index{suurin riippumaton joukko@suurin riippumaton joukko} + +Kun verkosta valitaan kaikki solmut, +jotka \emph{eivät} kuulu pienimpään +solmupeitteeseen, syntyy +\key{suurin riippumaton joukko}. +Tämä on suurin mahdollinen joukko solmuja, +jossa minkään kahden solmun +välillä ei ole kaarta. +Pienimmän solmupeitteen tavoin +riippumattoman joukon muodostaminen on +NP-vaikea ongelma yleisessä verkossa, +mutta Kőnigin lauseen avulla +ongelma on mahdollista ratkaista +tehokkaasti kaksijakoisessa verkossa. +Esimerkkiverkossa suurin riippumaton joukko on seuraava: + +\begin{center} +\begin{tikzpicture}[scale=0.60] +\node[draw, circle] (1) at (2,4.5) {1}; +\node[draw, circle, fill=lightgray] (2) at (2,3) {2}; +\node[draw, circle] (3) at (2,1.5) {3}; +\node[draw, circle, fill=lightgray] (4) at (2,0) {4}; +\node[draw, circle, fill=lightgray] (5) at (8,4.5) {5}; +\node[draw, circle, fill=lightgray] (6) at (8,3) {6}; +\node[draw, circle] (7) at (8,1.5) {7}; +\node[draw, circle, fill=lightgray] (8) at (8,0) {8}; + +\path[draw,thick,-] (1) -- (5); +\path[draw,thick,-] (2) -- (7); +\path[draw,thick,-] (3) -- (5); +\path[draw,thick,-] (3) -- (6); +\path[draw,thick,-] (3) -- (8); +\path[draw,thick,-] (4) -- (7); +\end{tikzpicture} +\end{center} + +\section{Polkupeitteet} + +\index{polkupeite@polkupeite} + +\key{Polkupeite} on joukko verkon polkuja, +jotka on valittu niin, että jokainen verkon solmu kuuluu +ainakin yhteen polkuun. +Osoittautuu, että voimme muodostaa +virtauslaskennan avulla +pienimmän polkupeitteen suunnatussa, +syklittömässä verkossa. + +Polkupeitteestä on kaksi muunnelmaa: +\key{Solmuerillinen peite} on polkupeite, +jossa jokainen verkon solmu esiintyy tasan yhdessä polussa. +\key{Yleinen peite} taas on polkupeite, jossa sama solmu voi +esiintyä useammassa polussa. +Kummassakin tapauksessa pienin polkupeite löytyy +samanlaisella idealla. + +\subsubsection{Solmuerillinen peite} + +Tarkastellaan esimerkkinä seuraavaa verkkoa: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {1}; +\node[draw, circle] (2) at (2,0) {2}; +\node[draw, circle] (3) at (4,0) {3}; +\node[draw, circle] (4) at (6,0) {4}; +\node[draw, circle] (5) at (0,-2) {5}; +\node[draw, circle] (6) at (2,-2) {6}; +\node[draw, circle] (7) at (4,-2) {7}; + +\path[draw,thick,->,>=latex] (1) -- (5); +\path[draw,thick,->,>=latex] (2) -- (6); +\path[draw,thick,->,>=latex] (3) -- (4); +\path[draw,thick,->,>=latex] (5) -- (6); +\path[draw,thick,->,>=latex] (6) -- (3); +\path[draw,thick,->,>=latex] (6) -- (7); +\end{tikzpicture} +\end{center} + +Tässä tapauksessa pienin solmuerillinen polkupeite +muodostuu kolmesta polusta. +Voimme valita polut esimerkiksi seuraavasti: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {1}; +\node[draw, circle] (2) at (2,0) {2}; +\node[draw, circle] (3) at (4,0) {3}; +\node[draw, circle] (4) at (6,0) {4}; +\node[draw, circle] (5) at (0,-2) {5}; +\node[draw, circle] (6) at (2,-2) {6}; +\node[draw, circle] (7) at (4,-2) {7}; + +\path[draw=red,thick,->,line width=2pt] (1) -- (5); +\path[draw=red,thick,->,line width=2pt] (5) -- (6); +\path[draw=red,thick,->,line width=2pt] (6) -- (7); +\path[draw=red,thick,->,line width=2pt] (3) -- (4); +\end{tikzpicture} +\end{center} + +Huomaa, että yksi poluista sisältää vain solmun 2, +eli on sallittua, että polussa ei ole kaaria. + +Polkupeitteen etsiminen voidaan tulkita paritusongelmana +verkossa, jossa jokaista alkuperäisen verkon solmua +vastaa kaksi solmua: vasen ja oikea solmu. +Vasemmasta solmusta oikeaan solmuun on kaari, +jos tällainen kaari esiintyy alkuperäisessä verkossa. +Ideana on, että paritus määrittää, mitkä solmut +ovat yhteydessä toisiinsa poluissa. + +Esimerkkiverkossa tilanne on seuraava: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1a) at (0,6) {1}; +\node[draw, circle] (2a) at (0,5) {2}; +\node[draw, circle] (3a) at (0,4) {3}; +\node[draw, circle] (4a) at (0,3) {4}; +\node[draw, circle] (5a) at (0,2) {5}; +\node[draw, circle] (6a) at (0,1) {6}; +\node[draw, circle] (7a) at (0,0) {7}; + +\node[draw, circle] (1b) at (4,6) {1}; +\node[draw, circle] (2b) at (4,5) {2}; +\node[draw, circle] (3b) at (4,4) {3}; +\node[draw, circle] (4b) at (4,3) {4}; +\node[draw, circle] (5b) at (4,2) {5}; +\node[draw, circle] (6b) at (4,1) {6}; +\node[draw, circle] (7b) at (4,0) {7}; + +\node[draw, circle] (a) at (-3,3) {\phantom{0}}; +\node[draw, circle] (b) at (7,3) {\phantom{0}}; + +%\path[draw,thick,->,>=latex] (1a) -- (5b); +\path[draw,thick,->,>=latex] (2a) -- (6b); +%\path[draw,thick,->,>=latex] (3a) -- (4b); +%\path[draw,thick,->,>=latex] (5a) -- (6b); +\path[draw,thick,->,>=latex] (6a) -- (3b); +%\path[draw,thick,->,>=latex] (6a) -- (7b); + +\path[draw,thick,->,>=latex] (a) -- (1a); +\path[draw,thick,->,>=latex] (a) -- (2a); +\path[draw,thick,->,>=latex] (a) -- (3a); +\path[draw,thick,->,>=latex] (a) -- (4a); +\path[draw,thick,->,>=latex] (a) -- (5a); +\path[draw,thick,->,>=latex] (a) -- (6a); +\path[draw,thick,->,>=latex] (a) -- (7a); + +\path[draw,thick,->,>=latex] (1b) -- (b); +\path[draw,thick,->,>=latex] (2b) -- (b); +\path[draw,thick,->,>=latex] (3b) -- (b); +\path[draw,thick,->,>=latex] (4b) -- (b); +\path[draw,thick,->,>=latex] (5b) -- (b); +\path[draw,thick,->,>=latex] (6b) -- (b); +\path[draw,thick,->,>=latex] (7b) -- (b); + +\path[draw=red,thick,->,line width=2pt] (1a) -- (5b); +\path[draw=red,thick,->,line width=2pt] (5a) -- (6b); +\path[draw=red,thick,->,line width=2pt] (6a) -- (7b); +\path[draw=red,thick,->,line width=2pt] (3a) -- (4b); + +\end{tikzpicture} +\end{center} + +Tässä tapauksessa maksimiparitukseen kuuluu neljä kaarta, +jotka vastaavat alkuperäisen verkon kaaria +$1 \rightarrow 5$, $3 \rightarrow 4$, +$5 \rightarrow 6$ ja $6 \rightarrow 7$. +Niinpä pienin solmuerillinen polkupeite syntyy muodostamalla +polut kyseisten kaarten avulla. + +Pienimmän polkupeitteen koko on $n-c$, jossa $n$ on verkon +solmujen määrä ja $c$ on maksimiparituksen kaarten määrä. +Esimerkiksi yllä olevassa verkossa pienimmän +polkupeitteen koko on $7-4=3$. + +\subsubsection{Yleinen peite} + +Yleisessä polkupeitteessä sama solmu voi kuulua moneen polkuun, +minkä ansiosta tarvittava polkujen määrä saattaa olla pienempi. +Esimerkkiverkossa pienin yleinen polkupeite muodostuu +kahdesta polusta seuraavasti: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {1}; +\node[draw, circle] (2) at (2,0) {2}; +\node[draw, circle] (3) at (4,0) {3}; +\node[draw, circle] (4) at (6,0) {4}; +\node[draw, circle] (5) at (0,-2) {5}; +\node[draw, circle] (6) at (2,-2) {6}; +\node[draw, circle] (7) at (4,-2) {7}; + +\path[draw=blue,thick,->,line width=2pt] (1) -- (5); +\path[draw=blue,thick,->,line width=2pt] (5) -- (6); +\path[draw=blue,thick,->,line width=2pt] (6) -- (3); +\path[draw=blue,thick,->,line width=2pt] (3) -- (4); +\path[draw=green,thick,->,line width=2pt] (2) -- (6); +\path[draw=green,thick,->,line width=2pt] (6) -- (7); +\end{tikzpicture} +\end{center} + +Tässä verkossä yleisessä polkupeitteessä on 2 polkua, +kun taas solmuerillisessä polkupeitteessä on 3 polkua. +Erona on, että yleisessä polkupeitteessä solmua 6 +käytetään kahdessa polussa. + +Yleisen polkupeitteen voi löytää lähes samalla +tavalla kuin solmuerillisen polkupeitteen. +Riittää täydentää maksimiparituksen verkkoa niin, +että siinä on kaari $a \rightarrow b$ aina silloin, +kun alkuperäisessä verkossa solmusta $a$ pääsee +solmuun $b$ (mahdollisesti usean kaaren kautta). + +Nyt esimerkkiverkossa on seuraava tilanne: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1a) at (0,6) {1}; +\node[draw, circle] (2a) at (0,5) {2}; +\node[draw, circle] (3a) at (0,4) {3}; +\node[draw, circle] (4a) at (0,3) {4}; +\node[draw, circle] (5a) at (0,2) {5}; +\node[draw, circle] (6a) at (0,1) {6}; +\node[draw, circle] (7a) at (0,0) {7}; + +\node[draw, circle] (1b) at (4,6) {1}; +\node[draw, circle] (2b) at (4,5) {2}; +\node[draw, circle] (3b) at (4,4) {3}; +\node[draw, circle] (4b) at (4,3) {4}; +\node[draw, circle] (5b) at (4,2) {5}; +\node[draw, circle] (6b) at (4,1) {6}; +\node[draw, circle] (7b) at (4,0) {7}; + +\node[draw, circle] (a) at (-3,3) {\phantom{0}}; +\node[draw, circle] (b) at (7,3) {\phantom{0}}; + + +%\path[draw,thick,->,>=latex] (1a) -- (5b); +\path[draw,thick,->,>=latex] (1a) -- (6b); +\path[draw,thick,->,>=latex] (1a) -- (7b); +\path[draw,thick,->,>=latex] (1a) -- (3b); +\path[draw,thick,->,>=latex] (1a) -- (4b); +\path[draw,thick,->,>=latex] (5a) -- (6b); +\path[draw,thick,->,>=latex] (5a) -- (7b); +%\path[draw,thick,->,>=latex] (5a) -- (3b); +\path[draw,thick,->,>=latex] (5a) -- (4b); +\path[draw,thick,->,>=latex] (6a) -- (7b); +%\path[draw,thick,->,>=latex] (6a) -- (7b); +\path[draw,thick,->,>=latex] (6a) -- (3b); +%\path[draw,thick,->,>=latex] (3a) -- (4b); +%\path[draw,thick,->,>=latex] (2a) -- (6b); +\path[draw,thick,->,>=latex] (2a) -- (7b); +\path[draw,thick,->,>=latex] (2a) -- (3b); +\path[draw,thick,->,>=latex] (2a) -- (4b); + + +\path[draw,thick,->,>=latex] (a) -- (1a); +\path[draw,thick,->,>=latex] (a) -- (2a); +\path[draw,thick,->,>=latex] (a) -- (3a); +\path[draw,thick,->,>=latex] (a) -- (4a); +\path[draw,thick,->,>=latex] (a) -- (5a); +\path[draw,thick,->,>=latex] (a) -- (6a); +\path[draw,thick,->,>=latex] (a) -- (7a); + +\path[draw,thick,->,>=latex] (1b) -- (b); +\path[draw,thick,->,>=latex] (2b) -- (b); +\path[draw,thick,->,>=latex] (3b) -- (b); +\path[draw,thick,->,>=latex] (4b) -- (b); +\path[draw,thick,->,>=latex] (5b) -- (b); +\path[draw,thick,->,>=latex] (6b) -- (b); +\path[draw,thick,->,>=latex] (7b) -- (b); + +\path[draw=red,thick,->,line width=2pt] (1a) -- (5b); +\path[draw=red,thick,->,line width=2pt] (5a) -- (3b); +\path[draw=red,thick,->,line width=2pt] (3a) -- (4b); +\path[draw=red,thick,->,line width=2pt] (2a) -- (6b); +\path[draw=red,thick,->,line width=2pt] (6a) -- (7b); + + +% \path[draw=red,thick,->,line width=2pt] (1a) -- (6b); +% \path[draw=red,thick,->,line width=2pt] (1a) -- (7b); +% \path[draw=red,thick,->,line width=2pt] (1a) -- (3b); +% \path[draw=red,thick,->,line width=2pt] (1a) -- (4b); +% \path[draw=red,thick,->,line width=2pt] (5a) -- (6b); +% \path[draw=red,thick,->,line width=2pt] (5a) -- (7b); +% \path[draw=red,thick,->,line width=2pt] (5a) -- (3b); +% \path[draw=red,thick,->,line width=2pt] (5a) -- (4b); +% \path[draw=red,thick,->,line width=2pt] (6a) -- (7b); +% \path[draw=red,thick,->,line width=2pt] (6a) -- (7b); +% \path[draw=red,thick,->,line width=2pt] (6a) -- (3b); +% \path[draw=red,thick,->,line width=2pt] (3a) -- (4b); +% \path[draw=red,thick,->,line width=2pt] (2a) -- (6b); +% \path[draw=red,thick,->,line width=2pt] (2a) -- (7b); +% \path[draw=red,thick,->,line width=2pt] (2a) -- (3b); +% \path[draw=red,thick,->,line width=2pt] (2a) -- (4b); + +\end{tikzpicture} +\end{center} + + +\subsubsection{Dilworthin lause} + +\index{Dilworthin lause@Dilworthin lause} +\index{antiketju@antiketju} + +\key{Dilworthin lauseen} mukaan suunnatun, syklittömän +verkon pienin yleinen polkupeite +on yhtä suuri kuin suurin verkossa oleva \key{antiketju} +eli kokoelma solmuja, +jossa minkään kahden solmun välillä ei ole polkua. + +Esimerkiksi äskeisessä verkossa pienin +yleinen polkupeite sisältää kaksi polkua, +joten verkon suurimmassa antiketjussa on kaksi solmua. +Tällainen antiketju muodostuu esimerkiksi +valitsemalla solmut 3 ja 7: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {1}; +\node[draw, circle] (2) at (2,0) {2}; +\node[draw, circle, fill=lightgray] (3) at (4,0) {3}; +\node[draw, circle] (4) at (6,0) {4}; +\node[draw, circle] (5) at (0,-2) {5}; +\node[draw, circle] (6) at (2,-2) {6}; +\node[draw, circle, fill=lightgray] (7) at (4,-2) {7}; + +\path[draw,thick,->,>=latex] (1) -- (5); +\path[draw,thick,->,>=latex] (2) -- (6); +\path[draw,thick,->,>=latex] (3) -- (4); +\path[draw,thick,->,>=latex] (5) -- (6); +\path[draw,thick,->,>=latex] (6) -- (3); +\path[draw,thick,->,>=latex] (6) -- (7); +\end{tikzpicture} +\end{center} + +Verkossa ei ole polkua solmusta 3 solmuun 7 +eikä polkua solmusta 7 solmuun 3, +joten valinta on kelvollinen. +Toisaalta jos verkosta valitaan mitkä tahansa +kolme solmua, jostain solmusta toiseen on polku. + diff --git a/luku21.tex b/luku21.tex new file mode 100644 index 0000000..f4c973d --- /dev/null +++ b/luku21.tex @@ -0,0 +1,718 @@ +\chapter{Number theory} + +\index{lukuteoria@lukuteoria} + +\key{Lukuteoria} on kokonaislukuja tutkiva +matematiikan ala, jonka keskeinen +käsite on lukujen jaollisuus. +Lukuteoriassa on kiehtovaa, että monet kokonaislukuihin +liittyvät kysymykset ovat hyvin vaikeita ratkaista, +vaikka ne saattavat näyttää päältä päin yksinkertaisilta. + +Tarkastellaan esimerkkinä seuraavaa yhtälöä: +\[x^3 + y^3 + z^3 = 33\] +On helppoa löytää kolme reaalilukua $x$, $y$ ja $z$, +jotka toteuttavat yhtälön. Voimme valita +esimerkiksi +\[ +\begin{array}{lcl} +x = 3, \\ +y = \sqrt[3]{3}, \\ +z = \sqrt[3]{3}.\\ +\end{array} +\] +Sen sijaan kukaan ei tiedä, onko olemassa +kolmea \emph{kokonaislukua} $x$, $y$ ja $z$, +jotka toteuttaisivat yhtälön, vaan kyseessä +on avoin lukuteorian ongelma. + +Tässä luvussa tutustumme lukuteorian peruskäsitteisiin ja +-algoritmeihin. +Lähdemme liikkeelle lukujen jaollisuudesta, +johon liittyvät keskeiset algoritmit ovat +alkuluvun tarkastaminen sekä luvun jakaminen tekijöihin. + +\section{Alkuluvut ja tekijät} + +\index{jaollisuus@jaollisuus} +\index{jakaja@jakaja} +\index{tekijx@tekijä} + +Luku $a$ on luvun $b$ \key{jakaja} eli \key{tekijä}, +jos $b$ on jaollinen $a$:lla. +Jos $a$ on $b$:n jakaja, +niin merkitään $a \mid b$, +ja muuten merkitään $a \nmid b$. +Esimerkiksi luvun 24 jakajat ovat 1, 2, 3, 4, 6, 8, 12 ja 24. + +\index{alkuluku@alkuluku} +\index{alkutekijxhajotelma@alkutekijähajotelma} + +Luku $n$ on \key{alkuluku}, jos sen ainoat +positiiviset jakajat ovat 1 ja $n$. +Esimerkiksi luvut 7, 19 ja 41 ovat alkulukuja. +Luku 35 taas ei ole alkuluku, koska se voidaan +jakaa tekijöihin $5 \cdot 7 = 35$. +Jokaiselle luvulle $n>1$ on olemassa yksikäsitteinen +\key{alkutekijähajotelma} +\[ n = p_1^{\alpha_1} p_2^{\alpha_2} \cdots p_k^{\alpha_k},\] +missä $p_1,p_2,\ldots,p_k$ ovat alkulukuja +ja $\alpha_1,\alpha_2,\ldots,\alpha_k$ ovat positiivisia +lukuja. Esimerkiksi luvun 84 alkutekijähajotelma on +\[84 = 2^2 \cdot 3^1 \cdot 7^1.\] + +Luvun $n$ \key{jakajien määrä} on +\[\tau(n)=\prod_{i=1}^k (\alpha_i+1),\] +koska alkutekijän $p_i$ kohdalla on $\alpha_i+1$ +tapaa valita, montako kertaa alkutekijä +esiintyy jakajassa. +Esimerkiksi luvun 84 jakajien määrä +on $\tau(84)=3 \cdot 2 \cdot 2 = 12$. +Jakajat ovat +1, 2, 3, 4, 6, 7, 12, 14, 21, 28, 42 ja 84. + +Luvun $n$ \key{jakajien summa} on +\[\sigma(n)=\prod_{i=1}^k (1+p_i+\ldots+p_i^{\alpha_i}) = \prod_{i=1}^k \frac{p_i^{a_i+1}-1}{p_i-1},\] +missä jälkimmäinen muoto perustuu geometriseen summaan. +Esimerkiksi luvun 84 jakajien summa on +\[\sigma(84)=\frac{2^3-1}{2-1} \cdot \frac{3^2-1}{3-1} \cdot \frac{7^2-1}{7-1} = 7 \cdot 4 \cdot 8 = 224.\] + +Luvun $n$ \key{jakajien tulo} on +\[\mu(n)=n^{\tau(n)/2},\] +koska jakajista voidaan muodostaa +$\tau(n)/2$ paria, joiden jokaisen tulona on $n$. +Esimerkiksi luvun 84 jakajista muodostuvat parit +$1 \cdot 84$, $2 \cdot 42$, $3 \cdot 28$, jne., +ja jakajien tulo on $\mu(84)=84^6=351298031616$. + +%\index{tzydellinen luku@täydellinen luku} +\index{txydellinen luku@täydellinen luku} + +Luku $n$ on \key{täydellinen}, jos $n=\sigma(n)-n$ +eli luku on yhtä suuri kuin summa sen jakajista +välillä $1 \ldots n-1$. +Esimerkiksi luku 28 on täydellinen, koska +se muodostuu summana $1+2+4+7+14$. + +\subsubsection{Alkulukujen määrä} + +On helppoa osoittaa, että alkulukuja on äärettömästi. +Jos nimittäin alkulukuja olisi äärellinen määrä, +voisimme muodostaa joukon $P=\{p_1,p_2,\ldots,p_n\}$, +joka sisältää kaikki alkuluvut. +Esimerkiksi $p_1=2$, $p_2=3$, $p_3=5$, jne. +Nyt kuitenkin voisimme muodostaa uuden alkuluvun +\[p_1 p_2 \cdots p_n+1,\] +joka on kaikkia $P$:n lukuja suurempi. +Koska tätä lukua ei ole joukossa $P$, +syntyy ristiriita ja alkulukujen määrän on +pakko olla ääretön. + +\subsubsection{Alkulukujen tiheys} + +Alkulukujen tiheys tarkoittaa, kuinka usein alkulukuja +esiintyy muiden lukujen joukossa. +Merkitään funktiolla $\pi(n)$, +montako alkulukua on välillä $1 \ldots n$. +Esimerkiksi $\pi(10)=4$, koska välillä $1 \ldots 10$ +on alkuluvut 2, 3, 5 ja 7. + +On mahdollista osoittaa, että +\[\pi(n) \approx \frac{n}{\ln n},\] +mikä tarkoittaa, että alkulukuja esiintyy +varsin usein. Esimerkiksi alkulukujen määrä +välillä $1 \ldots 10^6$ on $\pi(10^6)=78498$ +ja $10^6 / \ln 10^6 \approx 72382$. + +\subsubsection{Konjektuureja} + +Alkulukuihin liittyy useita \emph{konjektuureja} +eli lauseita, joiden uskotaan olevan tosia mutta +joita kukaan ei ole onnistunut todistamaan tähän mennessä. +Kuuluisia konjektuureja ovat seuraavat: + +\begin{itemize} +\index{Goldbachin konjektuuri@Goldbachin konjektuuri} +\item \key{Goldbachin konjektuuri}: +Jokainen parillinen kokonaisluku $n>2$ voidaan esittää +muodossa $n=a+b$ niin, että $a$ ja $b$ +ovat alkulukuja. +\index{alkulukupari@alkulukupari} +\item \key{Alkulukuparit}: +On olemassa äärettömästi pareja muotoa $\{p,p+2\}$, +joissa sekä $p$ että $p+2$ on alkuluku. +\index{Legendren konjektuuri@Legendren konjektuuri} +\item \key{Legendren konjektuuri}: +Lukujen $n^2$ ja $(n+1)^2$ välillä on aina alkuluku, +kun $n$ on mikä tahansa positiivinen kokonaisluku. +\end{itemize} + +\subsubsection{Perusalgoritmit} + +Jos luku $n$ ei ole alkuluku, +niin sen voi esittää muodossa $a \cdot b$, +missä $a \le \sqrt n$ tai $b \le \sqrt n$, +minkä ansiosta sillä on varmasti +tekijä välillä $2 \ldots \sqrt n$. +Tämän havainnon avulla voi tarkastaa ajassa $O(\sqrt n)$, +onko luku alkuluku, +sekä myös selvittää ajassa $O(\sqrt n)$ +luvun alkutekijät. + +Seuraava funktio \texttt{alkuluku} tutkii, +onko annettu luku $n$ alkuluku. +Funktio koettaa jakaa lukua kaikilla luvuilla +välillä $2 \ldots \sqrt n$, ja jos mikään +luvuista ei jaa $n$:ää, niin $n$ on alkuluku. + +\begin{lstlisting} +bool alkuluku(int n) { + if (n < 2) return false; + for (int x = 2; x*x <= n; x++) { + if (n%x == 0) return false; + } + return true; +} +\end{lstlisting} + +\noindent +Seuraava funktio \texttt{tekijat} muodostaa +vektorin, joka sisältää luvun $n$ +alkutekijät. +Funktio jakaa $n$:ää sen alkutekijöillä ja lisää +niitä samaan aikaan vektoriin. +Prosessi päättyy, kun jäljellä on luku $n$, +jolla ei ole tekijää välillä $2 \ldots \sqrt n$. +Jos $n>1$, se on alkuluku ja viimeinen tekijä. + +\begin{lstlisting} +vector tekijat(int n) { + vector f; + for (int x = 2; x*x <= n; x++) { + while (n%x == 0) { + f.push_back(x); + n /= x; + } + } + if (n > 1) f.push_back(n); + return f; +} +\end{lstlisting} + +Huomaa, että funktio lisää jokaisen +alkutekijän vektoriin +niin monta kertaa, kuin kyseinen +alkutekijä jakaa luvun. +Esimerkiksi $24=2^3 \cdot 3$, +joten funktio muodostaa vektorin $[2,2,2,3]$. + +\subsubsection{Eratostheneen seula} + +\index{Eratostheneen seula@Eratostheneen seula} + +\key{Eratostheneen seula} on esilaskenta-algoritmi, +jonka suorituksen jälkeen mistä tahansa +välin $2 \ldots n$ luvusta pystyy tarkastamaan +nopeasti, onko se alkuluku, +sekä etsimään yhden luvun alkutekijän, +jos luku ei ole alkuluku. + +Algoritmi luo taulukon $\texttt{a}$, +jossa on käytössä indeksit $2,3,\ldots,n$. +Taulukossa $\texttt{a}[k]=0$ tarkoittaa, +että $k$ on alkuluku, +ja $\texttt{a}[k] \neq 0$ tarkoittaa, +että $k$ ei ole alkuluku. +Jälkimmäisessä tapauksessa $\texttt{a}[k]$ +on yksi $k$:n alkutekijöistä. + +Algoritmi käy läpi välin +$2 \ldots n$ luvut yksi kerrallaan. +Aina kun vastaan tulee uusi alkuluku $x$, +niin algoritmi merkitsee taulukkoon, että $x$:n moninkerrat +$2x,3x,4x,\ldots$ eivät ole alkulukuja, +koska niillä on alkutekijä $x$. + +Esimerkiksi jos $n=20$, +taulukosta tulee: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (19,1); + +\node at (0.5,0.5) {$0$}; +\node at (1.5,0.5) {$0$}; +\node at (2.5,0.5) {$2$}; +\node at (3.5,0.5) {$0$}; +\node at (4.5,0.5) {$3$}; +\node at (5.5,0.5) {$0$}; +\node at (6.5,0.5) {$2$}; +\node at (7.5,0.5) {$3$}; +\node at (8.5,0.5) {$5$}; +\node at (9.5,0.5) {$0$}; +\node at (10.5,0.5) {$3$}; +\node at (11.5,0.5) {$0$}; +\node at (12.5,0.5) {$7$}; +\node at (13.5,0.5) {$5$}; +\node at (14.5,0.5) {$2$}; +\node at (15.5,0.5) {$0$}; +\node at (16.5,0.5) {$3$}; +\node at (17.5,0.5) {$0$}; +\node at (18.5,0.5) {$5$}; + +\footnotesize + +\node at (0.5,1.5) {$2$}; +\node at (1.5,1.5) {$3$}; +\node at (2.5,1.5) {$4$}; +\node at (3.5,1.5) {$5$}; +\node at (4.5,1.5) {$6$}; +\node at (5.5,1.5) {$7$}; +\node at (6.5,1.5) {$8$}; +\node at (7.5,1.5) {$9$}; +\node at (8.5,1.5) {$10$}; +\node at (9.5,1.5) {$11$}; +\node at (10.5,1.5) {$12$}; +\node at (11.5,1.5) {$13$}; +\node at (12.5,1.5) {$14$}; +\node at (13.5,1.5) {$15$}; +\node at (14.5,1.5) {$16$}; +\node at (15.5,1.5) {$17$}; +\node at (16.5,1.5) {$18$}; +\node at (17.5,1.5) {$19$}; +\node at (18.5,1.5) {$20$}; + +\end{tikzpicture} +\end{center} + +Seuraava koodi toteuttaa +Eratostheneen seulan. +Koodi olettaa, että jokainen taulukon \texttt{a} +alkio on aluksi 0. + +\begin{lstlisting} +for (int x = 2; x <= n; x++) { + if (a[x]) continue; + for (int u = 2*x; u <= n; u += x) { + a[u] = x; + } +} +\end{lstlisting} + +Algoritmin sisäsilmukka suoritetaan +$n/x$ kertaa tietyllä $x$:n arvolla, +joten yläraja algoritmin ajankäytölle +on harmoninen summa + +\index{harmoninen summa@harmoninen summa} + +\[\sum_{x=2}^n n/x = n/2 + n/3 + n/4 + \cdots + n/n = O(n \log n).\] + +Todellisuudessa algoritmi on vielä nopeampi, +koska sisäsilmukka suoritetaan vain, +jos luku $x$ on alkuluku. +Voidaan osoittaa, että algoritmin aikavaativuus +on vain $O(n \log \log n)$ eli hyvin lähellä vaativuutta $O(n)$. + +\subsubsection{Eukleideen algoritmi} + +\index{suurin yhteinen tekijx@suurin yhteinen tekijä} +\index{pienin yhteinen moninkerta@pienin yhteinen moninkerta} +\index{Eukleideen algoritmi@Eukleideen algoritmi} + +Lukujen $a$ ja $b$ \key{suurin yhteinen tekijä} eli $\textrm{syt}(a,b)$ +on suurin luku, jolla sekä $a$ että $b$ on jaollinen. +Lukujen $a$ ja $b$ \key{pienin yhteinen moninkerta} eli $\textrm{pym}(a,b)$ +on puolestaan pienin luku, joka on jaollinen sekä $a$:lla että $b$:llä. +Esimerkiksi $\textrm{syt}(24,36)=12$ ja +$\textrm{pym}(24,36)=72$. + +Suurimman yhteisen tekijän ja pienimmän yhteisen +moninkerran välillä on yhteys +\[\textrm{pym}(a,b)=\frac{ab}{\textrm{syt}(a,b)}.\] + +\key{Eukleideen algoritmi} on tehokas tapa etsiä +suurin yhteinen tekijä. +Se laskee suurimman yhteisen tekijän kaavalla +\begin{equation*} + \textrm{syt}(a,b) = \begin{cases} + a & b = 0\\ + \textrm{syt}(b,a \bmod b) & b \neq 0\\ + \end{cases} +\end{equation*} +Esimerkiksi +\[\textrm{syt}(24,36) = \textrm{syt}(36,24) += \textrm{syt}(24,12) = \textrm{syt}(12,0)=12.\] +Eukleideen algoritmin aikavaativuus +on $O(\log n)$, kun $n=\min(a,b)$. +Pahin tapaus algoritmille on, jos luvut ovat +peräkkäiset Fibonaccin luvut. +Silloin algoritmi käy läpi kaikki pienemmät +peräkkäiset Fibonaccin luvut. +Esimerkiksi +\[\textrm{syt}(13,8)=\textrm{syt}(8,5) +=\textrm{syt}(5,3)=\textrm{syt}(3,2)=\textrm{syt}(2,1)=\textrm{syt}(1,0)=1.\] + +\subsubsection{Eulerin totienttifunktio} + +\index{suhteellinen alkuluku@suhteellinen alkuluku} +\index{Eulerin totienttifunktio@Eulerin totienttifunktio} + +Luvut $a$ ja $b$ ovat suhteelliset alkuluvut, +jos $\textrm{syt}(a,b)=1$. +\key{Eulerin totienttifunktio} $\varphi(n)$ +laskee luvun $n$ suhteellisten alkulukujen +määrän välillä $1 \ldots n$. +Esimerkiksi $\varphi(12)=4$, +koska luvut 1, 5, 7 ja 11 ovat suhteellisia +alkulukuja luvun 12:n kanssa. + +Totienttifunktion arvon $\varphi(n)$ pystyy laskemaan +luvun $n$ alkutekijähajotelmasta kaavalla +\[ \varphi(n) = \prod_{i=1}^k p_i^{\alpha_i-1}(p_i-1). \] +Esimerkiksi $\varphi(12)=2^1 \cdot (2-1) \cdot 3^0 \cdot (3-1)=4$. +Huomaa myös, että $\varphi(n)=n-1$, +jos $n$ on alkuluku. + +\section{Modulolaskenta} + +\index{modulolaskenta@modulolaskenta} + +\key{Modulolaskennassa} lukualuetta rajoitetaan +niin, että käytössä ovat vain +kokonaisluvut $0,1,2,\ldots,m-1$, +missä $m$ on vakio. +Ideana on, että lukua $x$ vastaa luku $x \bmod m$ +eli luvun $x$ jakojäännös luvulla $m$. +Esimerkiksi jos $m=17$, niin lukua $75$ vastaa luku +$75 \bmod 17 = 7$. + +Useissa laskutoimituksissa jakojäännöksen voi laskea +ennen laskutoimitusta, minkä ansiosta saadaan seuraavat kaavat: +\[ +\begin{array}{rcl} +(x+y) \bmod m & = & (x \bmod m + y \bmod m) \bmod m \\ +(x-y) \bmod m & = & (x \bmod m - y \bmod m) \bmod m \\ +(x \cdot y) \bmod m & = & (x \bmod m \cdot y \bmod m) \bmod m \\ +(x^k) \bmod m & = & (x \bmod m)^k \bmod m \\ +\end{array} +\] + +\subsubsection{Tehokas potenssilasku} + +Modulolaskennassa tulee usein tarvetta laskea +tehokkaasti potenssilasku $x^n$. +Tämä onnistuu ajassa $O(\log n)$ +seuraavan rekursion avulla: +\begin{equation*} + x^n = \begin{cases} + 1 & n = 0\\ + x^{n/2} \cdot x^{n/2} & \text{$n$ on parillinen}\\ + x^{n-1} \cdot x & \text{$n$ on pariton} + \end{cases} +\end{equation*} + +Oleellista on, että parillisen $n$:n +tapauksessa luku $x^{n/2}$ lasketaan vain kerran. +Tämän ansiosta potenssilaskun aikavaativuus on $O(\log n)$, +koska $n$:n koko puolittuu aina silloin, +kun $n$ on parillinen. + +Seuraava funktio laskee luvun $x^n \bmod m$: + +\begin{lstlisting} +int pot(int x, int n, int m) { + if (n == 0) return 1%m; + int u = pot(x,n/2,m); + u = (u*u)%m; + if (n%2 == 1) u = (u*x)%m; + return u; +} +\end{lstlisting} + +\subsubsection{Fermat'n pieni lause ja Eulerin lause} + +\index{Fermat'n pieni lause} +\index{Eulerin lause@Eulerin lause} + +\key{Fermat'n pienen lauseen} mukaan +\[x^{m-1} \bmod m = 1,\] +kun $m$ on alkuluku ja $x$ ja $m$ ovat suhteelliset alkuluvut. +Tällöin myös +\[x^k \bmod m = x^{k \bmod (m-1)} \bmod m.\] +Yleisemmin \key{Eulerin lauseen} mukaan +\[x^{\varphi(m)} \bmod m = 1,\] +kun $x$ ja $m$ ovat suhteelliset alkuluvut. +Fermat'n pieni lause seuraa Eulerin lauseesta, +koska jos $m$ on alkuluku, niin $\varphi(m)=m-1$. + +\subsubsection{Modulon käänteisluku} + +\index{modulon kxxnteisluku@modulon käänteisluku} + +Luvun $x$ käänteisluku modulo $m$ +tarkoittaa sellaista lukua $x^{-1}$, +että +\[ x x^{-1} \bmod m = 1. \] +Esimerkiksi jos $x=6$ ja $m=17$, +niin $x^{-1}=3$, koska $6\cdot3 \bmod 17=1$. + +Modulon käänteisluku mahdollistaa +jakolaskun laskemisen modulossa, +koska jakolasku luvulla $x$ vastaa +kertolaskua luvulla $x^{-1}$. +Esimerkiksi jos haluamme laskea +jakolaskun $36/6 \bmod 17$, +voimme muuttaa sen muotoon $2 \cdot 3 \bmod 17$, +koska $36 \bmod 17 = 2$ ja $6^{-1} \bmod 17 = 3$. + +Modulon käänteislukua ei +kuitenkaan ole aina olemassa. +Esimerkiksi jos $x=2$ ja $m=4$, +yhtälölle +\[ x x^{-1} \bmod m = 1. \] +ei ole ratkaisua, koska kaikki luvun 2 +moninkerrat ovat parillisia eikä jakojäännös +4:llä voi koskaan olla 1. +Osoittautuu, että $x^{-1} \bmod m$ +on olemassa tarkalleen silloin, +kun $x$ ja $m$ ovat suhteelliset alkuluvut. + +Jos modulon käänteisluku on olemassa, +sen saa laskettua kaavalla +\[ +x^{-1} = x^{\varphi(m)-1}. +\] +Erityisesti jos $m$ on alkuluku, kaavasta tulee +\[ +x^{-1} = x^{m-2}. +\] +Esimerkiksi jos $x=6$ ja $m=17$, niin +\[x^{-1}=6^{17-2} \bmod 17 = 3.\] +Tämän kaavan ansiosta modulon käänteisluvun pystyy +laskemaan nopeasti tehokkaan potenssilaskun avulla. + +Modulon käänteisluvun kaavan voi perustella Eulerin lauseen avulla. +Ensinnäkin käänteisluvulle täytyy päteä +\[ +x x^{-1} \bmod m = 1. +\] +Toisaalta Eulerin lauseen mukaan +\[ +x^{\varphi(m)} \bmod m = xx^{\varphi(m)-1} \bmod m = 1, +\] +joten lukujen $x^{-1}$ ja $x^{\varphi(m)-1}$ on oltava samat. + +\subsubsection{Modulot tietokoneessa} + +Tietokone käsittelee etumerkittömiä kokonaislukuja +modulo $2^k$, missä $k$ on luvun bittien määrä. +Usein näkyvä seuraus tästä on luvun arvon pyörähtäminen +ympäri, jos luku kasvaa liian suureksi. + +Esimerkiksi C++:ssa \texttt{unsigned int} -tyyppinen +arvo lasketaan modulo $2^{32}$. +Seuraava koodi määrittelee muuttujan +tyyppiä \texttt{unsigned int}, +joka saa arvon $123456789$. +Sitten muuttujan arvo kerrotaan itsellään, +jolloin tuloksena on luku +$123456789^2 \bmod 2^{32} = 2537071545$. + +\begin{lstlisting} +unsigned int x = 123456789; +cout << x*x << "\n"; // 2537071545 +\end{lstlisting} + +\section{Yhtälönratkaisu} + +\index{Diofantoksen yhtxlz@Diofantoksen yhtälö} + +\key{Diofantoksen yhtälö} on muotoa +\[ ax + by = c, \] +missä $a$, $b$ ja $c$ ovat vakioita +ja tehtävänä on ratkaista muuttujat $x$ ja $y$. +Jokaisen yhtälössä esiintyvän luvun tulee +olla kokonaisluku. +Esimerkiksi jos yhtälö on $5x+2y=11$, yksi ratkaisu +on valita $x=3$ ja $y=-2$. + +\index{Eukleideen algoritmi@Eukleideen algoritmi} + +Diofantoksen yhtälön voi ratkaista +tehokkaasti Eukleideen algoritmin avulla, +koska Eukleideen algoritmia laajentamalla +pystyy löytämään luvun $\textrm{syt}(a,b)$ +lisäksi luvut $x$ ja $y$, +jotka toteuttavat yhtälön +\[ +ax + by = \textrm{syt}(a,b). +\] + +Diofantoksen yhtälön ratkaisu on olemassa, jos $c$ on +jaollinen $\textrm{syt}(a,b)$:llä, +ja muussa tapauksessa yhtälöllä ei ole ratkaisua. + +\index{laajennettu Eukleideen algoritmi@laajennettu Eukleideen algoritmi} + +\subsubsection*{Laajennettu Eukleideen algoritmi} + +Etsitään esimerkkinä luvut $x$ ja $y$, +jotka toteuttavat yhtälön +\[ +39x + 15y = 12. +\] +Yhtälöllä on ratkaisu, koska $\textrm{syt}(39,15)=3$ +ja $3 \mid 12$. +Kun Eukleideen algoritmi laskee lukujen +39 ja 15 suurimman +yhteisen tekijän, syntyy ketju +\[ +\textrm{syt}(39,15) = \textrm{syt}(15,9) += \textrm{syt}(9,6) = \textrm{syt}(6,3) += \textrm{syt}(3,0) = 3. \] +Algoritmin aikana muodostuvat jakoyhtälöt ovat: +\[ +\begin{array}{lcl} +39 - 2 \cdot 15 & = & 9 \\ +15 - 1 \cdot 9 & = & 6 \\ +9 - 1 \cdot 6 & = & 3 \\ +\end{array} +\] +Näiden yhtälöiden avulla saadaan +\[ +39 \cdot 2 + 15 \cdot (-5) = 3 +\] +ja kertomalla yhtälö 4:lla tuloksena on +\[ +39 \cdot 8 + 15 \cdot (-20) = 12, +\] +joten alkuperäisen yhtälön ratkaisu on $x=8$ ja $y=-20$. + +Diofantoksen yhtälön ratkaisu ei ole yksikäsitteinen, +vaan yhdestä ratkaisusta on mahdollista muodostaa +äärettömästi muita ratkaisuja. +Kun yhtälön ratkaisu on $(x,y)$, +niin myös +\[(x+\frac{kb}{\textrm{syt}(a,b)},y-\frac{ka}{\textrm{syt}(a,b)})\] +on ratkaisu, missä $k$ on mikä tahansa kokonaisluku. + +\subsubsection{Kiinalainen jäännöslause} + +\index{kiinalainen jxxnnzslause@kiinalainen jäännöslause} + +\key{Kiinalainen jäännöslause} ratkaisee yhtälöryhmän muotoa +\[ +\begin{array}{lcl} +x & = & a_1 \bmod m_1 \\ +x & = & a_2 \bmod m_2 \\ +\cdots \\ +x & = & a_n \bmod m_n \\ +\end{array} +\] +missä kaikki parit luvuista $m_1,m_2,\ldots,m_n$ +ovat suhteellisia alkulukuja. + +Olkoon $x^{-1}_m$ luvun $x$ käänteisluku +modulo $m$ ja +\[ X_k = \frac{m_1 m_2 \cdots m_n}{m_k}.\] +Näitä merkintöjä käyttäen yhtälöryhmän ratkaisu on +\[x = a_1 X_1 {X_1}^{-1}_{m_1} + a_2 X_2 {X_2}^{-1}_{m_2} + \cdots + a_n X_n {X_n}^{-1}_{m_n}.\] +Tässä ratkaisussa jokaiselle luvulle $k=1,2,\ldots,n$ +pätee, että +\[a_k X_k {X_k}^{-1}_{m_k} \bmod m_k = a_k,\] +sillä +\[X_k {X_k}^{-1}_{m_k} \bmod m_k = 1.\] +Koska kaikki muut summan osat ovat jaollisia luvulla +$m_k$, ne eivät vaikuta jakojäännökseen ja +koko summan jakojäännös $m_k$:lla on $a_k$. + +Esimerkiksi yhtälöryhmän +\[ +\begin{array}{lcl} +x & = & 3 \bmod 5 \\ +x & = & 4 \bmod 7 \\ +x & = & 2 \bmod 3 \\ +\end{array} +\] +ratkaisu on +\[ 3 \cdot 21 \cdot 1 + 4 \cdot 15 \cdot 1 + 2 \cdot 35 \cdot 2 = 263.\] + +Kun yksi ratkaisu $x$ on löytynyt, +sen avulla voi muodostaa äärettömästi +erilaisia ratkaisuja, koska kaikki luvut muotoa +\[x+m_1 m_2 \cdots m_n\] +ovat ratkaisuja. + + +\section{Muita tuloksia} + +\subsubsection{Lagrangen lause} + +\index{Lagrangen lause@Lagrangen lause} + +\key{Lagrangen lauseen} mukaan jokainen positiivinen kokonaisluku voidaan +esittää neljän neliöluvun summana eli muodossa $a^2+b^2+c^2+d^2$. +Esimerkiksi luku 123 voidaan esittää muodossa $8^2+5^2+5^2+3^2$. + +\subsubsection{Zeckendorfin lause} + +\index{Zeckendorfin lause@Zeckendorfin lause} +\index{Fibonaccin luku@Fibonaccin luku} + +\key{Zeckendorfin lauseen} mukaan +jokaiselle positiiviselle kokonaisluvulle +on olemassa yksikäsitteinen esitys +Fibonaccin lukujen summana niin, että +mitkään kaksi lukua eivät ole samat eivätkä peräkkäiset +Fibonaccin luvut. +Esimerkiksi luku 74 voidaan esittää muodossa +$55+13+5+1$. + +\subsubsection{Pythagoraan kolmikot} + +\index{Pythagoraan kolmikko@Pythagoraan kolmikko} +\index{Eukleideen kaava@Eukleideen kaava} + +\key{Pythagoraan kolmikko} on lukukolmikko $(a,b,c)$, +joka toteuttaa Pythagoraan lauseen $a^2+b^2=c^2$ +eli $a$, $b$ ja $c$ voivat olla suorakulmaisen +kolmion sivujen pituudet. +Esimerkiksi $(3,4,5)$ on Pythagoraan kolmikko. + +Jos $(a,b,c)$ on Pythagoraan kolmikko, +niin myös kaikki kolmikot muotoa $(ka,kb,kc)$ +ovat Pythagoraan kolmikoita, +missä $k>1$. +Pythagoraan kolmikko on \key{primitiivinen}, +jos $a$, $b$ ja $c$ ovat suhteellisia alkulukuja, +ja primitiivisistä kolmikoista voi muodostaa +kaikki muut kolmikot kertoimen $k$ avulla. + +\key{Eukleideen kaavan} mukaan jokainen primitiivinen +Pythagoraan kolmikko on muotoa +\[(n^2-m^2,2nm,n^2+m^2),\] +missä $0 1\\ + \end{cases} +\end{equation*} +Pohjatapauksena on $f(1)=1$, +koska luvun 1 voi esittää vain yhdellä tavalla. +Rekursiivinen tapaus käy läpi +kaikki vaihtoehdot, +mikä on summan viimeinen luku. +Esimerkiksi tapauksessa $n=4$ summa voi päättyä +$+1$, $+2$ tai $+3$. +Tämän lisäksi lasketaan mukaan esitystapa, +jossa on pelkkä luku $n$. + +Funktion ensimmäiset arvot ovat: +\[ +\begin{array}{lcl} +f(1) & = & 1 \\ +f(2) & = & 2 \\ +f(3) & = & 4 \\ +f(4) & = & 8 \\ +f(5) & = & 16 \\ +\end{array} +\] +Osoittautuu, että funktiolle on myös suljettu muoto +\[ +f(n)=2^{n-1}, +\] +mikä johtuu siitä, että summassa on $n-1$ mahdollista +kohtaa +-merkille ja niistä valitaan mikä tahansa osajoukko. + +\section{Binomikerroin} + +\index{binomikerroin@binomikerroin} + +\key{Binomikerroin} ${n \choose k}$ ilmaisee, +monellako tavalla $n$ alkion joukosta +voidaan muodostaa $k$ alkion osajoukko. +Esimerkiksi ${5 \choose 3}=10$, +koska joukosta $\{1,2,3,4,5\}$ +voidaan valita $10$ tavalla $3$ alkiota: +\[ \{1,2,3\}, \{1,2,4\}, \{1,2,5\}, \{1,3,4\}, \{1,3,5\}, +\{1,4,5\}, \{2,3,4\}, \{2,3,5\}, \{2,4,5\}, \{3,4,5\} \] + +\subsubsection{Laskutapa 1} + +Binomikertoimen voi laskea rekursiivisesti seuraavasti: + +\[ +{n \choose k} = {n-1 \choose k-1} + {n-1 \choose k} +\] + +Ideana rekursiossa on tarkastella tiettyä +joukon alkiota $x$. +Jos alkio $x$ valitaan osajoukkoon, +täytyy vielä valita $n-1$ alkiosta $k-1$ alkiota. +Jos taas alkiota $x$ ei valita osajoukkoon, +täytyy vielä valita $n-1$ alkiosta $k$ alkiota. + +Rekursion pohjatapaukset ovat seuraavat: + +\[ +{n \choose 0} = {n \choose n} = 1 +\] + +Tämä johtuu siitä, että on aina yksi tapa +muodostaa tyhjä osajoukko, +samoin kuin valita kaikki alkiot osajoukkoon. + +\subsubsection{Laskutapa 2} + +Toinen tapa laskea binomikerroin on seuraava: +\[ +{n \choose k} = \frac{n!}{k!(n-k)!}. +\] + +Kaavassa $n!$ on $n$ alkion permutaatioiden määrä. +Ideana on käydä läpi kaikki permutaatiot +ja valita kussakin tapauksessa +permutaation $k$ ensimmäistä alkiota osajoukkoon. +Koska ei ole merkitystä, +missä järjestyksessä osajoukon alkiot +ja ulkopuoliset alkiot ovat, +tulos jaetaan luvuilla $k!$ ja $(n-k)!$. + +\subsubsection{Ominaisuuksia} + +Binomikertoimelle pätee +\[ +{n \choose k} = {n \choose n-k}, +\] +koska $k$ alkion valinta osajoukkoon +tarkoittaa samaa kuin että valitaan +$n-k$ alkiota osajoukon ulkopuolelle. + +Binomikerrointen summa on +\[ +{n \choose 0}+{n \choose 1}+{n \choose 2}+\ldots+{n \choose n}=2^n. +\] + +Nimi ''binomikerroin'' tulee siitä, että + +\[ (a+b)^n = +{n \choose 0} a^n b^0 + +{n \choose 1} a^{n-1} b^1 + +\ldots + +{n \choose n-1} a^1 b^{n-1} + +{n \choose n} a^0 b^n. \] + +\index{Pascalin kolmio} + +Binomikertoimet esiintyvät myös \key{Pascalin +kolmiossa}, jonka reunoilla on lukua 1 +ja jokainen luku saadaan +kahden yllä olevan luvun summana: +\begin{center} +\begin{tikzpicture}{0.9} +\node at (0,0) {1}; +\node at (-0.5,-0.5) {1}; +\node at (0.5,-0.5) {1}; +\node at (-1,-1) {1}; +\node at (0,-1) {2}; +\node at (1,-1) {1}; +\node at (-1.5,-1.5) {1}; +\node at (-0.5,-1.5) {3}; +\node at (0.5,-1.5) {3}; +\node at (1.5,-1.5) {1}; +\node at (-2,-2) {1}; +\node at (-1,-2) {4}; +\node at (0,-2) {6}; +\node at (1,-2) {4}; +\node at (2,-2) {1}; +\node at (-2,-2.5) {$\ldots$}; +\node at (-1,-2.5) {$\ldots$}; +\node at (0,-2.5) {$\ldots$}; +\node at (1,-2.5) {$\ldots$}; +\node at (2,-2.5) {$\ldots$}; +\end{tikzpicture} +\end{center} + +\subsubsection{Laatikot ja pallot} + +''Laatikot ja pallot'' on usein hyödyllinen malli, +jossa $n$ laatikkoon sijoitetaan $k$ palloa. +Tarkastellaan seuraavaksi kolmea tapausta: + +\textit{Tapaus 1}: Kuhunkin laatikkoon saa sijoittaa +enintään yhden pallon. +Esimerkiksi kun $n=5$ ja $k=2$, +sijoitustapoja on 10: + +\begin{center} +\begin{tikzpicture}[scale=0.5] +\newcommand\lax[3]{ +\path[draw,thick,-] (#1-0.5,#2+0.5) -- (#1-0.5,#2-0.5) -- + (#1+0.5,#2-0.5) -- (#1+0.5,#2+0.5); +\ifthenelse{\equal{#3}{1}}{\draw[fill=black] (#1,#2-0.3) circle (0.15);}{} +\ifthenelse{\equal{#3}{2}}{\draw[fill=black] (#1-0.2,#2-0.3) circle (0.15);}{} +\ifthenelse{\equal{#3}{2}}{\draw[fill=black] (#1+0.2,#2-0.3) circle (0.15);}{} +} +\newcommand\laa[7]{ + \lax{#1}{#2}{#3} + \lax{#1+1.2}{#2}{#4} + \lax{#1+2.4}{#2}{#5} + \lax{#1+3.6}{#2}{#6} + \lax{#1+4.8}{#2}{#7} +} + +\laa{0}{0}{1}{1}{0}{0}{0} +\laa{0}{-2}{1}{0}{1}{0}{0} +\laa{0}{-4}{1}{0}{0}{1}{0} +\laa{0}{-6}{1}{0}{0}{0}{1} +\laa{8}{0}{0}{1}{1}{0}{0} +\laa{8}{-2}{0}{1}{0}{1}{0} +\laa{8}{-4}{0}{1}{0}{0}{1} +\laa{16}{0}{0}{0}{1}{1}{0} +\laa{16}{-2}{0}{0}{1}{0}{1} +\laa{16}{-4}{0}{0}{0}{1}{1} + +\end{tikzpicture} +\end{center} + +Tässä tapauksessa vastauksen antaa suoraan binomikerroin ${n \choose k}$. + +\textit{Tapaus 2}: Samaan laatikkoon saa sijoittaa +monta palloa. +Esimerkiksi kun $n=5$ ja $k=2$, sijoitustapoja on 15: + +\begin{center} +\begin{tikzpicture}[scale=0.5] +\newcommand\lax[3]{ +\path[draw,thick,-] (#1-0.5,#2+0.5) -- (#1-0.5,#2-0.5) -- + (#1+0.5,#2-0.5) -- (#1+0.5,#2+0.5); +\ifthenelse{\equal{#3}{1}}{\draw[fill=black] (#1,#2-0.3) circle (0.15);}{} +\ifthenelse{\equal{#3}{2}}{\draw[fill=black] (#1-0.2,#2-0.3) circle (0.15);}{} +\ifthenelse{\equal{#3}{2}}{\draw[fill=black] (#1+0.2,#2-0.3) circle (0.15);}{} +} +\newcommand\laa[7]{ + \lax{#1}{#2}{#3} + \lax{#1+1.2}{#2}{#4} + \lax{#1+2.4}{#2}{#5} + \lax{#1+3.6}{#2}{#6} + \lax{#1+4.8}{#2}{#7} +} + +\laa{0}{0}{2}{0}{0}{0}{0} +\laa{0}{-2}{1}{1}{0}{0}{0} +\laa{0}{-4}{1}{0}{1}{0}{0} +\laa{0}{-6}{1}{0}{0}{1}{0} +\laa{0}{-8}{1}{0}{0}{0}{1} +\laa{8}{0}{0}{2}{0}{0}{0} +\laa{8}{-2}{0}{1}{1}{0}{0} +\laa{8}{-4}{0}{1}{0}{1}{0} +\laa{8}{-6}{0}{1}{0}{0}{1} +\laa{8}{-8}{0}{0}{2}{0}{0} +\laa{16}{0}{0}{0}{1}{1}{0} +\laa{16}{-2}{0}{0}{1}{0}{1} +\laa{16}{-4}{0}{0}{0}{2}{0} +\laa{16}{-6}{0}{0}{0}{1}{1} +\laa{16}{-8}{0}{0}{0}{0}{2} + +\end{tikzpicture} +\end{center} + +Prosessin voi kuvata merkkijonona, joka muodostuu +merkeistä ''o'' ja ''$\rightarrow$''. +Pallojen sijoittaminen alkaa +vasemmanpuoleisimmasta laatikosta. +Merkki ''o'' tarkoittaa, että pallo sijoitetaan +nykyiseen laatikkoon, ja merkki +''$\rightarrow$'' tarkoittaa, että siirrytään +seuraavaan laatikkoon. + +Nyt jokainen sijoitustapa on merkkijono, jossa +on $k$ kertaa merkki ''o'' ja $n-1$ kertaa +merkki ''$\rightarrow$''. +Esimerkiksi sijoitustapaa +ylhäällä oikealla +vastaa merkkijono ''$\rightarrow$ $\rightarrow$ o $\rightarrow$ o $\rightarrow$''. +Niinpä sijoitustapojen määrä on ${k+n-1 \choose k}$. + +\textit{Tapaus 3}: Kuhunkin laatikkoon saa sijoittaa +enintään yhden pallon ja lisäksi missään kahdessa +vierekkäisessä laatikossa ei saa olla palloa. +Esimerkiksi kun $n=5$ ja $k=2$, sijoitustapoja on 6: + + +\begin{center} +\begin{tikzpicture}[scale=0.5] +\newcommand\lax[3]{ +\path[draw,thick,-] (#1-0.5,#2+0.5) -- (#1-0.5,#2-0.5) -- + (#1+0.5,#2-0.5) -- (#1+0.5,#2+0.5); +\ifthenelse{\equal{#3}{1}}{\draw[fill=black] (#1,#2-0.3) circle (0.15);}{} +\ifthenelse{\equal{#3}{2}}{\draw[fill=black] (#1-0.2,#2-0.3) circle (0.15);}{} +\ifthenelse{\equal{#3}{2}}{\draw[fill=black] (#1+0.2,#2-0.3) circle (0.15);}{} +} +\newcommand\laa[7]{ + \lax{#1}{#2}{#3} + \lax{#1+1.2}{#2}{#4} + \lax{#1+2.4}{#2}{#5} + \lax{#1+3.6}{#2}{#6} + \lax{#1+4.8}{#2}{#7} +} + +\laa{0}{0}{1}{0}{1}{0}{0} +\laa{0}{-2}{1}{0}{0}{1}{0} +\laa{8}{0}{1}{0}{0}{0}{1} +\laa{8}{-2}{0}{1}{0}{1}{0} +\laa{16}{0}{0}{1}{0}{0}{1} +\laa{16}{-2}{0}{0}{1}{0}{1} +\end{tikzpicture} +\end{center} + +Tässä tapauksessa voi ajatella, että alussa $k$ palloa +ovat laatikoissaan ja joka välissä on yksi tyhjä laatikko. +Tämän jälkeen jää valittavaksi $n-k-(k-1)=n-2k+1$ tyhjän laatikon paikat. +Mahdollisia välejä on $k+1$, joten tapauksen 2 perusteella +sijoitustapoja on ${n-2k+1+k+1-1 \choose n-2k+1} = {n-k+1 \choose n-2k+1}$. + +\subsubsection{Multinomikerroin} + +\index{multinomikerroin@multinomikerroin} + +Binomikertoimen yleistys on \key{multinomikerroin} + +\[ {n \choose k_1,k_2,\ldots,k_m} = \frac{n!}{k_1! k_2! \cdots k_m!}, \] + +missä $k_1+k_2+\cdots+k_m=n$. +Multinomikerroin ilmaisee, monellako tavalla $n$ alkiota voidaan jakaa osajoukkoihin, +joiden koot ovat $k_1,k_2,\ldots,k_m$. +Jos $m=2$, multinomikertoimen kaava vastaa binomikertoimen kaavaa. + +\section{Catalanin luvut} + +\index{Catalanin luku@Catalanin luku} + +\key{Catalanin luku} $C_n$ ilmaisee, +montako tapaa on muodostaa kelvollinen sulkulauseke +$n$ alkusulusta ja $n$ loppusulusta. + +Esimerkiksi $C_3=5$, koska 3 alkusulusta +ja 3 loppusulusta on mahdollista muodostaa +seuraavat kelvolliset sulkulausekkeet: + +\begin{itemize}[noitemsep] +\item \texttt{()()()} +\item \texttt{(())()} +\item \texttt{()(())} +\item \texttt{((()))} +\item \texttt{(()())} +\end{itemize} + +\subsubsection{Sulkulausekkeet} + +\index{sulkulauseke@sulkulauseke} + +Mikä sitten tarkkaan ottaen on +\textit{kelvollinen sulkulauseke}? +Seuraavat säännöt kuvailevat täsmällisesti +kaikki kelvolliset sulkulausekkeet: + +\begin{itemize} +\item Sulkulauseke \texttt{()} on kelvollinen. +\item Jos sulkulauseke $A$ on kelvollinen, +niin myös sulkulauseke \texttt{(}$A$\texttt{)} +on kelvollinen. +\item Jos sulkulausekkeet $A$ ja $B$ ovat kelvollisia, +niin myös sulkulauseke $AB$ on kelvollinen. +\end{itemize} + +Toinen tapa luonnehtia kelvollista sulkulauseketta on, +että jos valitaan mikä tahansa lausekkeen alkuosa, +niin alkusulkuja on ainakin yhtä monta +kuin loppusulkuja. +Lisäksi koko lausekkeessa +tulee olla tarkalleen yhtä monta +alkusulkua ja loppusulkua. + +\subsubsection{Laskutapa 1} + +Catalanin lukuja voi laskea rekursiivisesti kaavalla +\[ C_n = \sum_{i=0}^{n-1} C_{i} C_{n-i-1}.\] + +Summa käy läpi tavat +jakaa sulkulauseke kahteen osaan niin, +että kumpikin osa on kelvollinen sulkulauseke +ja alkuosa on mahdollisimman lyhyt mutta ei tyhjä. +Kunkin vaihtoehdon kohdalla alkuosassa +on $i+1$ sulkuparia ja lausekkeiden määrä +saadaan kertomalla keskenään: + +\begin{itemize} +\item $C_{i}$: tavat muodostaa sulkulauseke +alkuosan sulkupareista ulointa sulkuparia lukuun ottamatta +\item $C_{n-i-1}$: tavat muodostaa sulkulauseke +loppuosan sulkupareista +\end{itemize} +Lisäksi pohjatapauksena on $C_0=1$, koska 0 +sulkuparista voi muodostaa +tyhjän sulkulausekkeen. + +\subsubsection{Laskutapa 2} + +Catalanin lukuja voi laskea myös binomikertoimen avulla: +\[ C_n = \frac{1}{n+1} {2n \choose n}\] +Kaavan voi perustella seuraavasti: + +Kun käytössä on $n$ alkusulkua ja $n$ loppusulkua, +niistä voi muodostaa kaikkiaan ${2n \choose n}$ +sulkulauseketta. +Lasketaan seuraavaksi, moniko tällainen +sulkulauseke \textit{ei} ole kelvollinen. + +Jos sulkulauseke ei ole kelvollinen, +siinä on oltava alkuosa, +jossa loppusulkuja on alkusulkuja enemmän. +Muutetaan jokainen tällaisen alkuosan +sulkumerkki käänteiseksi. +Esimerkiksi lausekkeessa \texttt{())()(} +alkuosa on \texttt{())} ja kääntämisen +jälkeen lausekkeesta tulee \texttt{)((()(}. + +Tuloksena olevassa lausekkeessa on $n+1$ alkusulkua +ja $n-1$ loppusulkua. Tällaisia lausekkeita on +kaikkiaan ${2n \choose n+1}$, +joka on sama kuin ei-kelvollisten +sulkulausekkeiden määrä. +Niinpä kelvollisten +sulkulausekkeiden määrä voidaan laskea kaavalla +\[{2n \choose n}-{2n \choose n+1} = {2n \choose n} - \frac{n}{n+1} {2n \choose n} = \frac{1}{n+1} {2n \choose n}.\] + +\subsubsection{Puiden laskeminen} + +Catalanin luvut kertovat myös juurellisten +puiden lukumääriä: + +\begin{itemize} +\item $n$ solmun binääripuiden määrä on $C_n$ +\item $n$ solmun juurellisten puiden määrä on $C_{n-1}$ +\end{itemize} +\noindent +Esimerkiksi tapauksessa $C_3=5$ binääripuut ovat + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\path[draw,thick,-] (0,0) -- (-1,-1); +\path[draw,thick,-] (0,0) -- (1,-1); +\draw[fill=white] (0,0) circle (0.3); +\draw[fill=white] (-1,-1) circle (0.3); +\draw[fill=white] (1,-1) circle (0.3); + +\path[draw,thick,-] (4,0) -- (4-0.75,-1) -- (4-1.5,-2); +\draw[fill=white] (4,0) circle (0.3); +\draw[fill=white] (4-0.75,-1) circle (0.3); +\draw[fill=white] (4-1.5,-2) circle (0.3); + +\path[draw,thick,-] (6.5,0) -- (6.5-0.75,-1) -- (6.5-0,-2); +\draw[fill=white] (6.5,0) circle (0.3); +\draw[fill=white] (6.5-0.75,-1) circle (0.3); +\draw[fill=white] (6.5-0,-2) circle (0.3); + +\path[draw,thick,-] (9,0) -- (9+0.75,-1) -- (9-0,-2); +\draw[fill=white] (9,0) circle (0.3); +\draw[fill=white] (9+0.75,-1) circle (0.3); +\draw[fill=white] (9-0,-2) circle (0.3); + +\path[draw,thick,-] (11.5,0) -- (11.5+0.75,-1) -- (11.5+1.5,-2); +\draw[fill=white] (11.5,0) circle (0.3); +\draw[fill=white] (11.5+0.75,-1) circle (0.3); +\draw[fill=white] (11.5+1.5,-2) circle (0.3); +\end{tikzpicture} +\end{center} +ja yleiset puut ovat +\begin{center} +\begin{tikzpicture}[scale=0.7] +\path[draw,thick,-] (0,0) -- (-1,-1); +\path[draw,thick,-] (0,0) -- (0,-1); +\path[draw,thick,-] (0,0) -- (1,-1); +\draw[fill=white] (0,0) circle (0.3); +\draw[fill=white] (-1,-1) circle (0.3); +\draw[fill=white] (0,-1) circle (0.3); +\draw[fill=white] (1,-1) circle (0.3); + +\path[draw,thick,-] (3,0) -- (3,-1) -- (3,-2) -- (3,-3); +\draw[fill=white] (3,0) circle (0.3); +\draw[fill=white] (3,-1) circle (0.3); +\draw[fill=white] (3,-2) circle (0.3); +\draw[fill=white] (3,-3) circle (0.3); + +\path[draw,thick,-] (6+0,0) -- (6-1,-1); +\path[draw,thick,-] (6+0,0) -- (6+1,-1) -- (6+1,-2); +\draw[fill=white] (6+0,0) circle (0.3); +\draw[fill=white] (6-1,-1) circle (0.3); +\draw[fill=white] (6+1,-1) circle (0.3); +\draw[fill=white] (6+1,-2) circle (0.3); + +\path[draw,thick,-] (9+0,0) -- (9+1,-1); +\path[draw,thick,-] (9+0,0) -- (9-1,-1) -- (9-1,-2); +\draw[fill=white] (9+0,0) circle (0.3); +\draw[fill=white] (9+1,-1) circle (0.3); +\draw[fill=white] (9-1,-1) circle (0.3); +\draw[fill=white] (9-1,-2) circle (0.3); + +\path[draw,thick,-] (12+0,0) -- (12+0,-1) -- (12-1,-2); +\path[draw,thick,-] (12+0,0) -- (12+0,-1) -- (12+1,-2); +\draw[fill=white] (12+0,0) circle (0.3); +\draw[fill=white] (12+0,-1) circle (0.3); +\draw[fill=white] (12-1,-2) circle (0.3); +\draw[fill=white] (12+1,-2) circle (0.3); + +\end{tikzpicture} +\end{center} + + +\section{Inkluusio-ekskluusio} + +\index{inkluusio-ekskluusio} + +\key{Inkluusio-ekskluusio} +on tekniikka, jonka avulla pystyy laskemaan +joukkojen yhdisteen koon leikkausten +kokojen perusteella ja päinvastoin. +Yksinkertainen esimerkki periaatteesta on kaava +\[ |A \cup B| = |A| + |B| - |A \cap B|,\] +jossa $A$ ja $B$ ovat joukkoja ja $|X|$ +tarkoittaa joukon $X$ kokoa. +Seuraava kuva havainnollistaa kaavaa, +kun joukot ovat tason ympyröitä: + +\begin{center} +\begin{tikzpicture}[scale=0.8] + +\draw (0,0) circle (1.5); +\draw (1.5,0) circle (1.5); + +\node at (-0.75,0) {\small $A$}; +\node at (2.25,0) {\small $B$}; +\node at (0.75,0) {\small $A \cap B$}; + +\end{tikzpicture} +\end{center} + +Tavoitteena on laskea, kuinka suuri on yhdiste $A \cup B$ +eli alue, joka on toisen tai kummankin ympyrän sisällä. +Kuvan mukaisesti yhdisteen $A \cup B$ koko +saadaan laskemalla ensin yhteen ympyröiden $A$ ja $B$ koot +ja vähentämällä siitä sitten leikkauksen $A \cap B$ koko. + +Samaa ideaa voi soveltaa, kun joukkoja on enemmän. +Kolmen joukon tapauksessa kaavasta tulee +\[ |A \cup B \cup C| = |A| + |B| + |C| - |A \cap B| - |A \cap C| - |B \cap C| + |A \cap B \cap C| \] +ja vastaava kuva on + +\begin{center} +\begin{tikzpicture}[scale=0.8] + +\draw (0,0) circle (1.75); +\draw (2,0) circle (1.75); +\draw (1,1.5) circle (1.75); + +\node at (-0.75,-0.25) {\small $A$}; +\node at (2.75,-0.25) {\small $B$}; +\node at (1,2.5) {\small $C$}; +\node at (1,-0.5) {\small $A \cap B$}; +\node at (0,1.25) {\small $A \cap C$}; +\node at (2,1.25) {\small $B \cap C$}; +\node at (1,0.5) {\scriptsize $A \cap B \cap C$}; + +\end{tikzpicture} +\end{center} + +Yleisessä tapauksessa yhdisteen $X_1 \cup X_2 \cup \cdots \cup X_n$ +koon saa laskettua käymällä läpi kaikki tavat muodostaa +leikkaus joukoista $X_1,X_2,\ldots,X_n$. +Parittoman määrän joukkoja sisältävät leikkaukset +lasketaan mukaan positiivisina ja +parillisen määrän negatiivisina. + +Huomaa, että vastaavat kaavat toimivat myös käänteisesti +leikkauksen koon laskemiseen yhdisteiden kokojen perusteella. +Esimerkiksi +\[ |A \cap B| = |A| + |B| - |A \cup B|\] +ja +\[ |A \cap B \cap C| = |A| + |B| + |C| - |A \cup B| - |A \cup C| - |B \cup C| + |A \cup B \cup C| .\] + +\subsubsection{Epäjärjestykset} + +\index{epxjxrjestys@epäjärjestys} + +Lasketaan esimerkkinä, +montako tapaa on muodostaa luvuista +$(1,2,\ldots,n)$ \key{epäjärjestys} +eli permutaatio, +jossa mikään luku ei ole alkuperäisellä paikallaan. +Esimerkiksi jos $n=3$, niin epäjärjestyksiä on kaksi: $(2,3,1)$ ja $(3,1,2)$. + +Yksi tapa lähestyä tehtävää on käyttää inkluusio-ekskluusiota. +Olkoon joukko $X_k$ niiden permutaatioiden joukko, +jossa kohdassa $k$ on luku $k$. +Esimerkiksi jos $n=3$, niin joukot ovat seuraavat: +\[ +\begin{array}{lcl} +X_1 & = & \{(1,2,3),(1,3,2)\} \\ +X_2 & = & \{(1,2,3),(3,2,1)\} \\ +X_3 & = & \{(1,2,3),(2,1,3)\} \\ +\end{array} +\] +Näitä joukkoja käyttäen epäjärjestysten määrä on +\[ n! - |X_1 \cup X_2 \cup \cdots \cup X_n|, \] +eli +riittää laskea joukkojen yhdisteen koko. +Tämä palautuu inkluusio-eks\-kluu\-sion avulla +joukkojen leikkausten kokojen laskemiseen, +mikä onnistuu tehokkaasti. +Esimerkiksi kun $n=3$, joukon $|X_1 \cup X_2 \cup X_3|$ koko on +\[ +\begin{array}{lcl} + & & |X_1| + |X_2| + |X_3| - |X_1 \cap X_2| - |X_1 \cap X_3| - |X_2 \cap X_3| + |X_1 \cap X_2 \cap X_3| \\ + & = & 2+2+2-1-1-1+1 \\ + & = & 4, \\ +\end{array} +\] +joten ratkaisujen määrä on $3!-4=2$. + +Osoittautuu, että tehtävän voi ratkaista myös toisella +tavalla käyttämättä inkluusio-ekskluusiota. +Merkitään $f(n)$:llä jonon $(1,2,\ldots,n)$ epäjärjestysten määrää, +jolloin seuraava rekursio pätee: + +\begin{equation*} + f(n) = \begin{cases} + 0 & n = 1\\ + 1 & n = 2\\ + (n-1)(f(n-2) + f(n-1)) & n>2 \\ + \end{cases} +\end{equation*} + +Kaavan voi perustella käymällä läpi tapaukset, +miten luku 1 muuttuu epäjärjestyksessä. +On $n-1$ tapaa valita jokin luku $x$ luvun 1 tilalle. +Jokaisessa tällaisessa valinnassa on kaksi vaihtoehtoa: + +\textit{Vaihtoehto 1:} Luvun $x$ tilalle valitaan luku 1. +Tällöin jää $n-2$ lukua, joille tulee muodostaa epäjärjestys. + +\textit{Vaihtoehto 2:} Luvun $x$ tilalle ei valita lukua 1. +Tällöin jää $n-1$ lukua, joille tulee muodostaa epäjärjestys, +koska luvun $x$ tilalle ei saa valita lukua 1 +ja kaikki muut luvut tulee saattaa epäjärjestykseen. + +\section{Burnsiden lemma} + +\index{Burnsiden lemma@Burnsiden lemma} + +\key{Burnsiden lemma} laskee yhdistelmien määrän niin, +että symmetrisistä yhdistelmistä lasketaan +mukaan vain yksi edustaja. +Burnsiden lemman mukaan yhdistelmien määrä on +\[\sum_{k=1}^n \frac{c(k)}{n},\] +missä yhdistelmän asentoa voi muuttaa $n$ tavalla +ja $c(k)$ on niiden yhdistelmien määrä, +jotka pysyvät ennallaan, kun asentoa +muutetaan tavalla $k$. + +Lasketaan esimerkkinä, montako +erilaista tapaa on +muodostaa $n$ helmen helminauha, +kun kunkin helmen värin tulee olla +väliltä $1,2,\ldots,m$. +Kaksi helminauhaa ovat symmetriset, +jos ne voi saada näyttämään samalta pyörittämällä. +Esimerkiksi helminauhan +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw[fill=white] (0,0) circle (1); +\draw[fill=red] (0,1) circle (0.3); +\draw[fill=blue] (1,0) circle (0.3); +\draw[fill=red] (0,-1) circle (0.3); +\draw[fill=green] (-1,0) circle (0.3); +\end{tikzpicture} +\end{center} +kanssa symmetriset helminauhat ovat seuraavat: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw[fill=white] (0,0) circle (1); +\draw[fill=red] (0,1) circle (0.3); +\draw[fill=blue] (1,0) circle (0.3); +\draw[fill=red] (0,-1) circle (0.3); +\draw[fill=green] (-1,0) circle (0.3); + +\draw[fill=white] (4,0) circle (1); +\draw[fill=green] (4+0,1) circle (0.3); +\draw[fill=red] (4+1,0) circle (0.3); +\draw[fill=blue] (4+0,-1) circle (0.3); +\draw[fill=red] (4+-1,0) circle (0.3); + +\draw[fill=white] (8,0) circle (1); +\draw[fill=red] (8+0,1) circle (0.3); +\draw[fill=green] (8+1,0) circle (0.3); +\draw[fill=red] (8+0,-1) circle (0.3); +\draw[fill=blue] (8+-1,0) circle (0.3); + +\draw[fill=white] (12,0) circle (1); +\draw[fill=blue] (12+0,1) circle (0.3); +\draw[fill=red] (12+1,0) circle (0.3); +\draw[fill=green] (12+0,-1) circle (0.3); +\draw[fill=red] (12+-1,0) circle (0.3); +\end{tikzpicture} +\end{center} +Tapoja muuttaa asentoa on $n$, +koska helminauhaa voi pyörittää $0,1,\ldots,n-1$ +askelta myötäpäivään. +Jos helminauhaa pyörittää 0 askelta, +kaikki $m^n$ väritystä säilyvät ennallaan. +Jos taas helminauhaa pyörittää 1 askeleen, +vain $m$ yksiväristä helminauhaa säilyy ennallaan. + +Yleisemmin kun helminauhaa pyörittää $k$ askelta, +ennallaan säilyvien yhdistelmien määrä on +\[m^{\textrm{syt}(k,n)},\] +missä $\textrm{syt}(k,n)$ on lukujen $k$ ja $n$ +suurin yhteinen tekijä. +Tämä johtuu siitä, että $\textrm{syt}(k,n)$-kokoiset +pätkät helmiä siirtyvät toistensa paikoille +$k$ askelta eteenpäin. +Niinpä helminauhojen määrä on +Burnsiden lemman mukaan +\[\sum_{i=0}^{n-1} \frac{m^{\textrm{syt}(i,n)}}{n}. \] +Esimerkiksi kun helminauhan pituus on 4 +ja värejä on 3, helminauhoja on +\[\frac{3^4+3+3^2+3}{4} = 24. \] + +\section{Cayleyn kaava} + +\index{Cayleyn kaava@Cayleyn kaava} + +\key{Cayleyn kaavan} mukaan $n$ solmusta voi +muodostaa $n^{n-2}$ numeroitua puuta. +Puun solmut on numeroitu $1,2,\ldots,n$, +ja kaksi puuta ovat erilaiset, +jos niiden rakenne on erilainen +tai niissä on eri numerointi. + +\begin{samepage} +\noindent +Esimerkiksi kun $n=4$, numeroitujen puiden määrä on $4^{4-2}=16$: + +\begin{center} +\begin{tikzpicture}[scale=0.8] +\footnotesize + +\newcommand\puua[6]{ +\path[draw,thick,-] (#1,#2) -- (#1-1.25,#2-1.5); +\path[draw,thick,-] (#1,#2) -- (#1,#2-1.5); +\path[draw,thick,-] (#1,#2) -- (#1+1.25,#2-1.5); +\node[draw, circle, fill=white] at (#1,#2) {#3}; +\node[draw, circle, fill=white] at (#1-1.25,#2-1.5) {#4}; +\node[draw, circle, fill=white] at (#1,#2-1.5) {#5}; +\node[draw, circle, fill=white] at (#1+1.25,#2-1.5) {#6}; +} +\newcommand\puub[6]{ +\path[draw,thick,-] (#1,#2) -- (#1+1,#2); +\path[draw,thick,-] (#1+1,#2) -- (#1+2,#2); +\path[draw,thick,-] (#1+2,#2) -- (#1+3,#2); +\node[draw, circle, fill=white] at (#1,#2) {#3}; +\node[draw, circle, fill=white] at (#1+1,#2) {#4}; +\node[draw, circle, fill=white] at (#1+2,#2) {#5}; +\node[draw, circle, fill=white] at (#1+3,#2) {#6}; +} + +\puua{0}{0}{1}{2}{3}{4} +\puua{4}{0}{2}{1}{3}{4} +\puua{8}{0}{3}{1}{2}{4} +\puua{12}{0}{4}{1}{2}{3} + +\puub{0}{-3}{1}{2}{3}{4} +\puub{4.5}{-3}{1}{2}{4}{3} +\puub{9}{-3}{1}{3}{2}{4} +\puub{0}{-4.5}{1}{3}{4}{2} +\puub{4.5}{-4.5}{1}{4}{2}{3} +\puub{9}{-4.5}{1}{4}{3}{2} +\puub{0}{-6}{2}{1}{3}{4} +\puub{4.5}{-6}{2}{1}{4}{3} +\puub{9}{-6}{2}{3}{1}{4} +\puub{0}{-7.5}{2}{4}{1}{3} +\puub{4.5}{-7.5}{3}{1}{2}{4} +\puub{9}{-7.5}{3}{2}{1}{4} +\end{tikzpicture} +\end{center} +\end{samepage} + +Seuraavaksi näemme, miten Cayleyn kaavan +voi perustella samastamalla numeroidut puut +Prüfer-koodeihin. + +\subsubsection{Prüfer-koodi} + +\index{Prüfer-koodi} + +\key{Prüfer-koodi} on $n-2$ luvun jono, +joka kuvaa numeroidun puun rakenteen. +Koodi muodostuu poistamalla puusta +joka askeleella lehden, jonka numero on pienin, +ja lisäämällä lehden vieressä olevan solmun +numeron koodiin. + +Esimerkiksi puun +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (2,3) {$1$}; +\node[draw, circle] (2) at (4,3) {$2$}; +\node[draw, circle] (3) at (2,1) {$3$}; +\node[draw, circle] (4) at (4,1) {$4$}; +\node[draw, circle] (5) at (5.5,2) {$5$}; + +%\path[draw,thick,-] (1) -- (2); +%\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (1) -- (4); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (2) -- (4); +\path[draw,thick,-] (2) -- (5); +%\path[draw,thick,-] (4) -- (5); +\end{tikzpicture} +\end{center} +Prüfer-koodi on $[4,4,2]$, +koska puusta poistetaan ensin solmu 1, +sitten solmu 3 ja lopuksi solmu 5. + +Jokaiselle puulle voidaan laskea +Prüfer-koodi, minkä lisäksi +Prüfer-koodista pystyy palauttamaan +yksikäsitteisesti alkuperäisen puun. +Niinpä numeroituja puita on yhtä monta +kuin Prüfer-koodeja eli $n^{n-2}$. + diff --git a/luku23.tex b/luku23.tex new file mode 100644 index 0000000..7a1b57a --- /dev/null +++ b/luku23.tex @@ -0,0 +1,858 @@ +\chapter{Matrices} + +\index{matriisi@matriisi} + +\key{Matriisi} on kaksiulotteista taulukkoa +vastaava matemaattinen käsite, +jolle on määritelty laskutoimituksia. +Esimerkiksi +\[ +A = + \begin{bmatrix} + 6 & 13 & 7 & 4 \\ + 7 & 0 & 8 & 2 \\ + 9 & 5 & 4 & 18 \\ + \end{bmatrix} +\] +on matriisi, jossa on 3 riviä ja 4 saraketta +eli se on kokoa $3 \times 4$. +Viittaamme matriisin alkioihin +merkinnällä $[i,j]$, +jossa $i$ on rivi ja $j$ on sarake. +Esimerkiksi yllä olevassa matriisissa +$A[2,3]=8$ ja $A[3,1]=9$. + +\index{vektori@vektori} + +Matriisin erikoistapaus on \key{vektori}, +joka on kokoa $n \times 1$ oleva yksiulotteinen matriisi. +Esimerkiksi +\[ +V = +\begin{bmatrix} +4 \\ +7 \\ +5 \\ +\end{bmatrix} +\] +on vektori, jossa on 3 alkiota. + +\index{transpoosi@transpoosi} + +Matriisin $A$ \key{transpoosi} $A^T$ syntyy, +kun matriisin rivit ja sarakkeet vaihdetaan +keskenään eli $A^T[i,j]=A[j,i]$: +\[ +A^T = + \begin{bmatrix} + 6 & 7 & 9 \\ + 13 & 0 & 5 \\ + 7 & 8 & 4 \\ + 4 & 2 & 18 \\ + \end{bmatrix} +\] + +\index{nelizmatriisi@neliömatriisi} + +Matriisi on \key{neliömatriisi}, jos sen +korkeus ja leveys ovat samat. +Esimerkiksi seuraava matriisi on neliömatriisi: + +\[ +S = + \begin{bmatrix} + 3 & 12 & 4 \\ + 5 & 9 & 15 \\ + 0 & 2 & 4 \\ + \end{bmatrix} +\] + +\section{Laskutoimitukset} + +Matriisien $A$ ja $B$ summa $A+B$ on määritelty, +jos matriisit ovat yhtä suuret. +Tuloksena oleva matriisi on +samaa kokoa kuin +matriisit $A$ ja $B$ ja sen jokainen +alkio on vastaavissa kohdissa +olevien alkioiden summa. + +Esimerkiksi +\[ + \begin{bmatrix} + 6 & 1 & 4 \\ + 3 & 9 & 2 \\ + \end{bmatrix} ++ + \begin{bmatrix} + 4 & 9 & 3 \\ + 8 & 1 & 3 \\ + \end{bmatrix} += + \begin{bmatrix} + 6+4 & 1+9 & 4+3 \\ + 3+8 & 9+1 & 2+3 \\ + \end{bmatrix} += + \begin{bmatrix} + 10 & 10 & 7 \\ + 11 & 10 & 5 \\ + \end{bmatrix}. +\] + +Matriisin $A$ kertominen luvulla $x$ tarkoittaa, +että jokainen matriisin alkio kerrotaan luvulla $x$. + +Esimerkiksi +\[ + 2 \cdot \begin{bmatrix} + 6 & 1 & 4 \\ + 3 & 9 & 2 \\ + \end{bmatrix} += + \begin{bmatrix} + 2 \cdot 6 & 2\cdot1 & 2\cdot4 \\ + 2\cdot3 & 2\cdot9 & 2\cdot2 \\ + \end{bmatrix} += + \begin{bmatrix} + 12 & 2 & 8 \\ + 6 & 18 & 4 \\ + \end{bmatrix}. +\] + +\subsubsection{Matriisitulo} + +\index{matriisitulo@matriisitulo} + +Matriisien $A$ ja $B$ tulo $AB$ on määritelty, +jos matriisi $A$ on kokoa $a \times n$ +ja matriisi $B$ on kokoa $n \times b$ +eli matriisin $A$ leveys on sama kuin matriisin +$B$ korkeus. +Tuloksena oleva matriisi +on kokoa $a \times b$ +ja sen alkiot lasketaan kaavalla +\[ +AB[i,j] = \sum_{k=1}^n A[i,k] \cdot B[k,j]. +\] + +Kaavan tulkintana on, että kukin $AB$:n alkio +saadaan summana, joka muodostuu $A$:n ja +$B$:n alkioparien tuloista seuraavan +kuvan mukaisesti: + +\begin{center} +\begin{tikzpicture}[scale=0.5] +\draw (0,0) grid (4,3); +\draw (5,0) grid (10,3); +\draw (5,4) grid (10,8); + +\node at (2,-1) {$A$}; +\node at (7.5,-1) {$AB$}; +\node at (11,6) {$B$}; + +\draw[thick,->,red,line width=2pt] (0,1.5) -- (4,1.5); +\draw[thick,->,red,line width=2pt] (6.5,8) -- (6.5,4); +\draw[thick,red,line width=2pt] (6.5,1.5) circle (0.4); +\end{tikzpicture} +\end{center} + +Esimerkiksi + +\[ + \begin{bmatrix} + 1 & 4 \\ + 3 & 9 \\ + 8 & 6 \\ + \end{bmatrix} +\cdot + \begin{bmatrix} + 1 & 6 \\ + 2 & 9 \\ + \end{bmatrix} += + \begin{bmatrix} + 1 \cdot 1 + 4 \cdot 2 & 1 \cdot 6 + 4 \cdot 9 \\ + 3 \cdot 1 + 9 \cdot 2 & 3 \cdot 6 + 9 \cdot 9 \\ + 8 \cdot 1 + 6 \cdot 2 & 8 \cdot 6 + 6 \cdot 9 \\ + \end{bmatrix} += + \begin{bmatrix} + 9 & 42 \\ + 21 & 99 \\ + 20 & 102 \\ + \end{bmatrix}. +\] + +Matriisitulo ei ole vaihdannainen, +eli ei ole voimassa $A \cdot B = B \cdot A$. +Kuitenkin matriisitulo +on liitännäinen, eli on voimassa $A \cdot (B \cdot C)=(A \cdot B) \cdot C$. + +\index{ykkzsmatriisi@ykkösmatriisi} + +\key{Ykkösmatriisi} on neliömatriisi, +jonka lävistäjän jokainen alkio on 1 +ja jokainen muu alkio on 0. +Esimerkiksi $3 \times 3$ -yksikkömatriisi on +seuraavanlainen: +\[ + I = \begin{bmatrix} + 1 & 0 & 0 \\ + 0 & 1 & 0 \\ + 0 & 0 & 1 \\ + \end{bmatrix} +\] + +\begin{samepage} +Ykkösmatriisilla kertominen säilyttää matriisin +ennallaan. Esimerkiksi +\[ + \begin{bmatrix} + 1 & 0 & 0 \\ + 0 & 1 & 0 \\ + 0 & 0 & 1 \\ + \end{bmatrix} +\cdot + \begin{bmatrix} + 1 & 4 \\ + 3 & 9 \\ + 8 & 6 \\ + \end{bmatrix} += + \begin{bmatrix} + 1 & 4 \\ + 3 & 9 \\ + 8 & 6 \\ + \end{bmatrix} \hspace{10px} \textrm{ja} \hspace{10px} + \begin{bmatrix} + 1 & 4 \\ + 3 & 9 \\ + 8 & 6 \\ + \end{bmatrix} +\cdot + \begin{bmatrix} + 1 & 0 \\ + 0 & 1 \\ + \end{bmatrix} += + \begin{bmatrix} + 1 & 4 \\ + 3 & 9 \\ + 8 & 6 \\ + \end{bmatrix}. +\] +\end{samepage} + +Kahden $n \times n$ kokoisen matriisin tulon +laskeminen vie aikaa $O(n^3)$ +käyttäen suoraviivaista algoritmia. +Myös nopeampia algoritmeja on olemassa: +tällä hetkellä nopein tunnettu algoritmi +vie aikaa $O(n^{2{,}37})$. +Tällaiset algoritmit eivät kuitenkaan +ole tarpeen kisakoodauksessa. + +\subsubsection{Matriisipotenssi} + +\index{matriisipotenssi@matriisipotenssi} + +Matriisin $A$ potenssi $A^k$ on +määritelty, jos $A$ on neliömatriisi. +Määritelmä nojautuu kertolaskuun: +\[ A^k = \underbrace{A \cdot A \cdot A \cdots A}_{\textrm{$k$ kertaa}} \] +Esimerkiksi + +\[ + \begin{bmatrix} + 2 & 5 \\ + 1 & 4 \\ + \end{bmatrix}^3 = + \begin{bmatrix} + 2 & 5 \\ + 1 & 4 \\ + \end{bmatrix} \cdot + \begin{bmatrix} + 2 & 5 \\ + 1 & 4 \\ + \end{bmatrix} \cdot + \begin{bmatrix} + 2 & 5 \\ + 1 & 4 \\ + \end{bmatrix} = + \begin{bmatrix} + 48 & 165 \\ + 33 & 114 \\ + \end{bmatrix}. +\] +Lisäksi $A^0$ tuottaa ykkösmatriisin. Esimerkiksi +\[ + \begin{bmatrix} + 2 & 5 \\ + 1 & 4 \\ + \end{bmatrix}^0 = + \begin{bmatrix} + 1 & 0 \\ + 0 & 1 \\ + \end{bmatrix}. +\] + +Matriisin $A^k$ voi laskea tehokkaasti ajassa +$O(n^3 \log k)$ soveltamalla luvun 21.2 +tehokasta potenssilaskua. Esimerkiksi +\[ + \begin{bmatrix} + 2 & 5 \\ + 1 & 4 \\ + \end{bmatrix}^8 = + \begin{bmatrix} + 2 & 5 \\ + 1 & 4 \\ + \end{bmatrix}^4 \cdot + \begin{bmatrix} + 2 & 5 \\ + 1 & 4 \\ + \end{bmatrix}^4. +\] + + +\subsubsection{Determinantti} + +\index{determinantti@determinantti} + +Matriisin $A$ \key{determinantti} $\det(A)$ +on määritelty, jos $A$ on neliömatriisi. +Jos $A$ on kokoa $1 \times 1$, +niin $\det(A)=A[1,1]$. +Suuremmalle matriisille determinaatti lasketaan rekursiivisesti +kaavalla \index{kofaktori@kofaktori} +\[\det(A)=\sum_{j=1}^n A[1,j] C[1,j],\] +missä $C[i,j]$ on matriisin $A$ \key{kofaktori} +kohdassa $[i,j]$. +Kofaktori lasketaan puolestaan kaavalla +\[C[i,j] = (-1)^{i+j} \det(M[i,j]),\] +missä $M[i,j]$ on matriisi $A$, josta on poistettu +rivi $i$ ja sarake $j$. +Kofaktorissa olevan kertoimen $(-1)^{i+j}$ ansiosta +joka toinen determinantti +lisätään summaan positiivisena +ja joka toinen negatiivisena. + +\begin{samepage} +Esimerkiksi +\[ +\det( + \begin{bmatrix} + 3 & 4 \\ + 1 & 6 \\ + \end{bmatrix} +) = 3 \cdot 6 - 4 \cdot 1 = 14 +\] +\end{samepage} + +ja + +\[ +\det( + \begin{bmatrix} + 2 & 4 & 3 \\ + 5 & 1 & 6 \\ + 7 & 2 & 4 \\ + \end{bmatrix} +) = +2 \cdot +\det( + \begin{bmatrix} + 1 & 6 \\ + 2 & 4 \\ + \end{bmatrix} +) +-4 \cdot +\det( + \begin{bmatrix} + 5 & 6 \\ + 7 & 4 \\ + \end{bmatrix} +) ++3 \cdot +\det( + \begin{bmatrix} + 5 & 1 \\ + 7 & 2 \\ + \end{bmatrix} +) = 81. +\] + +\index{kxxnteismatriisi@käänteismatriisi} + +Determinantti kertoo, onko matriisille +$A$ olemassa \key{käänteismatriisia} +$A^{-1}$, jolle pätee $A \cdot A^{-1} = I$, +missä $I$ on ykkösmatriisi. +Osoittautuu, että $A^{-1}$ on olemassa +tarkalleen silloin, kun $\det(A) \neq 0$, +ja sen voi laskea kaavalla + +\[A^{-1}[i,j] = \frac{C[j,i]}{det(A)}.\] + +Esimerkiksi + +\[ +\underbrace{ + \begin{bmatrix} + 2 & 4 & 3\\ + 5 & 1 & 6\\ + 7 & 2 & 4\\ + \end{bmatrix} +}_{A} +\cdot +\underbrace{ + \frac{1}{81} + \begin{bmatrix} + -8 & -10 & 21 \\ + 22 & -13 & 3 \\ + 3 & 24 & -18 \\ + \end{bmatrix} +}_{A^{-1}} += +\underbrace{ + \begin{bmatrix} + 1 & 0 & 0 \\ + 0 & 1 & 0 \\ + 0 & 0 & 1 \\ + \end{bmatrix} +}_{I}. +\] + +\section{Lineaariset rekursioyhtälöt} + +\index{rekursioyhtxlz@rekursioyhtälö} +\index{lineaarinen rekursioyhtxlz@lineaarinen rekursioyhtälö} + +\key{Lineaarinen rekursioyhtälö} +voidaan esittää funktiona $f(n)$, +jolle on annettu alkuarvot +$f(0),f(1),\ldots,f(k-1)$ +ja jonka suuremmat arvot +parametrista $k$ lähtien lasketaan +rekursiivisesti kaavalla +\[f(n) = c_1 f(n-1) + c_2 f(n-2) + \ldots + c_k f (n-k),\] +missä $c_1,c_2,\ldots,c_k$ ovat vakiokertoimia. + +Funktion arvon $f(n)$ voi laskea dynaamisella +ohjelmoinnilla ajassa $O(kn)$ +laskemalla kaikki arvot $f(0),f(1),\ldots,f(n)$ järjestyksessä. +Tätä ratkaisua voi kuitenkin tehostaa merkittävästi +matriisien avulla, kun $k$ on pieni. +Seuraavaksi näemme, miten arvon $f(n)$ +voi laskea ajassa $O(k^3 \log n)$. + +\subsubsection{Fibonaccin luvut} + +\index{Fibonaccin luku@Fibonaccin luku} + +Yksinkertainen esimerkki lineaarisesta rekursioyhtälöstä +on Fibonaccin luvut määrittelevä funktio: +\[ +\begin{array}{lcl} +f(0) & = & 0 \\ +f(1) & = & 1 \\ +f(n) & = & f(n-1)+f(n-2) \\ +\end{array} +\] +Tässä tapauksessa $k=2$ ja $c_1=c_2=1$. + +\begin{samepage} +Ideana on esittää Fibonaccin lukujen laskukaava +$2 \times 2$ -kokoisena neliömatriisina +$X$, jolle pätee +\[ X \cdot + \begin{bmatrix} + f(i) \\ + f(i+1) \\ + \end{bmatrix} += + \begin{bmatrix} + f(i+1) \\ + f(i+2) \\ + \end{bmatrix}, + \] +eli $X$:lle annetaan +''syötteenä'' arvot $f(i)$ ja $f(i+1)$, +ja $X$ muodostaa niistä +arvot $f(i+1)$ ja $f(i+2)$. +Osoittautuu, että tällainen matriisi on + +\[ X = + \begin{bmatrix} + 0 & 1 \\ + 1 & 1 \\ + \end{bmatrix}. +\] +\end{samepage} +\noindent +Esimerkiksi +\[ + \begin{bmatrix} + 0 & 1 \\ + 1 & 1 \\ + \end{bmatrix} +\cdot + \begin{bmatrix} + f(5) \\ + f(6) \\ + \end{bmatrix} += + \begin{bmatrix} + 0 & 1 \\ + 1 & 1 \\ + \end{bmatrix} +\cdot + \begin{bmatrix} + 5 \\ + 8 \\ + \end{bmatrix} += + \begin{bmatrix} + 8 \\ + 13 \\ + \end{bmatrix} += + \begin{bmatrix} + f(6) \\ + f(7) \\ + \end{bmatrix}. +\] +Tämän ansiosta arvon $f(n)$ sisältävän matriisin saa laskettua +kaavalla +\[ + \begin{bmatrix} + f(n) \\ + f(n+1) \\ + \end{bmatrix} += +X^n \cdot + \begin{bmatrix} + f(0) \\ + f(1) \\ + \end{bmatrix} += + \begin{bmatrix} + 0 & 1 \\ + 1 & 1 \\ + \end{bmatrix}^n +\cdot + \begin{bmatrix} + 0 \\ + 1 \\ + \end{bmatrix}. +\] +Potenssilasku $X^n$ on mahdollista laskea ajassa +$O(k^3 \log n)$, +joten myös funktion arvon $f(n)$ +saa laskettua ajassa $O(k^3 \log n)$. + +\subsubsection{Yleinen tapaus} + +Tarkastellaan sitten yleistä tapausta, +missä $f(n)$ on mikä tahansa lineaarinen +rekursioyhtälö. Nyt tavoitteena on etsiä +matriisi $X$, jolle pätee + +\[ X \cdot + \begin{bmatrix} + f(i) \\ + f(i+1) \\ + \vdots \\ + f(i+k-1) \\ + \end{bmatrix} += + \begin{bmatrix} + f(i+1) \\ + f(i+2) \\ + \vdots \\ + f(i+k) \\ + \end{bmatrix}. +\] +Tällainen matriisi on +\[ +X = + \begin{bmatrix} + 0 & 1 & 0 & 0 & \cdots & 0 \\ + 0 & 0 & 1 & 0 & \cdots & 0 \\ + 0 & 0 & 0 & 1 & \cdots & 0 \\ + \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ + 0 & 0 & 0 & 0 & \cdots & 1 \\ + c_k & c_{k-1} & c_{k-2} & c_{k-3} & \cdots & c_1 \\ + \end{bmatrix}. +\] +Matriisin $k-1$ ensimmäisen rivin jokainen alkio on 0, +paitsi yksi alkio on 1. +Nämä rivit kopioivat +arvon $f(i+1)$ arvon $f(i)$ tilalle, +arvon $f(i+2)$ arvon $f(i+1)$ tilalle jne. +Viimeinen rivi sisältää rekursiokaavan kertoimet, +joiden avulla muodostuu uusi arvo $f(i+k)$. + +\begin{samepage} +Nyt arvon $f(n)$ pystyy laskemaan ajassa $O(k^3 \log n)$ +kaavalla +\[ + \begin{bmatrix} + f(n) \\ + f(n+1) \\ + \vdots \\ + f(n+k-1) \\ + \end{bmatrix} += +X^n \cdot + \begin{bmatrix} + f(0) \\ + f(1) \\ + \vdots \\ + f(k-1) \\ + \end{bmatrix}. +\] +\end{samepage} + +\section{Verkot ja matriisit} + +\subsubsection{Polkujen määrä} + +Matriisipotenssilla +on mielenkiintoinen vaikutus +verkon vierusmatriisin sisältöön. +Kun $V$ on painottoman verkon vierusmatriisi, +niin $V^n$ kertoo, +montako $n$ kaaren pituista polkua +eri solmuista on toisiinsa. + +Esimerkiksi verkon +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$1$}; +\node[draw, circle] (2) at (1,1) {$4$}; +\node[draw, circle] (3) at (3,3) {$2$}; +\node[draw, circle] (4) at (5,3) {$3$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; + +\path[draw,thick,->,>=latex] (1) -- (2); +\path[draw,thick,->,>=latex] (2) -- (3); +\path[draw,thick,->,>=latex] (3) -- (1); +\path[draw,thick,->,>=latex] (4) -- (3); +\path[draw,thick,->,>=latex] (3) -- (5); +\path[draw,thick,->,>=latex] (3) -- (6); +\path[draw,thick,->,>=latex] (6) -- (4); +\path[draw,thick,->,>=latex] (6) -- (5); +\end{tikzpicture} +\end{center} +vierusmatriisi on +\[ +V= \begin{bmatrix} + 0 & 0 & 0 & 1 & 0 & 0 \\ + 1 & 0 & 0 & 0 & 1 & 1 \\ + 0 & 1 & 0 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 0 & 1 & 0 \\ + \end{bmatrix}. +\] +Nyt esimerkiksi matriisi +\[ +V^4= \begin{bmatrix} + 0 & 0 & 1 & 1 & 1 & 0 \\ + 2 & 0 & 0 & 0 & 2 & 2 \\ + 0 & 2 & 0 & 0 & 0 & 0 \\ + 0 & 2 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 1 & 1 & 0 \\ + \end{bmatrix} +\] +kertoo, montako 4 kaaren pituista polkua +solmuista on toisiinsa. +Esimerkiksi $V^4[2,5]=2$, +koska solmusta 2 solmuun 5 on olemassa +4 kaaren pituiset polut +$2 \rightarrow 1 \rightarrow 4 \rightarrow 2 \rightarrow 5$ +ja +$2 \rightarrow 6 \rightarrow 3 \rightarrow 2 \rightarrow 5$. + +\subsubsection{Lyhimmät polut} + +Samantapaisella idealla voi laskea painotetussa verkossa +kullekin solmuparille, +kuinka pitkä on lyhin $n$ kaaren pituinen polku solmujen välillä. +Tämä vaatii matriisitulon määritelmän muuttamista +niin, että siinä ei lasketa polkujen yhteismäärää +vaan minimoidaan polun pituutta. + +\begin{samepage} +Tarkastellaan esimerkkinä seuraavaa verkkoa: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$1$}; +\node[draw, circle] (2) at (1,1) {$4$}; +\node[draw, circle] (3) at (3,3) {$2$}; +\node[draw, circle] (4) at (5,3) {$3$}; +\node[draw, circle] (5) at (3,1) {$5$}; +\node[draw, circle] (6) at (5,1) {$6$}; + +\path[draw,thick,->,>=latex] (1) -- node[font=\small,label=left:4] {} (2); +\path[draw,thick,->,>=latex] (2) -- node[font=\small,label=left:1] {} (3); +\path[draw,thick,->,>=latex] (3) -- node[font=\small,label=north:2] {} (1); +\path[draw,thick,->,>=latex] (4) -- node[font=\small,label=north:4] {} (3); +\path[draw,thick,->,>=latex] (3) -- node[font=\small,label=left:1] {} (5); +\path[draw,thick,->,>=latex] (3) -- node[font=\small,label=left:2] {} (6); +\path[draw,thick,->,>=latex] (6) -- node[font=\small,label=right:3] {} (4); +\path[draw,thick,->,>=latex] (6) -- node[font=\small,label=below:2] {} (5); +\end{tikzpicture} +\end{center} +\end{samepage} + +Muodostetaan verkosta vierusmatriisi, jossa arvo +$\infty$ tarkoittaa, että kaarta ei ole, +ja muut arvot ovat kaarten pituuksia. +Matriisista tulee +\[ +V= \begin{bmatrix} + \infty & \infty & \infty & 4 & \infty & \infty \\ + 2 & \infty & \infty & \infty & 1 & 2 \\ + \infty & 4 & \infty & \infty & \infty & \infty \\ + \infty & 1 & \infty & \infty & \infty & \infty \\ + \infty & \infty & \infty & \infty & \infty & \infty \\ + \infty & \infty & 3 & \infty & 2 & \infty \\ + \end{bmatrix}. +\] + +Nyt voimme laskea matriisitulon kaavan +\[ +AB[i,j] = \sum_{k=1}^n A[i,k] \cdot B[k,j] +\] +sijasta kaavalla +\[ +AB[i,j] = \min_{k=1}^n A[i,k] + B[k,j], +\] +eli summa muuttuu minimiksi ja tulo summaksi. +Tämän seurauksena matriisipotenssi +selvittää lyhimmät polkujen pituudet solmujen +välillä. Esimerkiksi + +\[ +V^4= \begin{bmatrix} + \infty & \infty & 10 & 11 & 9 & \infty \\ + 9 & \infty & \infty & \infty & 8 & 9 \\ + \infty & 11 & \infty & \infty & \infty & \infty \\ + \infty & 8 & \infty & \infty & \infty & \infty \\ + \infty & \infty & \infty & \infty & \infty & \infty \\ + \infty & \infty & 12 & 13 & 11 & \infty \\ + \end{bmatrix} +\] +eli esimerkiksi lyhin 4 kaaren pituinen polku +solmusta 2 solmuun 5 on pituudeltaan 8. +Tämä polku on $2 \rightarrow 1 \rightarrow 4 \rightarrow 2 \rightarrow 5$. + +\subsubsection{Kirchhoffin lause} + +\index{Kirchhoffin lause@Kirchhoffin lause} +\index{virittxvx puu@virittävä puu} + +\key{Kirchhoffin lause} laskee +verkon virittävän puiden määrän +verkosta muodostetun matriisin determinantin avulla. +Esimerkiksi verkolla +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (1,3) {$1$}; +\node[draw, circle] (2) at (3,3) {$2$}; +\node[draw, circle] (3) at (1,1) {$3$}; +\node[draw, circle] (4) at (3,1) {$4$}; + +\path[draw,thick,-] (1) -- (2); +\path[draw,thick,-] (1) -- (3); +\path[draw,thick,-] (3) -- (4); +\path[draw,thick,-] (1) -- (4); +\end{tikzpicture} +\end{center} +on kolme virittävää puuta: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1a) at (1,3) {$1$}; +\node[draw, circle] (2a) at (3,3) {$2$}; +\node[draw, circle] (3a) at (1,1) {$3$}; +\node[draw, circle] (4a) at (3,1) {$4$}; + +\path[draw,thick,-] (1a) -- (2a); +%\path[draw,thick,-] (1a) -- (3a); +\path[draw,thick,-] (3a) -- (4a); +\path[draw,thick,-] (1a) -- (4a); + +\node[draw, circle] (1b) at (1+4,3) {$1$}; +\node[draw, circle] (2b) at (3+4,3) {$2$}; +\node[draw, circle] (3b) at (1+4,1) {$3$}; +\node[draw, circle] (4b) at (3+4,1) {$4$}; + +\path[draw,thick,-] (1b) -- (2b); +\path[draw,thick,-] (1b) -- (3b); +%\path[draw,thick,-] (3b) -- (4b); +\path[draw,thick,-] (1b) -- (4b); + +\node[draw, circle] (1c) at (1+8,3) {$1$}; +\node[draw, circle] (2c) at (3+8,3) {$2$}; +\node[draw, circle] (3c) at (1+8,1) {$3$}; +\node[draw, circle] (4c) at (3+8,1) {$4$}; + +\path[draw,thick,-] (1c) -- (2c); +\path[draw,thick,-] (1c) -- (3c); +\path[draw,thick,-] (3c) -- (4c); +%\path[draw,thick,-] (1c) -- (4c); +\end{tikzpicture} +\end{center} +\index{Laplacen matriisi@Laplacen matriisi} +Muodostetaan verkosta \key{Laplacen matriisi} $L$, +jossa $L[i,i]$ on solmun $i$ aste ja +$L[i,j]=-1$, jos solmujen $i$ ja $j$ välillä on kaari, +ja muuten $L[i,j]=0$. +Tässä tapauksessa matriisista tulee + +\[ +L= \begin{bmatrix} + 3 & -1 & -1 & -1 \\ + -1 & 1 & 0 & 0 \\ + -1 & 0 & 2 & -1 \\ + -1 & 0 & -1 & 2 \\ + \end{bmatrix}. +\] + +Nyt virittävien puiden määrä on determinantti +matriisista, joka saadaan poistamasta matriisista $L$ +jokin rivi ja jokin sarake. +Esimerkiksi jos poistamme ylimmän rivin ja +vasemman sarakkeen, tuloksena on + +\[ \det( +\begin{bmatrix} + 1 & 0 & 0 \\ + 0 & 2 & -1 \\ + 0 & -1 & 2 \\ + \end{bmatrix} +) =3.\] +Determinantista tulee aina sama riippumatta siitä, +mikä rivi ja sarake matriisista $L$ poistetaan. + +Huomaa, että Kirchhoffin lauseen erikoistapauksena on +luvun 22.5 Cayleyn kaava, koska +täydellisessä $n$ solmun verkossa + +\[ \det( +\begin{bmatrix} + n-1 & -1 & \cdots & -1 \\ + -1 & n-1 & \cdots & -1 \\ + \vdots & \vdots & \ddots & \vdots \\ + -1 & -1 & \cdots & n-1 \\ + \end{bmatrix} +) =n^{n-2}.\] + + + diff --git a/luku24.tex b/luku24.tex new file mode 100644 index 0000000..2e2ab08 --- /dev/null +++ b/luku24.tex @@ -0,0 +1,686 @@ +\chapter{Probability} + +\index{todennxkzisyys@todennäköisyys} + +\key{Todennäköisyys} on luku väliltä $0 \ldots 1$, +joka kuvaa sitä, miten todennäköinen jokin +tapahtuma on. +Varman tapahtuman todennäköisyys on 1, +ja mahdottoman tapahtuman todennäköisyys on 0. + +Tyypillinen esimerkki todennäköisyydestä +on nopan heitto, jossa tuloksena +on silmäluku väliltä $1,2,\ldots,6$. +Yleensä oletetaan, että kunkin silmäluvun +todennäköisyys on $1/6$ +eli kaikki tulokset ovat yhtä todennäköisiä. + +Tapahtuman todennäköisyyttä merkitään $P(\cdots)$, +jossa kolmen pisteen tilalla on tapahtuman kuvaus. +Esimerkiksi nopan heitossa +$P(\textrm{''silmäluku on 4''})=1/6$, +$P(\textrm{''silmäluku ei ole 6''})=5/6$ +ja $P(\textrm{''silmäluku on parillinen''})=1/2$. + +\section{Laskutavat} + +Todennäköisyyden laskemiseen on kaksi +tavallista laskutapaa: +kombinatorinen laskeminen ja prosessin simulointi. +Lasketaan esimerkkinä, mikä on todennäköisyys sille, +että kun sekoitetusta korttipakasta nostetaan +kolme ylintä korttia, jokaisen kortin arvo on sama +(esimerkiksi ristikasi, herttakasi ja patakasi). + +\subsubsection*{Laskutapa 1} + +Kombinatorisessa laskutavassa +todennäköisyyden kaava on + +\[\frac{\textrm{halutut tapaukset}}{\textrm{kaikki tapaukset}}.\] + +Tässä tehtävässä halutut tapaukset ovat niitä, +joissa jokaisen kolmen kortin arvo on sama. +Tällaisia tapauksia on $13 {4 \choose 3}$, +koska on 13 vaihtoehtoa, mikä on kortin arvo, +ja ${4 \choose 3}$ tapaa valita 3 maata 4 mahdollisesta. + +Kaikkien tapausten määrä on ${52 \choose 3}$, +koska 52 kortista valitaan 3 korttia. +Niinpä tapahtuman todennäköisyys on + +\[\frac{13 {4 \choose 3}}{{52 \choose 3}} = \frac{1}{425}.\] + +\subsubsection*{Laskutapa 2} + +Toinen tapa laskea todennäköisyys on simuloida prosessia, +jossa tapahtuma syntyy. +Tässä tapauksessa pakasta nostetaan kolme korttia, +joten prosessissa on kolme vaihetta. +Vaatimuksena on, että prosessin jokainen vaihe onnistuu. + +Ensimmäisen kortin nosto onnistuu varmasti, +koska mikä tahansa kortti kelpaa. +Tämän jälkeen kahden seuraavan kortin +arvon tulee olla sama. +Toisen kortin nostossa kortteja on jäljellä 51 +ja niistä 3 kelpaa, joten todennäköisyys on $3/51$. +Vastaavasti kolmannen kortin nostossa +todennäköisyys on $2/50$. + +Todennäköisyys koko prosessin onnistumiselle on + +\[1 \cdot \frac{3}{51} \cdot \frac{2}{50} = \frac{1}{425}.\] + +\section{Tapahtumat} + +Todennäköisyyden tapahtuma +voidaan esittää joukkona +\[A \subset X,\] +missä $X$ sisältää kaikki mahdolliset alkeistapaukset +ja $A$ on jokin alkeistapausten osajoukko. +Esimerkiksi nopanheitossa alkeistapaukset ovat +\[X = \{x_1,x_2,x_3,x_4,x_5,x_6\},\] +missä $x_k$ tarkoittaa silmälukua $k$. +Nyt esimerkiksi tapahtumaa ''silmäluku on parillinen'' +vastaa joukko +\[A = \{x_2,x_4,x_6\}.\] + +Jokaista alkeistapausta $x$ +vastaa todennäköisyys $p(x)$. +Tämän ansiosta joukkoa $A$ vastaavan tapahtuman +todennäköisyys $P(A)$ voidaan +laskea alkeistapausten todennäköisyyksien +summana kaavalla +\[P(A) = \sum_{x \in A} p(x).\] +Esimerkiksi nopanheitossa $p(x)=1/6$ +jokaiselle alkeistapaukselle $x$, joten +tapahtuman ''silmäluku on parillinen'' +todennäköisyys on +\[p(x_2)+p(x_4)+p(x_6)=1/2.\] + +Alkeistapahtumat tulee aina valita niin, +että kaikkien alkeistapausten +todennäköisyyksien summa on 1 eli $P(X)=1$. + +Koska todennäköisyyden tapahtumat ovat joukkoja, +niihin voi soveltaa jouk\-ko-opin operaatioita: + +\begin{itemize} +\item \key{Komplementti} $\bar A$ tarkoittaa +tapahtumaa ''$A$ ei tapahdu''. +Esimerkiksi nopanheitossa tapahtuman +$A=\{x_2,x_4,x_6\}$ komplementti on +$\bar A = \{x_1,x_3,x_5\}$. +\item \key{Yhdiste} $A \cup B$ tarkoittaa +tapahtumaa ''$A$ tai $B$ tapahtuu''. +Esimerkiksi tapahtumien $A=\{x_2,x_5\}$ +ja $B=\{x_4,x_5,x_6\}$ yhdiste on +$A \cup B = \{x_2,x_4,x_5,x_6\}$. +\item \key{Leikkaus} $A \cap B$ tarkoittaa +tapahtumaa ''$A$ ja $B$ tapahtuvat''. +Esimerkiksi tapahtumien $A=\{x_2,x_5\}$ +ja $B=\{x_4,x_5,x_6\}$ leikkaus on +$A \cap B = \{x_5\}$. +\end{itemize} + +\subsubsection{Komplementti} + +Komplementin $\bar A$ +todennäköisyys lasketaan kaavalla +\[P(\bar A)=1-P(A).\] + +Joskus tehtävän ratkaisu on kätevää +laskea komplementin kautta +miettimällä tilannetta käänteisesti. +Esimerkiksi todennäköisyys saada +silmäluku 6 ainakin kerran, +kun noppaa heitetään kymmenen kertaa, on +\[1-(5/6)^{10}.\] + +Tässä $5/6$ on todennäköisyys, +että yksittäisen heiton silmäluku ei ole 6, +ja $(5/6)^{10}$ on todennäköisyys, että yksikään +silmäluku ei ole 6 kymmenessä heitossa. +Tämän komplementti tuottaa halutun tuloksen. + +\subsubsection{Yhdiste} + +Yhdisteen $A \cup B$ todennäköisyys lasketaan kaavalla +\[P(A \cup B)=P(A)+P(B)-P(A \cap B).\] +Esimerkiksi nopanheitossa tapahtumien +\[A=\textrm{''silmäluku on parillinen''}\] +ja +\[B=\textrm{''silmäluku on alle 4''}\] +yhdisteen +\[A \cup B=\textrm{''silmäluku on parillinen tai alle 4''}\] +todennäköisyys on +\[P(A \cup B) = P(A)+P(B)-P(A \cap B)=1/2+1/2-1/6=5/6.\] + +Jos tapahtumat $A$ ja $B$ ovat \key{erilliset} eli $A \cap B$ on tyhjä, +yhdisteen $A \cup B$ todennäköisyys on yksinkertaisesti + +\[P(A \cup B)=P(A)+P(B).\] + +\subsubsection{Ehdollinen todennäköisyys} + +\index{ehdollinen todennxkzisyys@ehdollinen todennäköisyys} + +\key{Ehdollinen todennäköisyys} +\[P(A | B) = \frac{P(A \cap B)}{P(B)}\] +on tapahtuman $A$ todennäköisyys +olettaen, että tapahtuma $B$ tapahtuu. +Tällöin todennäköisyyden laskennassa otetaan +huomioon vain ne alkeistapaukset, +jotka kuuluvat joukkoon $B$. + +Äskeisen esimerkin joukkoja käyttäen +\[P(A | B)= 1/3,\] +koska joukon $B$ alkeistapaukset ovat +$\{x_1,x_2,x_3\}$ ja niistä yhdessä +silmäluku on parillinen. +Tämä on todennäköisyys saada parillinen silmäluku, +jos tiedetään, että silmäluku on välillä 1--3. + +\subsubsection{Leikkaus} + +\index{riippumattomuus@riippumattomuus} + +Ehdollisen todennäköisyyden avulla +leikkauksen $A \cap B$ todennäköisyys +voidaan laskea kaavalla +\[P(A \cap B)=P(A)P(B|A).\] +Tapahtumat $A$ ja $B$ ovat \key{riippumattomat}, jos +\[P(A|B)=P(A) \hspace{10px}\textrm{ja}\hspace{10px} P(B|A)=P(B),\] +jolloin $B$:n tapahtuminen ei vaikuta $A$:n +todennäköisyyteen ja päinvastoin. +Tässä tapauksessa leikkauksen +todennäköisyys on +\[P(A \cap B)=P(A)P(B).\] +Esimerkiksi pelikortin nostamisessa +tapahtumat +\[A = \textrm{''kortin maa on risti''}\] +ja +\[B = \textrm{''kortin arvo on 4''}\] +ovat riippumattomat. +Niinpä tapahtuman +\[A \cap B = \textrm{''kortti on ristinelonen''}\] +todennäköisyys on +\[P(A \cap B)=P(A)P(B)=1/4 \cdot 1/13 = 1/52.\] + +\section{Satunnaismuuttuja} + +\index{satunnaismuuttuja@satunnaismuuttuja} + +\key{Satunnaismuuttuja} on arvo, joka syntyy satunnaisen +prosessin tuloksena. +Satunnaismuuttujaa merkitään yleensä +suurella kirjaimella. +Esimerkiksi kahden nopan heitossa yksi mahdollinen +satunnaismuuttuja on +\[X=\textrm{''silmälukujen summa''}.\] +Esimerkiksi jos heitot ovat $(4,6)$, +niin $X$ saa arvon 10. + +Merkintä $P(X=x)$ tarkoittaa todennäköisyyttä, +että satunnaismuuttujan $X$ arvo on $x$. +Edellisessä esimerkissä $P(X=10)=3/36$, +koska erilaisia heittotapoja on 36 +ja niistä summan 10 tuottavat heitot +$(4,6)$, $(5,5)$ ja $(6,4)$. + +\subsubsection{Odotusarvo} + +\index{odotusarvo@odotusarvo} + +\key{Odotusarvo} $E[X]$ kertoo, mikä satunnaismuuttujan $X$ +arvo on keskimääräisessä tilanteessa. +Odotusarvo lasketaan summana +\[\sum_x P(X=x)x,\] +missä $x$ saa kaikki mahdolliset satunnaismuuttujan arvot. + +Esimerkiksi nopan heitossa silmäluvun odotusarvo on + +\[1/6 \cdot 1 + 1/6 \cdot 2 + 1/6 \cdot 3 + 1/6 \cdot 4 + 1/6 \cdot 5 + 1/6 \cdot 6 = 7/2.\] + +Usein hyödyllinen odotusarvon ominaisuus on \key{lineaarisuus}. +Sen ansiosta summa $E[X_1+X_2+\cdots+X_n]$ voidaan laskea $E[X_1]+E[X_2]+\cdots+E[X_n]$. +Kaava pätee myös silloin, kun satunnaismuuttujat riippuvat toisistaan. + +Esimerkiksi kahden nopan heitossa silmälukujen summan odotusarvo on +\[E[X_1+X_2]=E[X_1]+E[X_2]=7/2+7/2=7.\] + +Tarkastellaan sitten tehtävää, +jossa $n$ laatikkoon sijoitetaan +satunnaisesti $n$ palloa +ja laskettavana on odotusarvo, +montako laatikkoa jää tyhjäksi. +Kullakin pallolla on yhtä suuri todennäköisyys +päätyä mihin tahansa laatikkoon. +Esimerkiksi jos $n=2$, niin +vaihtoehdot ovat seuraavat: +\begin{center} +\begin{tikzpicture} +\draw (0,0) rectangle (1,1); +\draw (1.2,0) rectangle (2.2,1); +\draw (3,0) rectangle (4,1); +\draw (4.2,0) rectangle (5.2,1); +\draw (6,0) rectangle (7,1); +\draw (7.2,0) rectangle (8.2,1); +\draw (9,0) rectangle (10,1); +\draw (10.2,0) rectangle (11.2,1); + +\draw[fill=blue] (0.5,0.2) circle (0.1); +\draw[fill=red] (1.7,0.2) circle (0.1); +\draw[fill=red] (3.5,0.2) circle (0.1); +\draw[fill=blue] (4.7,0.2) circle (0.1); +\draw[fill=blue] (6.25,0.2) circle (0.1); +\draw[fill=red] (6.75,0.2) circle (0.1); +\draw[fill=blue] (10.45,0.2) circle (0.1); +\draw[fill=red] (10.95,0.2) circle (0.1); +\end{tikzpicture} +\end{center} +Tässä tapauksessa odotusarvo +tyhjien laatikoiden määrälle on +\[\frac{0+0+1+1}{4} = \frac{1}{2}.\] +Yleisessä tapauksessa +todennäköisyys, että yksittäinen hattu on tyhjä, +on +\[\Big(\frac{n-1}{n}\Big)^n,\] +koska mikään pallo ei saa mennä sinne. +Niinpä odotusarvon lineaarisuuden ansiosta tyhjien hattujen +määrän odotusarvo on +\[n \cdot \Big(\frac{n-1}{n}\Big)^n.\] + +\subsubsection{Jakaumat} + +\index{jakauma@jakauma} + +Satunnaismuuttujan \key{jakauma} kertoo, +millä todennäköisyydellä satunnaismuuttuja +saa minkäkin arvon. +Jakauma muodostuu arvoista $P(X=x)$. +Esimerkiksi kahden nopan heitossa +silmälukujen summan jakauma on: +\begin{center} +\small { +\begin{tabular}{r|rrrrrrrrrrrrr} +$x$ & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 & 10 & 11 & 12 \\ +$P(X=x)$ & $1/36$ & $2/36$ & $3/36$ & $4/36$ & $5/36$ & $6/36$ & $5/36$ & $4/36$ & $3/36$ & $2/36$ & $1/36$ \\ +\end{tabular} +} +\end{center} +Tutustumme seuraavaksi muutamaan usein esiintyvään jakaumaan. +\index{tasajakauma@tasajakauma} +~\\\\ +\key{Tasajakauman} satunnaismuuttuja +saa arvoja väliltä $a \ldots b$ +ja jokaisen arvon todennäköisyys on sama. +Esimerkiksi yhden nopan heitto tuottaa tasajakauman, +jossa $P(X=x)=1/6$, kun $x=1,2,\ldots,6$. + +Tasajakaumassa $X$:n odotusarvo on +\[E[X] = \frac{a+b}{2}.\] +\index{binomijakauma@binomijakauma} +~\\ +\key{Binomijakauma} kuvaa tilannetta, jossa tehdään $n$ +yritystä ja joka yrityksessä onnistumisen +todennäköisyys on $p$. Satunnaismuuttuja $X$ +on onnistuneiden yritysten määrä, +ja arvon $x$ todennäköisyys on +\[P(X=x)=p^x (1-p)^{n-x} {n \choose x},\] +missä $p^x$ kuvaa onnistuneita yrityksiä, +$(1-p)^{n-x}$ kuvaa epäonnistuneita yrityksiä +ja ${n \choose x}$ antaa erilaiset tavat, +miten yritykset sijoittuvat toisiinsa nähden. + +Esimerkiksi jos heitetään 10 kertaa noppaa, +todennäköisyys saada tarkalleen 3 kertaa silmäluku 6 +on $(1/6)^3 (5/6)^7 {10 \choose 3}$. + +Binomijakaumassa $X$:n odotusarvo on +\[E[X] = pn.\] +\index{geometrinen jakauma@geometrinen jakauma} +~\\ +\key{Geometrinen jakauma} kuvaa tilannetta, +jossa onnistumisen todennäköisyys on $p$ +ja yrityksiä tehdään, kunnes tulee ensimmäinen +onnistuminen. Satunnaismuuttuja $X$ on +tarvittavien heittojen määrä, +ja arvon $x$ todennäköisyys on +\[P(X=x)=(1-p)^{x-1} p,\] +missä $(1-p)^{x-1}$ kuvaa epäonnistuneita yrityksiä ja +$p$ on ensimmäinen onnistunut yritys. + +Esimerkiksi jos heitetään noppaa, +kunnes tulee silmäluku 6, todennäköisyys +heittää tarkalleen 4 kertaa on $(5/6)^3 1/6$. + +Geometrisessa jakaumassa $X$:n odotusarvo on +\[E[X]=\frac{1}{p}.\] + +\section{Markovin ketju} + +\index{Markovin ketju@Markovin ketju} + +\key{Markovin ketju} on satunnaisprosessi, +joka muodostuu tiloista ja niiden välisistä siirtymistä. +Jokaisesta tilasta tiedetään, millä todennäköisyydellä +siitä siirrytään toisiin tiloihin. +Markovin ketju voidaan esittää verkkona, +jonka solmut ovat tiloja +ja kaaret niiden välisiä siirtymiä. + +Tarkastellaan esimerkkinä tehtävää, +jossa olet alussa $n$-kerroksisen +rakennuksen kerroksessa 1. +Joka askeleella liikut satunnaisesti +kerroksen ylöspäin tai alaspäin, +paitsi kerroksesta 1 liikut aina ylöspäin +ja kerroksesta $n$ aina alaspäin. +Mikä on todennäköisyys, että olet $m$ +askeleen jälkeen kerroksessa $k$? + +Tehtävässä kukin rakennuksen kerros +on yksi tiloista, ja kerrosten välillä liikutaan +satunnaisesti. +Esimerkiksi jos $n=5$, verkosta tulee: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {$1$}; +\node[draw, circle] (2) at (2,0) {$2$}; +\node[draw, circle] (3) at (4,0) {$3$}; +\node[draw, circle] (4) at (6,0) {$4$}; +\node[draw, circle] (5) at (8,0) {$5$}; + +\path[draw,thick,->] (1) edge [bend left=40] node[font=\small,label=$1$] {} (2); +\path[draw,thick,->] (2) edge [bend left=40] node[font=\small,label=$1/2$] {} (3); +\path[draw,thick,->] (3) edge [bend left=40] node[font=\small,label=$1/2$] {} (4); +\path[draw,thick,->] (4) edge [bend left=40] node[font=\small,label=$1/2$] {} (5); + +\path[draw,thick,->] (5) edge [bend left=40] node[font=\small,label=below:$1$] {} (4); +\path[draw,thick,->] (4) edge [bend left=40] node[font=\small,label=below:$1/2$] {} (3); +\path[draw,thick,->] (3) edge [bend left=40] node[font=\small,label=below:$1/2$] {} (2); +\path[draw,thick,->] (2) edge [bend left=40] node[font=\small,label=below:$1/2$] {} (1); + +%\path[draw,thick,->] (1) edge [bend left=40] node[font=\small,label=below:$1$] {} (2); +\end{tikzpicture} +\end{center} + +Markovin ketjun tilajakauma on vektori +$[p_1,p_2,\ldots,p_n]$, missä $p_k$ tarkoittaa +todennäköisyyttä olla tällä hetkellä tilassa $k$. +Todennäköisyyksille pätee aina $p_1+p_2+\cdots+p_n=1$. + +Esimerkissä jakauma on ensin $[1,0,0,0,0]$, +koska on varmaa, että kulku alkaa kerroksesta 1. +Seuraava jakauma on $[0,1,0,0,0]$, +koska kerroksesta 1 pääsee vain kerrokseen 2. +Tämän jälkeen on mahdollisuus mennä joko ylöspäin +tai alaspäin, joten seuraava jakauma on $[1/2,0,1/2,0,0]$, jne. + +Tehokas tapa simuloida kulkua Markovin ketjussa +on käyttää dynaamista ohjelmointia. +Ideana on pitää yllä tilajakaumaa +ja käydä joka vuorolla läpi kaikki tilat +ja jokaisesta tilasta kaikki mahdollisuudet jatkaa eteenpäin. +Tätä menetelmää käyttäen $m$ askeleen simulointi +vie aikaa $O(n^2 m)$. + +Markovin ketjun tilasiirtymät voi esittää myös matriisina, +jonka avulla voi päivittää tilajakaumaa askeleen eteenpäin. +Tässä tapauksessa matriisi on + +\[ + \begin{bmatrix} + 0 & 1/2 & 0 & 0 & 0 \\ + 1 & 0 & 1/2 & 0 & 0 \\ + 0 & 1/2 & 0 & 1/2 & 0 \\ + 0 & 0 & 1/2 & 0 & 1 \\ + 0 & 0 & 0 & 1/2 & 0 \\ + \end{bmatrix}. +\] + +Tällaisella matriisilla voi kertoa tilajakaumaa esittävän +vektorin, jolloin saadaan tilajakauma yhtä askelta myöhemmin. +Esimerkiksi jakaumasta $[1,0,0,0,0]$ pääsee jakaumaan +$[0,1,0,0,0]$ seuraavasti: + +\[ + \begin{bmatrix} + 0 & 1/2 & 0 & 0 & 0 \\ + 1 & 0 & 1/2 & 0 & 0 \\ + 0 & 1/2 & 0 & 1/2 & 0 \\ + 0 & 0 & 1/2 & 0 & 1 \\ + 0 & 0 & 0 & 1/2 & 0 \\ + \end{bmatrix} + \begin{bmatrix} + 1 \\ + 0 \\ + 0 \\ + 0 \\ + 0 \\ + \end{bmatrix} += + \begin{bmatrix} + 0 \\ + 1 \\ + 0 \\ + 0 \\ + 0 \\ + \end{bmatrix}. +\] + +Matriisiin voi soveltaa edelleen tehokasta +matriisipotenssia, jonka avulla voi laskea +ajassa $O(n^3 \log m)$, +mikä on jakauma $m$ askeleen jälkeen. + +\section{Satunnaisalgoritmit} + +\index{satunnaisalgoritmi@satunnaisalgoritmi} + +Joskus tehtävässä voi hyödyntää satunnaisuutta, +vaikka tehtävä ei itsessään liittyisi todennäköisyyteen. +\key{Satunnaisalgoritmi} on algoritmi, jonka toiminta +perustuu satunnaisuuteen. + +\index{Monte Carlo -algoritmi} + +\key{Monte Carlo -algoritmi} on satunnaisalgoritmi, +joka saattaa tuottaa joskus väärän tuloksen. +Jotta algoritmi olisi käyttökelpoinen, +väärän vastauksen todennäköisyyden tulee olla pieni. + +\index{Las Vegas -algoritmi} + +\key{Las Vegas -algoritmi} on satunnaisalgoritmi, +joka tuottaa aina oikean tuloksen mutta jonka +suoritusaika vaihtelee satunnaisesti. +Tavoitteena on, että algoritmi toimisi nopeasti +suurella todennäköisyydellä. + +Tutustumme seuraavaksi kolmeen esimerkkitehtävään, +jotka voi ratkaista satunnaisuuden avulla. + +\subsubsection{Järjestystunnusluku} + +\index{järjestystunnusluku} + +Taulukon $k$. \key{järjestystunnusluku} +on kohdassa $k$ oleva alkio, +kun alkiot järjestetään +pienimmästä suurimpaan. +On helppoa laskea mikä tahansa +järjestystunnusluku ajassa $O(n \log n)$ +järjestämällä taulukko, +mutta onko oikeastaan tarpeen järjestää koko taulukkoa? + +Osoittautuu, että järjestystunnusluvun +voi etsiä satunnaisalgoritmilla ilman taulukon +järjestämistä. +Algoritmi on Las Vegas -tyyppinen: +sen aikavaativuus on yleensä $O(n)$, +mutta pahimmassa tapauksessa $O(n^2)$. + +Algoritmi valitsee taulukosta satunnaisen alkion $x$ +ja siirtää $x$:ää pienemmät alkiot +taulukon vasempaan osaan ja loput alkiot +taulukon oikeaan osaan. +Tämä vie aikaa $O(n)$, kun taulukossa on $n$ alkiota. +Oletetaan, että vasemmassa osassa on $a$ +alkiota ja oikeassa osassa on $b$ alkiota. +Nyt jos $a=k-1$, alkio $x$ on haluttu alkio. +Jos $a>k-1$, etsitään rekursiivisesti +vasemmasta osasta, mikä on kohdassa $k$ oleva alkio. +Jos taas $a,>=latex] (2) -- (1); +\path[draw,thick,->,>=latex] (3) edge [bend right=20] (2); +\path[draw,thick,->,>=latex] (4) edge [bend left=20] (2); +\path[draw,thick,->,>=latex] (4) edge [bend left=20] (3); +\path[draw,thick,->,>=latex] (5) edge [bend right=20] (4); +\path[draw,thick,->,>=latex] (6) edge [bend left=20] (5); +\path[draw,thick,->,>=latex] (6) edge [bend left=20] (4); +\path[draw,thick,->,>=latex] (6) edge [bend right=40] (3); +\path[draw,thick,->,>=latex] (7) edge [bend right=20] (6); +\path[draw,thick,->,>=latex] (8) edge [bend right=20] (7); +\path[draw,thick,->,>=latex] (8) edge [bend right=20] (6); +\path[draw,thick,->,>=latex] (8) edge [bend left=20] (4); +\path[draw,thick,->,>=latex] (9) edge [bend left=20] (8); +\path[draw,thick,->,>=latex] (9) edge [bend right=20] (6); +\end{tikzpicture} +\end{center} + +Tämä peli päättyy aina tilaan 1, joka on häviötila, +koska siinä ei voi tehdä mitään siirtoja. +Pelin tilojen $1 \ldots 9$ luokittelu on seuraava: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (1,0) grid (10,1); + +\node at (1.5,0.5) {$H$}; +\node at (2.5,0.5) {$V$}; +\node at (3.5,0.5) {$H$}; +\node at (4.5,0.5) {$V$}; +\node at (5.5,0.5) {$H$}; +\node at (6.5,0.5) {$V$}; +\node at (7.5,0.5) {$H$}; +\node at (8.5,0.5) {$V$}; +\node at (9.5,0.5) {$H$}; + +\footnotesize +\node at (1.5,1.4) {$1$}; +\node at (2.5,1.4) {$2$}; +\node at (3.5,1.4) {$3$}; +\node at (4.5,1.4) {$4$}; +\node at (5.5,1.4) {$5$}; +\node at (6.5,1.4) {$6$}; +\node at (7.5,1.4) {$7$}; +\node at (8.5,1.4) {$8$}; +\node at (9.5,1.4) {$9$}; +\end{tikzpicture} +\end{center} + +Yllättävää kyllä, tässä pelissä kaikki +parilliset tilat ovat voittotiloja ja +kaikki parittomat tilat ovat häviötiloja. + +\section{Nim-peli} + +\index{nim-peli} + +\key{Nim-peli} on yksinkertainen peli, +joka on tärkeässä asemassa peliteoriassa, +koska monia pelejä voi pelata samalla +strategialla kuin nim-peliä. +Tutustumme aluksi nim-peliin ja yleistämme +strategian sitten muihin peleihin. + +Nim-pelissä on $n$ kasaa tikkuja, +joista kussakin on tietty määrä tikkuja. +Pelaajat poistavat kasoista tikkuja vuorotellen. +Joka vuorolla pelaaja valitsee yhden kasan, +jossa on vielä tikkuja, +ja poistaa siitä minkä tahansa määrän tikkuja. +Pelin voittaa se, joka poistaa viimeisen tikun. + +Nim-pelin tila on muotoa $[x_1,x_2,\ldots,x_n]$, +jossa $x_k$ on tikkujen määrä kasassa $k$. +Esimerkiksi $[10,12,5]$ tarkoittaa peliä, +jossa on kolme kasaa ja tikkujen määrät ovat 10, 12 ja 5. +Tila $[0,0,\ldots,0]$ on häviötila, +koska siitä ei voi poistaa mitään tikkua, +ja peli päättyy aina tähän tilaan. + +\subsubsection{Analyysi} +\index{nim-summa} +Osoittautuu, että nim-pelin tilan luonteen +kertoo \key{nim-summa} $x_1 \oplus x_2 \oplus \cdots \oplus x_n$, +missä $\oplus$ tarkoittaa xor-operaatiota. +Jos nim-summa on 0, tila on häviötila, +ja muussa tapauksessa tila on voittotila. +Esimerkiksi tilan $[10,12,5]$ nim-summa on +$10 \oplus 12 \oplus 5 = 3$, joten tila on voittotila. + +Mutta miten nim-summa liittyy nim-peliin? +Tämä selviää tutkimalla, miten nim-summa muuttuu, +kun nim-pelin tila muuttuu. + +~\\ +\noindent +\textit{Häviötilat:} +Pelin päätöstila $[0,0,\ldots,0]$ on häviötila, +ja sen nim-summa on 0, kuten kuuluukin. +Muissa häviötiloissa mikä tahansa siirto johtaa +voittotilaan, koska yksi luvuista $x_k$ muuttuu +ja samalla pelin nim-summa muuttuu +eli siirron jälkeen nim-summasta tulee jokin muu kuin 0. + +~\\ +\noindent +\textit{Voittotilat:} +Voittotilasta pääsee häviötilaan muuttamalla +jonkin kasan $k$ tikkujen määräksi $x_k \oplus s$, +missä $s$ on pelin nim-summa. +Vaatimuksena on, että $x_k \oplus s < x_k$, +koska kasasta voi vain poistaa tikkuja. +Sopiva kasa $x_k$ on sellainen, +jossa on ykkösbitti samassa kohdassa kuin +$s$:n vasemmanpuoleisin ykkösbitti. + +~\\ +\noindent +Tarkastellaan esimerkkinä tilaa $[10,2,5]$. +Tämä tila on voittotila, +koska sen nim-summa on 3. +Täytyy siis olla olemassa siirto, +jolla tilasta pääsee häviötilaan. +Selvitetään se seuraavaksi. + +\begin{samepage} +Pelin nim-summa muodostuu seuraavasti: + +\begin{center} +\begin{tabular}{r|r} +10 & \texttt{1010} \\ +12 & \texttt{1100} \\ +5 & \texttt{0101} \\ +\hline +3 & \texttt{0011} \\ +\end{tabular} +\end{center} +\end{samepage} + +Tässä tapauksessa +10 tikun kasa on ainoa, +jonka bittiesityksessä on ykkösbitti +samassa kohdassa kuin +nim-summan vasemmanpuoleisin ykkösbitti: + +\begin{center} +\begin{tabular}{r|r} +10 & \texttt{10\textcircled{1}0} \\ +12 & \texttt{1100} \\ +5 & \texttt{0101} \\ +\hline +3 & \texttt{00\textcircled{1}1} \\ +\end{tabular} +\end{center} + +Kasan uudeksi sisällöksi täytyy saada +$10 \oplus 3 = 9$ tikkua, +mikä onnistuu poistamalla 1 tikku +10 tikun kasasta. +Tämän seurauksena tilaksi tulee $[9,12,5]$, +joka on häviötila, kuten pitääkin: + +\begin{center} +\begin{tabular}{r|r} +9 & \texttt{1001} \\ +12 & \texttt{1100} \\ +5 & \texttt{0101} \\ +\hline +0 & \texttt{0000} \\ +\end{tabular} +\end{center} + +\subsubsection{Misääripeli} + +\index{misxxripeli@misääripeli} + +\key{Misääripelissä} nim-pelin tavoite on käänteinen, +eli pelin häviää se, joka poistaa viimeisen tikun. +Osoittautuu, että misääripeliä pystyy pelaamaan lähes samalla +strategialla kuin tavallista nim-peliä. + +Ideana on pelata misääripeliä aluksi kuin tavallista +nim-peliä, mutta muuttaa strategiaa pelin +lopussa. Käänne tapahtuu silloin, kun seuraavan +siirron seurauksena kaikissa pelin kasoissa olisi 0 tai 1 tikkua. + +Tavallisessa nim-pelissä tulisi nyt tehdä siirto, +jonka jälkeen 1-tikkuisia kasoja on parillinen määrä. +Misääripelissä tulee kuitenkin tehdä siirto, +jonka jälkeen 1-tikkuisia kasoja on pariton määrä. + +Tämä strategia toimii, koska käännekohta tulee aina +vastaan jossakin vaiheessa peliä, +ja kyseinen tila on voittotila, +koska siinä on tarkalleen yksi kasa, +jossa on yli 1 tikkua, +joten nim-summa ei ole 0. + +\section{Sprague–Grundyn lause} + +\index{Sprague–Grundyn lause} + +\key{Sprague–Grundyn lause} yleistää nim-pelin strategian +kaikkiin peleihin, jotka täyttävät +seuraavat vaatimukset: + +\begin{itemize}[noitemsep] +\item Pelissä on kaksi pelaajaa, jotka tekevät vuorotellen siirtoja. +\item Peli muodostuu tiloista ja mahdolliset siirrot tilasta +eivät riipu siitä, kumpi pelaaja on vuorossa. +\item Peli päättyy, kun toinen pelaaja ei voi tehdä siirtoa. +\item Peli päättyy varmasti ennemmin tai myöhemmin. +\item Pelaajien saatavilla on kaikki tieto tiloista +ja siirroista, eikä pelissä ole satunnaisuutta. +\end{itemize} +Ideana on laskea kullekin pelin tilalle Grundy-luku, +joka vastaa tikkujen määrää nim-pelin kasassa. +Kun kaikkien tilojen Grundy-luvut ovat tiedossa, +peliä voi pelata aivan kuin se olisi nim-peli. + +\subsubsection{Grundy-luku} + +\index{Grundy-luku} +\index{mex-funktio} + +Pelin tilan \key{Grundy-luku} määritellään rekursiivisesti +kaavalla +\[\textrm{mex}(\{g_1,g_2,\ldots,g_n\}),\] +jossa $g_1,g_2,\ldots,g_n$ ovat niiden tilojen +Grundy-luvut, joihin tilasta pääsee yhdellä siirrolla, +ja funktio mex antaa pienimmän ei-negatiivisen +luvun, jota ei esiinny joukossa. +Esimerkiksi $\textrm{mex}(\{0,1,3\})=2$. +Jos tilasta ei voi tehdä mitään siirtoa, +sen Grundy-luku on 0, koska $\textrm{mex}(\emptyset)=0$. + +Esimerkiksi tilaverkossa +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {\phantom{0}}; +\node[draw, circle] (2) at (2,0) {\phantom{0}}; +\node[draw, circle] (3) at (4,0) {\phantom{0}}; +\node[draw, circle] (4) at (1,-2) {\phantom{0}}; +\node[draw, circle] (5) at (3,-2) {\phantom{0}}; +\node[draw, circle] (6) at (5,-2) {\phantom{0}}; + +\path[draw,thick,->,>=latex] (2) -- (1); +\path[draw,thick,->,>=latex] (3) -- (2); +\path[draw,thick,->,>=latex] (5) -- (4); +\path[draw,thick,->,>=latex] (6) -- (5); +\path[draw,thick,->,>=latex] (4) -- (1); +\path[draw,thick,->,>=latex] (4) -- (2); +\path[draw,thick,->,>=latex] (5) -- (2); +\path[draw,thick,->,>=latex] (6) -- (2); +\end{tikzpicture} +\end{center} +Grundy-luvut ovat seuraavat: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\node[draw, circle] (1) at (0,0) {0}; +\node[draw, circle] (2) at (2,0) {1}; +\node[draw, circle] (3) at (4,0) {0}; +\node[draw, circle] (4) at (1,-2) {2}; +\node[draw, circle] (5) at (3,-2) {0}; +\node[draw, circle] (6) at (5,-2) {2}; + +\path[draw,thick,->,>=latex] (2) -- (1); +\path[draw,thick,->,>=latex] (3) -- (2); +\path[draw,thick,->,>=latex] (5) -- (4); +\path[draw,thick,->,>=latex] (6) -- (5); +\path[draw,thick,->,>=latex] (4) -- (1); +\path[draw,thick,->,>=latex] (4) -- (2); +\path[draw,thick,->,>=latex] (5) -- (2); +\path[draw,thick,->,>=latex] (6) -- (2); +\end{tikzpicture} +\end{center} +Jos tila on häviötila, sen Grundy-luku on 0. +Jos taas tila on voittotila, sen Grundy-luku +on jokin positiivinen luku. + +Grundy-luvun hyötynä on, +että se vastaa tikkujen määrää nim-kasassa. +Jos Grundy-luku on 0, niin tilasta pääsee vain tiloihin, +joiden Grundy-luku ei ole 0. +Jos taas Grundy-luku on $x>0$, niin tilasta pääsee tiloihin, +joiden Grundy-luvut kattavat välin $0,1,\ldots,x-1$. + +~\\ +\noindent +Tarkastellaan esimerkkinä peliä, +jossa pelaajat siirtävät vuorotellen +pelihahmoa sokkelossa. +Jokainen sokkelon ruutu on lattiaa tai seinää. +Kullakin siirrolla hahmon tulee liikkua jokin +määrä askeleita vasemmalle tai jokin +määrä askeleita ylöspäin. +Pelin voittaja on se, joka tekee viimeisen siirron. + +\begin{samepage} +Esimerkiksi seuraavassa on pelin mahdollinen aloitustilanne, +jossa @ on pelihahmo ja * merkitsee ruutua, johon hahmo voi siirtyä. + +\begin{center} +\begin{tikzpicture}[scale=.65] + \begin{scope} + \fill [color=black] (0, 1) rectangle (1, 2); + \fill [color=black] (0, 3) rectangle (1, 4); + \fill [color=black] (2, 2) rectangle (3, 3); + \fill [color=black] (2, 4) rectangle (3, 5); + \fill [color=black] (4, 3) rectangle (5, 4); + + \draw (0, 0) grid (5, 5); + + \node at (4.5,0.5) {@}; + \node at (3.5,0.5) {*}; + \node at (2.5,0.5) {*}; + \node at (1.5,0.5) {*}; + \node at (0.5,0.5) {*}; + \node at (4.5,1.5) {*}; + \node at (4.5,2.5) {*}; + + \end{scope} +\end{tikzpicture} +\end{center} +\end{samepage} + +Sokkelopelin tiloja ovat kaikki sokkelon +lattiaruudut. Tässä tapauksessa +tilojen Grundy-luvut ovat seuraavat: + +\begin{center} +\begin{tikzpicture}[scale=.65] + \begin{scope} + \fill [color=black] (0, 1) rectangle (1, 2); + \fill [color=black] (0, 3) rectangle (1, 4); + \fill [color=black] (2, 2) rectangle (3, 3); + \fill [color=black] (2, 4) rectangle (3, 5); + \fill [color=black] (4, 3) rectangle (5, 4); + + \draw (0, 0) grid (5, 5); + + \node at (0.5,4.5) {0}; + \node at (1.5,4.5) {1}; + \node at (2.5,4.5) {}; + \node at (3.5,4.5) {0}; + \node at (4.5,4.5) {1}; + + \node at (0.5,3.5) {}; + \node at (1.5,3.5) {0}; + \node at (2.5,3.5) {1}; + \node at (3.5,3.5) {2}; + \node at (4.5,3.5) {}; + + \node at (0.5,2.5) {0}; + \node at (1.5,2.5) {2}; + \node at (2.5,2.5) {}; + \node at (3.5,2.5) {1}; + \node at (4.5,2.5) {0}; + + \node at (0.5,1.5) {}; + \node at (1.5,1.5) {3}; + \node at (2.5,1.5) {0}; + \node at (3.5,1.5) {4}; + \node at (4.5,1.5) {1}; + + \node at (0.5,0.5) {0}; + \node at (1.5,0.5) {4}; + \node at (2.5,0.5) {1}; + \node at (3.5,0.5) {3}; + \node at (4.5,0.5) {2}; + \end{scope} +\end{tikzpicture} +\end{center} + +Tämän seurauksena sokkelopelin +tila käyttäytyy +samalla tavalla kuin nim-pelin kasa. +Esimerkiksi oikean alakulman ruudun +Grundy-luku on 2, +joten kyseessä on voittotila. +Voittoon johtava siirto on joko liikkua neljä +askelta vasemmalle tai kaksi askelta ylöspäin. + +Huomaa, että toisin kuin alkuperäisessä nim-pelissä, +tilasta saattaa päästä toiseen tilaan, +jonka Grundy-luku on suurempi. +Vastustaja voi kuitenkin aina peruuttaa +tällaisen siirron niin, +että Grundy-luku palautuu samaksi. + +\subsubsection{Alipelit} + +Oletetaan seuraavaksi, että peli muodostuu +alipeleistä ja jokaisella vuorolla +pelaaja valitsee jonkin alipeleistä ja +tekee siirron siinä. +Peli päättyy, kun missään alipelissä ei +pysty tekemään siirtoa. + +Nyt pelin tilan Grundy-luku on alipelien +Grundy-lukujen nim-summa. +Peliä pystyy pelaamaan nim-pelin +tapaan selvittämällä kaikkien alipelien Grundy-luvut +ja laskemalla niiden nim-summa. + +~\\ +\noindent +Tarkastellaan esimerkkinä kolmen sokkelon peliä. +Tässä pelissä pelaaja valitsee joka siirrolla +yhden sokkeloista ja siirtää siinä olevaa hahmoa. +Pelin aloitustilanne voi olla seuraavanlainen: + +\begin{center} +\begin{tabular}{ccc} +\begin{tikzpicture}[scale=.55] + \begin{scope} + \fill [color=black] (0, 1) rectangle (1, 2); + \fill [color=black] (0, 3) rectangle (1, 4); + \fill [color=black] (2, 2) rectangle (3, 3); + \fill [color=black] (2, 4) rectangle (3, 5); + \fill [color=black] (4, 3) rectangle (5, 4); + + \draw (0, 0) grid (5, 5); + + \node at (4.5,0.5) {@}; + + \end{scope} +\end{tikzpicture} +& +\begin{tikzpicture}[scale=.55] + \begin{scope} + \fill [color=black] (1, 1) rectangle (2, 3); + \fill [color=black] (2, 3) rectangle (3, 4); + \fill [color=black] (4, 4) rectangle (5, 5); + + \draw (0, 0) grid (5, 5); + + \node at (4.5,0.5) {@}; + + \end{scope} +\end{tikzpicture} +& +\begin{tikzpicture}[scale=.55] + \begin{scope} + \fill [color=black] (1, 1) rectangle (4, 4); + + \draw (0, 0) grid (5, 5); + + \node at (4.5,0.5) {@}; + \end{scope} +\end{tikzpicture} +\end{tabular} +\end{center} + +Sokkeloiden ruutujen Grundy-luvut ovat: + +\begin{center} +\begin{tabular}{ccc} +\begin{tikzpicture}[scale=.55] + \begin{scope} + \fill [color=black] (0, 1) rectangle (1, 2); + \fill [color=black] (0, 3) rectangle (1, 4); + \fill [color=black] (2, 2) rectangle (3, 3); + \fill [color=black] (2, 4) rectangle (3, 5); + \fill [color=black] (4, 3) rectangle (5, 4); + + \draw (0, 0) grid (5, 5); + + \node at (0.5,4.5) {0}; + \node at (1.5,4.5) {1}; + \node at (2.5,4.5) {}; + \node at (3.5,4.5) {0}; + \node at (4.5,4.5) {1}; + + \node at (0.5,3.5) {}; + \node at (1.5,3.5) {0}; + \node at (2.5,3.5) {1}; + \node at (3.5,3.5) {2}; + \node at (4.5,3.5) {}; + + \node at (0.5,2.5) {0}; + \node at (1.5,2.5) {2}; + \node at (2.5,2.5) {}; + \node at (3.5,2.5) {1}; + \node at (4.5,2.5) {0}; + + \node at (0.5,1.5) {}; + \node at (1.5,1.5) {3}; + \node at (2.5,1.5) {0}; + \node at (3.5,1.5) {4}; + \node at (4.5,1.5) {1}; + + \node at (0.5,0.5) {0}; + \node at (1.5,0.5) {4}; + \node at (2.5,0.5) {1}; + \node at (3.5,0.5) {3}; + \node at (4.5,0.5) {2}; + \end{scope} +\end{tikzpicture} +& +\begin{tikzpicture}[scale=.55] + \begin{scope} + \fill [color=black] (1, 1) rectangle (2, 3); + \fill [color=black] (2, 3) rectangle (3, 4); + \fill [color=black] (4, 4) rectangle (5, 5); + + \draw (0, 0) grid (5, 5); + + \node at (0.5,4.5) {0}; + \node at (1.5,4.5) {1}; + \node at (2.5,4.5) {2}; + \node at (3.5,4.5) {3}; + \node at (4.5,4.5) {}; + + \node at (0.5,3.5) {1}; + \node at (1.5,3.5) {0}; + \node at (2.5,3.5) {}; + \node at (3.5,3.5) {0}; + \node at (4.5,3.5) {1}; + + \node at (0.5,2.5) {2}; + \node at (1.5,2.5) {}; + \node at (2.5,2.5) {0}; + \node at (3.5,2.5) {1}; + \node at (4.5,2.5) {2}; + + \node at (0.5,1.5) {3}; + \node at (1.5,1.5) {}; + \node at (2.5,1.5) {1}; + \node at (3.5,1.5) {2}; + \node at (4.5,1.5) {0}; + + \node at (0.5,0.5) {4}; + \node at (1.5,0.5) {0}; + \node at (2.5,0.5) {2}; + \node at (3.5,0.5) {5}; + \node at (4.5,0.5) {3}; + \end{scope} +\end{tikzpicture} +& +\begin{tikzpicture}[scale=.55] + \begin{scope} + \fill [color=black] (1, 1) rectangle (4, 4); + + \draw (0, 0) grid (5, 5); + + \node at (0.5,4.5) {0}; + \node at (1.5,4.5) {1}; + \node at (2.5,4.5) {2}; + \node at (3.5,4.5) {3}; + \node at (4.5,4.5) {4}; + + \node at (0.5,3.5) {1}; + \node at (1.5,3.5) {}; + \node at (2.5,3.5) {}; + \node at (3.5,3.5) {}; + \node at (4.5,3.5) {0}; + + \node at (0.5,2.5) {2}; + \node at (1.5,2.5) {}; + \node at (2.5,2.5) {}; + \node at (3.5,2.5) {}; + \node at (4.5,2.5) {1}; + + \node at (0.5,1.5) {3}; + \node at (1.5,1.5) {}; + \node at (2.5,1.5) {}; + \node at (3.5,1.5) {}; + \node at (4.5,1.5) {2}; + + \node at (0.5,0.5) {4}; + \node at (1.5,0.5) {0}; + \node at (2.5,0.5) {1}; + \node at (3.5,0.5) {2}; + \node at (4.5,0.5) {3}; + \end{scope} +\end{tikzpicture} +\end{tabular} +\end{center} + +Aloitustilanteessa Grundy-lukujen nim-summa on +$2 \oplus 3 \oplus 3 = 2$, joten +aloittaja pystyy voittamaan pelin. +Voittoon johtava siirto on liikkua vasemmassa sokkelossa +2 askelta ylöspäin, jolloin nim-summaksi +tulee $0 \oplus 3 \oplus 3 = 0$. + +\subsubsection{Jakautuminen} + +Joskus siirto pelissä jakaa pelin alipeleihin, +jotka ovat toisistaan riippumattomia. +Tällöin pelin Grundy-luku on + +\[\textrm{mex}(\{g_1, g_2, \ldots, g_n \}),\] +missä $n$ on siirtojen määrä ja +\[g_k = a_{k,1} \oplus a_{k,2} \oplus \ldots \oplus a_{k,m},\] +missä siirron $k$ tuottamien alipelien +Grundy-luvut ovat $a_{k,1},a_{k,2},\ldots,a_{k,m}$. + +\index{Grundyn peli@Grundyn peli} + +Esimerkki tällaisesta pelistä on \key{Grundyn peli}. +Pelin alkutilanteessa on yksittäinen kasa, jossa on $n$ tikkua. +Joka vuorolla pelaaja valitsee jonkin kasan +ja jakaa sen kahdeksi epätyhjäksi kasaksi +niin, että kasoissa on eri määrä tikkuja. +Pelin voittaja on se, joka tekee viimeisen jaon. + +Merkitään $f(n)$ Grundy-lukua kasalle, +jossa on $n$ tikkua. +Grundy-luku muodostuu käymällä läpi tavat +jakaa kasa kahdeksi kasaksi. +Esimerkiksi tapauksessa $n=8$ mahdolliset jakotavat +ovat $1+7$, $2+6$ ja $3+5$, joten +\[f(8)=\textrm{mex}(\{f(1) \oplus f(7), f(2) \oplus f(6), f(3) \oplus f(5)\}).\] + +Tässä pelissä luvun $f(n)$ laskeminen vaatii lukujen +$f(1),\ldots,f(n-1)$ laskemista. +Pohjatapauksina $f(1)=f(2)=0$, koska 1 ja 2 tikun +kasaa ei ole mahdollista jakaa mitenkään. +Ensimmäiset Grundy-luvut ovat: +\[ +\begin{array}{lcl} +f(1) & = & 0 \\ +f(2) & = & 0 \\ +f(3) & = & 1 \\ +f(4) & = & 0 \\ +f(5) & = & 2 \\ +f(6) & = & 1 \\ +f(7) & = & 0 \\ +f(8) & = & 2 \\ +\end{array} +\] +Tapauksen $n=8$ Grundy-luku on 2, joten peli on mahdollista +voittaa. +Voittosiirto on muodostaa kasat $1+7$, +koska $f(1) \oplus f(7) = 0$. + diff --git a/luku26.tex b/luku26.tex new file mode 100644 index 0000000..40c1a9c --- /dev/null +++ b/luku26.tex @@ -0,0 +1,1078 @@ +\chapter{String algorithms} + +\index{merkkijono@merkkijono} +\index{aakkosto@aakkosto} + +Merkkijonon $s$ merkit ovat $s[1],s[2],\ldots,s[n]$, +missä $n$ on merkkijonon pituus. + +\key{Aakkosto} sisältää ne merkit, +joita merkkijonossa voi esiintyä. +Esimerkiksi aakkosto $\{\texttt{A},\texttt{B},\ldots,\texttt{Z}\}$ +sisältää englannin kielen suuret kirjaimet. + +\index{osajono@osajono} + +\key{Osajono} +sisältää merkkijonon merkit +yhtenäiseltä väliltä. +Merkkijonon osa\-jonojen määrä on $n(n+1)/2$. +Esimerkiksi merkkijonon \texttt{ALGORITMI} +yksi osajono on \texttt{ORITM}, +joka muodostuu valitsemalla välin \texttt{ALG\underline{ORITM}I}. + +\index{alijono@alijono} + +\key{Alijono} +on osajoukko merkkijonon merkeistä. +Merkkijonon alijonojen määrä on $2^n-1$. +Esimerkiksi merkkijonon \texttt{ALGORITMI} +yksi alijono on \texttt{LGRMI}, joka muodostuu +valitsemalla merkit \texttt{A\underline{LG}O\underline{R}IT\underline{MI}}. + +\index{alkuosa@alkuosa} +\index{loppuosa@loppuosa} +\index{prefiksi@prefiksi} +\index{suffiksi@suffiksi} + +\key{Alkuosa} on merkkijonon +alusta alkava osajono, +ja \key{loppuosa} on merkkijonon +loppuun päättyvä osajono. +Esimerkiksi merkkijonon \texttt{KISSA} +alkuosat ovat \texttt{K}, \texttt{KI}, +\texttt{KIS}, \texttt{KISS} ja \texttt{KISSA} +ja loppuosat ovat \texttt{A}, \texttt{SA}, +\texttt{SSA}, \texttt{ISSA} ja \texttt{KISSA}. +Alkuosa tai loppuosa on \key{aito}, +jos se ei ole koko merkkijono. + +\index{kierto@kierto} + +\key{Kierto} syntyy +siirtämällä jokin alkuosa merkkijonon loppuun +tai jokin loppuosa merkkijonon alkuun. +Esimerkiksi merkkijonon \texttt{APILA} +kierrot ovat +\texttt{APILA}, +\texttt{PILAA}, +\texttt{ILAAP}, +\texttt{LAAPI} ja +\texttt{AAPIL}. + +\index{jakso@jakso} + +\key{Jakso} on alkuosa, +jota toistamalla merkkijono muodostuu. +Jakson viimeinen toistokerta voi olla osittainen +niin, että siinä on vain jakson alkuosa. +Usein on kiinnostavaa selvittää, mikä on merkkijonon +\key{lyhin jakso}. +Esimerkiksi merkkijonon \texttt{ABCABCA} lyhin jakso on \texttt{ABC}. +Tässä tapauksessa merkkijono syntyy toistamalla jaksoa ensin kahdesti kokonaan +ja sitten kerran osittain. + +\key{Reuna} on +merkkijono, joka on sekä +alkuosa että loppuosa. +Esimerkiksi merkkijonon \texttt{ABADABA} +reunat ovat \texttt{A}, \texttt{ABA} ja +\texttt{ABADABA}. +Usein halutaan etsiä \key{pisin reuna}, +joka ei ole koko merkkijono. + +\index{leksikografinen jxrjestys@leksikografinen järjestys} + +Merkkijonojen vertailussa käytössä on yleensä +\key{leksikografinen järjestys}, joka vastaa aakkosjärjestystä. +Siinä $x] (1) -- node[font=\small,label=\texttt{A}] {} (2); +\path[draw,thick,->] (1) -- node[font=\small,label=\texttt{S}] {} (3); +\path[draw,thick,->] (2) -- node[font=\small,label=left:\texttt{P}] {} (4); +\path[draw,thick,->] (4) -- node[font=\small,label=left:\texttt{I}] {} (5); +\path[draw,thick,->] (5) -- node[font=\small,label=left:\texttt{L}] {} (6); +\path[draw,thick,->] (5) -- node[font=\small,label=right:\texttt{N}] {} (7); +\path[draw,thick,->] (6) -- node[font=\small,label=left:\texttt{A}] {}(8); +\path[draw,thick,->] (7) -- node[font=\small,label=right:\texttt{A}] {} (9); +\path[draw,thick,->] (3) -- node[font=\small,label=right:\texttt{U}] {} (10); +\path[draw,thick,->] (10) -- node[font=\small,label=right:\texttt{U}] {} (11); +\path[draw,thick,->] (11) -- node[font=\small,label=right:\texttt{R}] {} (12); +\path[draw,thick,->] (12) -- node[font=\small,label=right:\texttt{I}] {} (13); +\end{tikzpicture} +\end{center} +Merkki * solmussa tarkoittaa, +että jokin merkkijono päättyy kyseiseen solmuun. +Tämä merkki on tarpeen, +koska merkkijono voi olla toisen merkkijonon alkuosa, +kuten tässä puussa merkkijono \texttt{SUU} +on merkkijonon \texttt{SUURI} alkuosa. + +Triessä merkkijonon lisääminen ja hakeminen +vievät aikaa $O(n)$, kun $n$ on merkkijonon pituus. +Molemmat operaatiot voi toteuttaa lähtemällä liikkeelle juuresta +ja kulkemalla alaspäin ketjua merkkien mukaisesti. +Tarvittaessa puuhun lisätään uusia solmuja. + +Triestä on mahdollista etsiä +sekä merkkijonoja että merkkijonojen alkuosia. +Lisäksi puun solmuissa voi pitää kirjaa, +monessako merkkijonossa on solmua vastaava alkuosa, +mikä lisää trien käyttömahdollisuuksia. + +Trie on kätevää tallentaa taulukkona +\begin{lstlisting} +int t[N][A]; +\end{lstlisting} +missä $N$ on solmujen suurin mahdollinen määrä +(eli tallennettavien merkkijonojen yhteispituus) +ja $A$ on aakkoston koko. +Trien solmut numeroidaan $1,2,3,\ldots$ niin, +että juuren numero on 1, +ja taulukon kohta $\texttt{t}[s][c]$ kertoo, +mihin solmuun solmusta $s$ pääsee merkillä $c$. + +\section{Merkkijonohajautus} + +\index{hajautus@hajautus} +\index{merkkijonohajautus@merkkijonohajautus} + +\key{Merkkijonohajautus} +on tekniikka, jonka avulla voi esikäsittelyn +jälkeen tarkastaa tehokkaasti, ovatko +kaksi merkkijonon osajonoa samat. +Ideana on verrata toisiinsa +osajonojen hajautusarvoja, +mikä on tehokkaampaa kuin osajonojen +vertaaminen merkki kerrallaan. + +\subsubsection*{Hajautusarvon laskeminen} + +\index{hajautusarvo@hajautusarvo} +\index{polynominen hajautus@polynominen hajautus} + +Merkkijonon \key{hajautusarvo} +on luku, joka lasketaan merkkijonon merkeistä +etukäteen valitulla tavalla. +Jos kaksi merkkijonoa ovat samat, +myös niiden hajautusarvot ovat samat, +minkä ansiosta merkkijonoja voi vertailla +niiden hajautusarvojen kautta. + +Tavallinen tapa toteuttaa merkkijonohajautus +on käyttää polynomista hajautusta. +Siinä hajautusarvo lasketaan kaavalla +\[(c[1] A^{n-1} + c[2] A^{n-2} + \cdots + c[n] A^0) \bmod B ,\] +missä merkkijonon merkkien koodit ovat +$c[1],c[2],\ldots,c[n]$ ja $A$ ja $B$ ovat etukäteen +valitut vakiot. + +Esimerkiksi merkkijonon \texttt{KISSA} merkkien koodit ovat: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (5,2); + +\node at (0.5, 1.5) {\texttt{K}}; +\node at (1.5, 1.5) {\texttt{I}}; +\node at (2.5, 1.5) {\texttt{S}}; +\node at (3.5, 1.5) {\texttt{S}}; +\node at (4.5, 1.5) {\texttt{A}}; + +\node at (0.5, 0.5) {75}; +\node at (1.5, 0.5) {73}; +\node at (2.5, 0.5) {83}; +\node at (3.5, 0.5) {83}; +\node at (4.5, 0.5) {65}; + +\end{tikzpicture} +\end{center} + +Jos $A=3$ ja $B=97$, merkkijonon \texttt{KISSA} hajautusarvoksi tulee + +\[(75 \cdot 3^4 + 73 \cdot 3^3 + 83 \cdot 3^2 + 83 \cdot 3^1 + 65 \cdot 3^0) \bmod 97 = 59.\] + +\subsubsection*{Esikäsittely} + +Merkkijonohajautuksen esikäsittely +muodostaa tietoa, jonka avulla +voi laskea tehokkaasti merkkijonon +osajonojen hajautusarvoja. +Osoittautuu, että polynomisessa hajautuksessa +$O(n)$-aikaisen esikäsittelyn jälkeen voi laskea +minkä tahansa osajonon hajautusarvon +ajassa $O(1)$. + +Ideana on muodostaa taulukko $h$, +jossa $h[k]$ on hajautusarvo merkkijonon +alkuosalle kohtaan $k$ asti. +Taulukon voi muodostaa rekursiolla seuraavasti: +\[ +\begin{array}{lcl} +h[0] & = & 0 \\ +h[k] & = & (h[k-1] A + c[k]) \bmod B \\ +\end{array} +\] +Lisäksi muodostetaan taulukko $p$, +jossa $p[k]=A^k \bmod B$: +\[ +\begin{array}{lcl} +p[0] & = & 1 \\ +p[k] & = & (p[k-1] A) \bmod B. \\ +\end{array} +\] +Näiden taulukoiden muodostaminen vie aikaa $O(n)$. +Tämän jälkeen hajautusarvo merkkijonon osajonolle, +joka alkaa kohdasta $a$ ja päättyy kohtaan $b$, +voidaan laskea $O(1)$-ajassa kaavalla +\[(h[b]-h[a-1] p[b-a+1]) \bmod B.\] + +\subsubsection*{Hajautuksen käyttö} + +Hajautusarvot tarjoavat nopean tavan merkkijonojen +vertailemiseen. +Ideana on vertailla merkkijonojen koko sisällön +sijasta niiden hajautusarvoja. +Jos hajautusarvot ovat samat, +myös merkkijonot ovat \textit{todennäköisesti} samat, +ja jos taas hajautusarvot eivät ole samat, +merkkijonot eivät \textit{varmasti} ole samat. + +Hajautuksen avulla voi usein tehostaa +raa'an voiman algoritmia niin, että siitä tulee tehokas. +Tarkastellaan esimerkkinä +raa'an voiman algoritmia, joka laskee, +montako kertaa merkkijono $p$ +esiintyy osajonona merkkijonossa $s$. +Algoritmi käy läpi kaikki kohdat, +joissa $p$ voi esiintyä, +ja vertailee merkkijonoja merkki merkiltä. +Tällaisen algoritmin aikavaativuus on $O(n^2)$. + +Voimme kuitenkin tehostaa algoritmia hajautuksen avulla, +koska algoritmissa vertaillaan merkkijonojen osajonoja. +Hajautusta käyttäen kukin vertailu vie aikaa vain $O(1)$, +koska vertailua ei tehdä merkki merkiltä +vaan suoraan hajautusarvon perusteella. +Tuloksena on algoritmi, jonka aikavaativuus on $O(n)$, +joka on paras mahdollinen aikavaativuus tehtävään. + +Yhdistämällä hajautus ja \emph{binäärihaku} on mahdollista +myös selvittää logaritmisessa ajassa, +kumpi kahdesta osajonosta on suurempi +aakkosjärjestyksessä. +Tämä onnistuu tutkimalla ensin binäärihaulla, +kuinka pitkä on merkkijonojen yhteinen alkuosa, +minkä jälkeen yhteisen alkuosan jälkeinen merkki +kertoo, kumpi merkkijono on suurempi. + +\subsubsection*{Törmäykset ja parametrit} + +\index{tzzmxys@törmäys} + +Ilmeinen riski hajautusarvojen vertailussa +on \key{törmäys}, joka tarkoittaa, että kahdessa merkkijonossa on +eri sisältö mutta niiden hajautusarvot ovat samat. +Tällöin hajautusarvojen perusteella merkkijonot +näyttävät samalta, vaikka todellisuudessa ne eivät ole samat, +ja algoritmi voi toimia väärin. + +Törmäyksen riski on aina olemassa, +koska erilaisia merkkijonoja on enemmän kuin +erilaisia hajautusarvoja. +Riskin saa kuitenkin pieneksi valitsemalla +hajautuksen vakiot $A$ ja $B$ huolellisesti. +Vakioiden valinnassa on kaksi tavoitetta: +hajautusarvojen tulisi +jakautua tasaisesti merkkijonoille +ja +erilaisten hajautusarvojen määrän tulisi +olla riittävän suuri. + +Hyvä ratkaisu on valita vakioiksi suuria +satunnaislukuja. Tavallinen tapa on valita vakiot +läheltä lukua $10^9$, esimerkiksi +\[ +\begin{array}{lcl} +A & = & 911382323 \\ +B & = & 972663749 \\ +\end{array} +\] +Tällainen valinta takaa sen, +että hajautusarvot jakautuvat +riittävän tasaisesti välille $0 \ldots B-1$. +Suuruusluokan $10^9$ etuna on, +että \texttt{long long} -tyyppi riittää +hajautusarvojen käsittelyyn koodissa, +koska tulot $AB$ ja $BB$ mahtuvat \texttt{long long} -tyyppiin. +Mutta onko $10^9$ riittävä määrä hajautusarvoja? + +Tarkastellaan nyt kolmea hajautuksen käyttötapaa: + +\textit{Tapaus 1:} Merkkijonoja $x$ ja $y$ verrataan toisiinsa. +Törmäyksen todennäköisyys on $1/B$ olettaen, +että kaikki hajautusarvot esiintyvät yhtä usein. + +\textit{Tapaus 2:} Merkkijonoa $x$ verrataan merkkijonoihin +$y_1,y_2,\ldots,y_n$. +Yhden tai useamman törmäyksen todennäköisyys on + +\[1-(1-1/B)^n.\] + +\textit{Tapaus 3:} Merkkijonoja $x_1,x_2,\ldots,x_n$ +verrataan kaikkia keskenään. +Yhden tai useamman törmäyksen todennäköisyys on +\[ 1 - \frac{B \cdot (B-1) \cdot (B-2) \cdots (B-n+1)}{B^n}.\] + +Seuraava taulukko sisältää törmäyksen todennäköisyydet, +kun vakion $B$ arvo vaihtelee ja $n=10^6$: + +\begin{center} +\begin{tabular}{rrrr} +vakio $B$ & tapaus 1 & tapaus 2 & tapaus 3 \\ +\hline +$10^3$ & $0.001000$ & $1.000000$ & $1.000000$ \\ +$10^6$ & $0.000001$ & $0.632121$ & $1.000000$ \\ +$10^9$ & $0.000000$ & $0.001000$ & $1.000000$ \\ +$10^{12}$ & $0.000000$ & $0.000000$ & $0.393469$ \\ +$10^{15}$ & $0.000000$ & $0.000000$ & $0.000500$ \\ +$10^{18}$ & $0.000000$ & $0.000000$ & $0.000001$ \\ +\end{tabular} +\end{center} + +Taulukosta näkee, että tapauksessa 1 +törmäyksen riski on olematon +valinnalla $B \approx 10^9$. +Tapauksessa 2 riski on olemassa, mutta se on silti edelleen vähäinen. +Tapauksessa 3 tilanne on kuitenkin täysin toinen: +törmäys tapahtuu käytännössä varmasti +vielä valinnalla $B \approx 10^9$. + +\index{syntymxpxivxparadoksi@syntymäpäiväparadoksi} + +Tapauksen 3 ilmiö tunnetaan nimellä +\key{syntymäpäiväparadoksi}: +jos huoneessa on $n$ henkilöä, on suuri +todennäköisyys, että jollain kahdella +henkilöllä on sama syntymäpäivä, vaikka +$n$ olisi melko pieni. +Vastaavasti hajautuksessa kun kaikkia +hajautusarvoja verrataan keskenään, +käy helposti niin, että jotkin +kaksi ovat sattumalta samoja. + +Hyvä tapa pienentää törmäyksen riskiä on laskea +\emph{useita} hajautusarvoja eri parametreilla +ja verrata niitä kaikkia. +On hyvin pieni todennäköisyys, +että törmäys tapahtuisi samaan aikaan +kaikissa hajautusarvoissa. +Esimerkiksi kaksi hajautusarvoa parametrilla +$B \approx 10^9$ vastaa yhtä hajautusarvoa +parametrilla $B \approx 10^{18}$, +mikä takaa hyvän suojan törmäyksiltä. + +Jotkut käyttävät hajautuksessa vakioita $B=2^{32}$ tai $B=2^{64}$, +jolloin modulo $B$ tulee laskettua +automaattisesti, kun muuttujan arvo pyörähtää ympäri. +Tämä ei ole kuitenkaan hyvä valinta, +koska muotoa $2^x$ olevaa moduloa vastaan +pystyy tekemään testisyötteen, joka aiheuttaa varmasti törmäyksen\footnote{ +J. Pachocki ja Jakub Radoszweski: +''Where to use and how not to use polynomial string hashing''. +\textit{Olympiads in Informatics}, 2013. +}. + +\section{Z-algoritmi} + +\index{Z-algoritmi} +\index{Z-taulukko} + +\key{Z-algoritmi} muodostaa merkkijonosta \key{Z-taulukon}, +joka kertoo kullekin merkkijonon kohdalle, +mikä on pisin kyseisestä kohdasta alkava osajono, +joka on myös merkkijonon alkuosa. +Z-algoritmin avulla voi ratkaista tehokkaasti +monia merkkijonotehtäviä. + +Z-algoritmi ja merkkijonohajautus ovat usein +vaihtoehtoisia tekniikoita, ja on makuasia, +kumpaa algoritmia käyttää. +Toisin kuin hajautus, Z-algoritmi toimii +varmasti oikein eikä siinä ole törmäysten riskiä. +Toisaalta Z-algoritmi on vaikeampi toteuttaa eikä +se sovellu kaikkeen samaan kuin hajautus. + +\subsubsection*{Algoritmin toiminta} + +Z-algoritmi muodostaa merkkijonolle Z-taulukon, +jonka jokaisessa kohdassa lukee, +kuinka pitkälle kohdasta +alkava osajono vastaa merkkijonon alkuosaa. +Esimerkiksi Z-taulukko +merkkijonolle \texttt{ACBACDACBACBACDA} on seuraava: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (16,2); + +\node at (0.5, 1.5) {\texttt{A}}; +\node at (1.5, 1.5) {\texttt{C}}; +\node at (2.5, 1.5) {\texttt{B}}; +\node at (3.5, 1.5) {\texttt{A}}; +\node at (4.5, 1.5) {\texttt{C}}; +\node at (5.5, 1.5) {\texttt{D}}; +\node at (6.5, 1.5) {\texttt{A}}; +\node at (7.5, 1.5) {\texttt{C}}; +\node at (8.5, 1.5) {\texttt{B}}; +\node at (9.5, 1.5) {\texttt{A}}; +\node at (10.5, 1.5) {\texttt{C}}; +\node at (11.5, 1.5) {\texttt{B}}; +\node at (12.5, 1.5) {\texttt{A}}; +\node at (13.5, 1.5) {\texttt{C}}; +\node at (14.5, 1.5) {\texttt{D}}; +\node at (15.5, 1.5) {\texttt{A}}; + +\node at (0.5, 0.5) {--}; +\node at (1.5, 0.5) {0}; +\node at (2.5, 0.5) {0}; +\node at (3.5, 0.5) {2}; +\node at (4.5, 0.5) {0}; +\node at (5.5, 0.5) {0}; +\node at (6.5, 0.5) {5}; +\node at (7.5, 0.5) {0}; +\node at (8.5, 0.5) {0}; +\node at (9.5, 0.5) {7}; +\node at (10.5, 0.5) {0}; +\node at (11.5, 0.5) {0}; +\node at (12.5, 0.5) {2}; +\node at (13.5, 0.5) {0}; +\node at (14.5, 0.5) {0}; +\node at (15.5, 0.5) {1}; + +\footnotesize +\node at (0.5, 2.5) {1}; +\node at (1.5, 2.5) {2}; +\node at (2.5, 2.5) {3}; +\node at (3.5, 2.5) {4}; +\node at (4.5, 2.5) {5}; +\node at (5.5, 2.5) {6}; +\node at (6.5, 2.5) {7}; +\node at (7.5, 2.5) {8}; +\node at (8.5, 2.5) {9}; +\node at (9.5, 2.5) {10}; +\node at (10.5, 2.5) {11}; +\node at (11.5, 2.5) {12}; +\node at (12.5, 2.5) {13}; +\node at (13.5, 2.5) {14}; +\node at (14.5, 2.5) {15}; +\node at (15.5, 2.5) {16}; + +\end{tikzpicture} +\end{center} + +Esimerkiksi kohdassa 7 on arvo 5, +koska siitä alkava 5-merkkinen osajono +\texttt{ACBAC} on merkkijonon alkuosa, +mutta 6-merkkinen osajono \texttt{ACBACB} +ei ole enää merkkijonon alkuosa. + +Z-algoritmi käy läpi merkkijonon +vasemmalta oikealle ja laskee +jokaisessa kohdassa, +kuinka pitkälle kyseisestä kohdasta alkava +osajono täsmää merkkijonon alkuun. +Algoritmi laskee yhteisen +alkuosan pituuden vertaamalla +merkkijonon alkua ja osajonon alkua toisiinsa. + +Suoraviivaisesti toteutettuna +tällaisen algoritmin aikavaativuus olisi $O(n^2)$, +koska yhteiset alkuosat voivat olla pitkiä. +Z-algoritmissa on kuitenkin yksi tärkeä +optimointi, jonka ansiosta algoritmin +aikavaativuus on vain $O(n)$. + +Ideana on pitää muistissa väliä $[x,y]$, +joka on aiemmin laskettu merkkijonon +alkuun täsmäävä väli, jossa $y$ on +mahdollisimman suuri. +Tällä välillä olevia +merkkejä ei tarvitse koskaan +verrata uudestaan +merkkijonon alkuun, vaan niitä koskevan +tiedon saa suoraan Z-taulukon lasketusta osasta. + +Z-algoritmin aikavaativuus on $O(n)$, +koska algoritmi aloittaa merkki kerrallaan +vertailemisen vasta kohdasta $y+1$. +Jos merkit täsmäävät, kohta $y$ +siirtyy eteenpäin +eikä algoritmin tarvitse enää +koskaan vertailla tätä kohtaa, +vaan algoritmi pystyy hyödyntämään +Z-taulukon alussa olevaa tietoa. + +\subsubsection*{Esimerkki} + +Katsotaan nyt, miten Z-algoritmi muodostaa +seuraavan Z-taulukon: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (16,2); + +\node at (0.5, 1.5) {A}; +\node at (1.5, 1.5) {C}; +\node at (2.5, 1.5) {B}; +\node at (3.5, 1.5) {A}; +\node at (4.5, 1.5) {C}; +\node at (5.5, 1.5) {D}; +\node at (6.5, 1.5) {A}; +\node at (7.5, 1.5) {C}; +\node at (8.5, 1.5) {B}; +\node at (9.5, 1.5) {A}; +\node at (10.5, 1.5) {C}; +\node at (11.5, 1.5) {B}; +\node at (12.5, 1.5) {A}; +\node at (13.5, 1.5) {C}; +\node at (14.5, 1.5) {D}; +\node at (15.5, 1.5) {A}; + +\node at (0.5, 0.5) {--}; +\node at (1.5, 0.5) {?}; +\node at (2.5, 0.5) {?}; +\node at (3.5, 0.5) {?}; +\node at (4.5, 0.5) {?}; +\node at (5.5, 0.5) {?}; +\node at (6.5, 0.5) {?}; +\node at (7.5, 0.5) {?}; +\node at (8.5, 0.5) {?}; +\node at (9.5, 0.5) {?}; +\node at (10.5, 0.5) {?}; +\node at (11.5, 0.5) {?}; +\node at (12.5, 0.5) {?}; +\node at (13.5, 0.5) {?}; +\node at (14.5, 0.5) {?}; +\node at (15.5, 0.5) {?}; + +\footnotesize +\node at (0.5, 2.5) {1}; +\node at (1.5, 2.5) {2}; +\node at (2.5, 2.5) {3}; +\node at (3.5, 2.5) {4}; +\node at (4.5, 2.5) {5}; +\node at (5.5, 2.5) {6}; +\node at (6.5, 2.5) {7}; +\node at (7.5, 2.5) {8}; +\node at (8.5, 2.5) {9}; +\node at (9.5, 2.5) {10}; +\node at (10.5, 2.5) {11}; +\node at (11.5, 2.5) {12}; +\node at (12.5, 2.5) {13}; +\node at (13.5, 2.5) {14}; +\node at (14.5, 2.5) {15}; +\node at (15.5, 2.5) {16}; + +\end{tikzpicture} +\end{center} + +Ensimmäinen mielenkiintoinen kohta tulee, +kun yhteisen alkuosan pituus on 5. +Silloin algoritmi laittaa muistiin +välin $[7,11]$ seuraavasti: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (6,0) rectangle (7,1); +\draw (0,0) grid (16,2); + +\node at (0.5, 1.5) {A}; +\node at (1.5, 1.5) {C}; +\node at (2.5, 1.5) {B}; +\node at (3.5, 1.5) {A}; +\node at (4.5, 1.5) {C}; +\node at (5.5, 1.5) {D}; +\node at (6.5, 1.5) {A}; +\node at (7.5, 1.5) {C}; +\node at (8.5, 1.5) {B}; +\node at (9.5, 1.5) {A}; +\node at (10.5, 1.5) {C}; +\node at (11.5, 1.5) {B}; +\node at (12.5, 1.5) {A}; +\node at (13.5, 1.5) {C}; +\node at (14.5, 1.5) {D}; +\node at (15.5, 1.5) {A}; + +\node at (0.5, 0.5) {--}; +\node at (1.5, 0.5) {0}; +\node at (2.5, 0.5) {0}; +\node at (3.5, 0.5) {2}; +\node at (4.5, 0.5) {0}; +\node at (5.5, 0.5) {0}; +\node at (6.5, 0.5) {5}; +\node at (7.5, 0.5) {?}; +\node at (8.5, 0.5) {?}; +\node at (9.5, 0.5) {?}; +\node at (10.5, 0.5) {?}; +\node at (11.5, 0.5) {?}; +\node at (12.5, 0.5) {?}; +\node at (13.5, 0.5) {?}; +\node at (14.5, 0.5) {?}; +\node at (15.5, 0.5) {?}; + +\draw [decoration={brace}, decorate, line width=0.5mm] (6,3.00) -- (11,3.00); + +\node at (6.5,3.50) {$x$}; +\node at (10.5,3.50) {$y$}; + + +\footnotesize +\node at (0.5, 2.5) {1}; +\node at (1.5, 2.5) {2}; +\node at (2.5, 2.5) {3}; +\node at (3.5, 2.5) {4}; +\node at (4.5, 2.5) {5}; +\node at (5.5, 2.5) {6}; +\node at (6.5, 2.5) {7}; +\node at (7.5, 2.5) {8}; +\node at (8.5, 2.5) {9}; +\node at (9.5, 2.5) {10}; +\node at (10.5, 2.5) {11}; +\node at (11.5, 2.5) {12}; +\node at (12.5, 2.5) {13}; +\node at (13.5, 2.5) {14}; +\node at (14.5, 2.5) {15}; +\node at (15.5, 2.5) {16}; + +\end{tikzpicture} +\end{center} + +Välin $[7,11]$ hyötynä on, että algoritmi +voi sen avulla laskea seuraavat +Z-taulukon arvot nopeammin. +Koska välin $[7,11]$ merkit ovat samat +kuin merkkijonon alussa, +myös Z-taulukon arvoissa on vastaavuutta. + +Ensinnäkin kohdissa 8 ja 9 +tulee olla samat arvot kuin +kohdissa 2 ja 3, +koska väli $[7,11]$ +vastaa väliä $[1,5]$: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (7,0) rectangle (9,1); +\draw (0,0) grid (16,2); + +\node at (0.5, 1.5) {A}; +\node at (1.5, 1.5) {C}; +\node at (2.5, 1.5) {B}; +\node at (3.5, 1.5) {A}; +\node at (4.5, 1.5) {C}; +\node at (5.5, 1.5) {D}; +\node at (6.5, 1.5) {A}; +\node at (7.5, 1.5) {C}; +\node at (8.5, 1.5) {B}; +\node at (9.5, 1.5) {A}; +\node at (10.5, 1.5) {C}; +\node at (11.5, 1.5) {B}; +\node at (12.5, 1.5) {A}; +\node at (13.5, 1.5) {C}; +\node at (14.5, 1.5) {D}; +\node at (15.5, 1.5) {A}; + +\node at (0.5, 0.5) {--}; +\node at (1.5, 0.5) {0}; +\node at (2.5, 0.5) {0}; +\node at (3.5, 0.5) {2}; +\node at (4.5, 0.5) {0}; +\node at (5.5, 0.5) {0}; +\node at (6.5, 0.5) {5}; +\node at (7.5, 0.5) {0}; +\node at (8.5, 0.5) {0}; +\node at (9.5, 0.5) {?}; +\node at (10.5, 0.5) {?}; +\node at (11.5, 0.5) {?}; +\node at (12.5, 0.5) {?}; +\node at (13.5, 0.5) {?}; +\node at (14.5, 0.5) {?}; +\node at (15.5, 0.5) {?}; + + +\draw [decoration={brace}, decorate, line width=0.5mm] (6,3.00) -- (11,3.00); + +\node at (6.5,3.50) {$x$}; +\node at (10.5,3.50) {$y$}; + + +\footnotesize +\node at (0.5, 2.5) {1}; +\node at (1.5, 2.5) {2}; +\node at (2.5, 2.5) {3}; +\node at (3.5, 2.5) {4}; +\node at (4.5, 2.5) {5}; +\node at (5.5, 2.5) {6}; +\node at (6.5, 2.5) {7}; +\node at (7.5, 2.5) {8}; +\node at (8.5, 2.5) {9}; +\node at (9.5, 2.5) {10}; +\node at (10.5, 2.5) {11}; +\node at (11.5, 2.5) {12}; +\node at (12.5, 2.5) {13}; +\node at (13.5, 2.5) {14}; +\node at (14.5, 2.5) {15}; +\node at (15.5, 2.5) {16}; + + +\draw[thick,<->] (7.5,-0.25) .. controls (7,-1.25) and (2,-1.25) .. (1.5,-0.25); +\draw[thick,<->] (8.5,-0.25) .. controls (8,-1.25) and (3,-1.25) .. (2.5,-0.25); +\end{tikzpicture} +\end{center} + +Seuraavaksi kohdasta 4 saa tietoa kohdan +10 arvon laskemiseksi. +Koska kohdassa 4 on arvo 2, +tämä tarkoittaa, että osajono +täsmää kohtaan $y=11$ asti, +mutta sen jälkeen on tutkimatonta +aluetta merkkijonossa. + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (9,0) rectangle (10,1); +\draw (0,0) grid (16,2); + +\node at (0.5, 1.5) {A}; +\node at (1.5, 1.5) {C}; +\node at (2.5, 1.5) {B}; +\node at (3.5, 1.5) {A}; +\node at (4.5, 1.5) {C}; +\node at (5.5, 1.5) {D}; +\node at (6.5, 1.5) {A}; +\node at (7.5, 1.5) {C}; +\node at (8.5, 1.5) {B}; +\node at (9.5, 1.5) {A}; +\node at (10.5, 1.5) {C}; +\node at (11.5, 1.5) {B}; +\node at (12.5, 1.5) {A}; +\node at (13.5, 1.5) {C}; +\node at (14.5, 1.5) {D}; +\node at (15.5, 1.5) {A}; + +\node at (0.5, 0.5) {--}; +\node at (1.5, 0.5) {0}; +\node at (2.5, 0.5) {0}; +\node at (3.5, 0.5) {2}; +\node at (4.5, 0.5) {0}; +\node at (5.5, 0.5) {0}; +\node at (6.5, 0.5) {5}; +\node at (7.5, 0.5) {0}; +\node at (8.5, 0.5) {0}; +\node at (9.5, 0.5) {?}; +\node at (10.5, 0.5) {?}; +\node at (11.5, 0.5) {?}; +\node at (12.5, 0.5) {?}; +\node at (13.5, 0.5) {?}; +\node at (14.5, 0.5) {?}; +\node at (15.5, 0.5) {?}; + +\draw [decoration={brace}, decorate, line width=0.5mm] (6,3.00) -- (11,3.00); + +\node at (6.5,3.50) {$x$}; +\node at (10.5,3.50) {$y$}; + + +\footnotesize +\node at (0.5, 2.5) {1}; +\node at (1.5, 2.5) {2}; +\node at (2.5, 2.5) {3}; +\node at (3.5, 2.5) {4}; +\node at (4.5, 2.5) {5}; +\node at (5.5, 2.5) {6}; +\node at (6.5, 2.5) {7}; +\node at (7.5, 2.5) {8}; +\node at (8.5, 2.5) {9}; +\node at (9.5, 2.5) {10}; +\node at (10.5, 2.5) {11}; +\node at (11.5, 2.5) {12}; +\node at (12.5, 2.5) {13}; +\node at (13.5, 2.5) {14}; +\node at (14.5, 2.5) {15}; +\node at (15.5, 2.5) {16}; + +\draw[thick,<->] (9.5,-0.25) .. controls (9,-1.25) and (4,-1.25) .. (3.5,-0.25); +\end{tikzpicture} +\end{center} + +Nyt algoritmi alkaa vertailla merkkejä +kohdasta $y+1=12$ alkaen merkki kerrallaan. +Algoritmi ei voi hyödyntää valmiina +Z-taulukossa olevaa tietoa, koska se ei ole vielä aiemmin +tutkinut merkkijonoa näin pitkälle. +Tuloksena osajonon pituudeksi tulee 7 +ja väli $[x,y]$ päivittyy vastaavasti: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=lightgray] (9,0) rectangle (10,1); +\draw (0,0) grid (16,2); + +\node at (0.5, 1.5) {A}; +\node at (1.5, 1.5) {C}; +\node at (2.5, 1.5) {B}; +\node at (3.5, 1.5) {A}; +\node at (4.5, 1.5) {C}; +\node at (5.5, 1.5) {D}; +\node at (6.5, 1.5) {A}; +\node at (7.5, 1.5) {C}; +\node at (8.5, 1.5) {B}; +\node at (9.5, 1.5) {A}; +\node at (10.5, 1.5) {C}; +\node at (11.5, 1.5) {B}; +\node at (12.5, 1.5) {A}; +\node at (13.5, 1.5) {C}; +\node at (14.5, 1.5) {D}; +\node at (15.5, 1.5) {A}; + +\node at (0.5, 0.5) {--}; +\node at (1.5, 0.5) {0}; +\node at (2.5, 0.5) {0}; +\node at (3.5, 0.5) {2}; +\node at (4.5, 0.5) {0}; +\node at (5.5, 0.5) {0}; +\node at (6.5, 0.5) {5}; +\node at (7.5, 0.5) {0}; +\node at (8.5, 0.5) {0}; +\node at (9.5, 0.5) {7}; +\node at (10.5, 0.5) {?}; +\node at (11.5, 0.5) {?}; +\node at (12.5, 0.5) {?}; +\node at (13.5, 0.5) {?}; +\node at (14.5, 0.5) {?}; +\node at (15.5, 0.5) {?}; + +\draw [decoration={brace}, decorate, line width=0.5mm] (9,3.00) -- (16,3.00); + +\node at (9.5,3.50) {$x$}; +\node at (15.5,3.50) {$y$}; + + +\footnotesize +\node at (0.5, 2.5) {1}; +\node at (1.5, 2.5) {2}; +\node at (2.5, 2.5) {3}; +\node at (3.5, 2.5) {4}; +\node at (4.5, 2.5) {5}; +\node at (5.5, 2.5) {6}; +\node at (6.5, 2.5) {7}; +\node at (7.5, 2.5) {8}; +\node at (8.5, 2.5) {9}; +\node at (9.5, 2.5) {10}; +\node at (10.5, 2.5) {11}; +\node at (11.5, 2.5) {12}; +\node at (12.5, 2.5) {13}; +\node at (13.5, 2.5) {14}; +\node at (14.5, 2.5) {15}; +\node at (15.5, 2.5) {16}; + +% \draw[thick,<->] (9.5,-0.25) .. controls (9,-1.25) and (4,-1.25) .. (3.5,-0.25); +\end{tikzpicture} +\end{center} + +Tämän jälkeen kaikkien seuraavien Z-taulukon +arvojen laskemisessa pystyy hyödyntämään +jälleen välin $[x,y]$ antamaa tietoa +ja algoritmi saa Z-taulukon loppuun tulevat +arvot suoraan Z-taulukon alusta: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (16,2); + +\node at (0.5, 1.5) {A}; +\node at (1.5, 1.5) {C}; +\node at (2.5, 1.5) {B}; +\node at (3.5, 1.5) {A}; +\node at (4.5, 1.5) {C}; +\node at (5.5, 1.5) {D}; +\node at (6.5, 1.5) {A}; +\node at (7.5, 1.5) {C}; +\node at (8.5, 1.5) {B}; +\node at (9.5, 1.5) {A}; +\node at (10.5, 1.5) {C}; +\node at (11.5, 1.5) {B}; +\node at (12.5, 1.5) {A}; +\node at (13.5, 1.5) {C}; +\node at (14.5, 1.5) {D}; +\node at (15.5, 1.5) {A}; + +\node at (0.5, 0.5) {--}; +\node at (1.5, 0.5) {0}; +\node at (2.5, 0.5) {0}; +\node at (3.5, 0.5) {2}; +\node at (4.5, 0.5) {0}; +\node at (5.5, 0.5) {0}; +\node at (6.5, 0.5) {5}; +\node at (7.5, 0.5) {0}; +\node at (8.5, 0.5) {0}; +\node at (9.5, 0.5) {7}; +\node at (10.5, 0.5) {0}; +\node at (11.5, 0.5) {0}; +\node at (12.5, 0.5) {2}; +\node at (13.5, 0.5) {0}; +\node at (14.5, 0.5) {0}; +\node at (15.5, 0.5) {1}; + +\draw [decoration={brace}, decorate, line width=0.5mm] (9,3.00) -- (16,3.00); + +\node at (9.5,3.50) {$x$}; +\node at (15.5,3.50) {$y$}; + + +\footnotesize +\node at (0.5, 2.5) {1}; +\node at (1.5, 2.5) {2}; +\node at (2.5, 2.5) {3}; +\node at (3.5, 2.5) {4}; +\node at (4.5, 2.5) {5}; +\node at (5.5, 2.5) {6}; +\node at (6.5, 2.5) {7}; +\node at (7.5, 2.5) {8}; +\node at (8.5, 2.5) {9}; +\node at (9.5, 2.5) {10}; +\node at (10.5, 2.5) {11}; +\node at (11.5, 2.5) {12}; +\node at (12.5, 2.5) {13}; +\node at (13.5, 2.5) {14}; +\node at (14.5, 2.5) {15}; +\node at (15.5, 2.5) {16}; + +\end{tikzpicture} +\end{center} + +\subsubsection{Z-taulukon käyttäminen} + +Ratkaistaan esimerkkinä tehtävä, +jossa laskettavana on, +montako kertaa merkkijono $p$ +esiintyy osajonona merkkijonossa $s$. +Ratkaisimme tehtävän aiemmin tehokkaasti +merkkijonohajautuksen avulla, +ja nyt Z-algoritmi tarjoaa siihen +vaihtoehtoisen lähestymistavan. + +Usein esiintyvä idea Z-algoritmin yhteydessä +on muodostaa merkkijono, +jonka osana on useita välimerkeillä +erotettuja merkkijonoja. +Tässä tehtävässä sopiva merkkijono on +$p$\texttt{\#}$s$, +jossa merkkijonojen $p$ ja $s$ välissä on +erikoismerkki \texttt{\#}, +jota ei esiinny merkkijonoissa. +Nyt merkkijonoa $p$\texttt{\#}$s$ +vastaava Z-taulukko kertoo, +missä kohdissa merkkijonoa $p$ +esiintyy merkkijono $s$. +Tällaiset kohdat ovat tarkalleen ne +Z-taulukon kohdat, joissa on +merkkijonon $p$ pituus. + +\begin{samepage} +Esimerkiksi jos $s=$\texttt{HATTIVATTI} ja $p=$\texttt{ATT}, +niin Z-taulukosta tulee: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (14,2); + +\node at (0.5, 1.5) {A}; +\node at (1.5, 1.5) {T}; +\node at (2.5, 1.5) {T}; +\node at (3.5, 1.5) {\#}; +\node at (4.5, 1.5) {H}; +\node at (5.5, 1.5) {A}; +\node at (6.5, 1.5) {T}; +\node at (7.5, 1.5) {T}; +\node at (8.5, 1.5) {I}; +\node at (9.5, 1.5) {V}; +\node at (10.5, 1.5) {A}; +\node at (11.5, 1.5) {T}; +\node at (12.5, 1.5) {T}; +\node at (13.5, 1.5) {I}; + +\node at (0.5, 0.5) {--}; +\node at (1.5, 0.5) {0}; +\node at (2.5, 0.5) {0}; +\node at (3.5, 0.5) {0}; +\node at (4.5, 0.5) {0}; +\node at (5.5, 0.5) {3}; +\node at (6.5, 0.5) {0}; +\node at (7.5, 0.5) {0}; +\node at (8.5, 0.5) {0}; +\node at (9.5, 0.5) {0}; +\node at (10.5, 0.5) {3}; +\node at (11.5, 0.5) {0}; +\node at (12.5, 0.5) {0}; +\node at (13.5, 0.5) {0}; + +\footnotesize +\node at (0.5, 2.5) {1}; +\node at (1.5, 2.5) {2}; +\node at (2.5, 2.5) {3}; +\node at (3.5, 2.5) {4}; +\node at (4.5, 2.5) {5}; +\node at (5.5, 2.5) {6}; +\node at (6.5, 2.5) {7}; +\node at (7.5, 2.5) {8}; +\node at (8.5, 2.5) {9}; +\node at (9.5, 2.5) {10}; +\node at (10.5, 2.5) {11}; +\node at (11.5, 2.5) {12}; +\node at (12.5, 2.5) {13}; +\node at (13.5, 2.5) {14}; +\end{tikzpicture} +\end{center} +\end{samepage} +Taulukon kohdissa 6 ja 11 on luku 3, +mikä tarkoittaa, että \texttt{ATT} +esiintyy vastaavissa kohdissa merkkijonossa +\texttt{HATTIVATTI}. + +Tuloksena olevan algoritmin aikavaativuus on +$O(n)$, koska riittää muodostaa Z-taulukko +ja käydä se läpi. \ No newline at end of file diff --git a/luku27.tex b/luku27.tex new file mode 100644 index 0000000..1d46b0c --- /dev/null +++ b/luku27.tex @@ -0,0 +1,405 @@ +\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)$. + + diff --git a/luku28.tex b/luku28.tex new file mode 100644 index 0000000..ff3bddc --- /dev/null +++ b/luku28.tex @@ -0,0 +1,1175 @@ +\chapter{Segment trees revisited} + +\index{segmenttipuu@segmenttipuu} + +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. + +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): + +\begin{lstlisting} +int summa(int a, int b) { + a += N; b += N; + int s = 0; + while (a <= b) { + if (a%2 == 1) s += p[a++]; + if (b%2 == 0) s += p[b--]; + a /= 2; b /= 2; + } + return s; +} +\end{lstlisting} +Ylhäältä alaspäin toteutettuna funktiosta tulee: + +\begin{lstlisting} +int summa(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); +} +\end{lstlisting} +Nyt välin $[a,b]$ summan saa laskettua +kutsumalla funktiota näin: + +\begin{lstlisting} +int s = summa(a, b, 1, 0, N-1); +\end{lstlisting} + +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}. +\\ +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=gray!50] (5,0) rectangle (6,1); +\draw (0,0) grid (16,1); + +\node[anchor=center] at (0.5, 0.5) {5}; +\node[anchor=center] at (1.5, 0.5) {8}; +\node[anchor=center] at (2.5, 0.5) {6}; +\node[anchor=center] at (3.5, 0.5) {3}; +\node[anchor=center] at (4.5, 0.5) {2}; +\node[anchor=center] at (5.5, 0.5) {7}; +\node[anchor=center] at (6.5, 0.5) {2}; +\node[anchor=center] at (7.5, 0.5) {6}; +\node[anchor=center] at (8.5, 0.5) {7}; +\node[anchor=center] at (9.5, 0.5) {1}; +\node[anchor=center] at (10.5, 0.5) {7}; +\node[anchor=center] at (11.5, 0.5) {5}; +\node[anchor=center] at (12.5, 0.5) {6}; +\node[anchor=center] at (13.5, 0.5) {2}; +\node[anchor=center] at (14.5, 0.5) {3}; +\node[anchor=center] at (15.5, 0.5) {2}; + +%\node[anchor=center] at (1,2.5) {13}; + +\node[draw, circle] (a) at (1,2.5) {13}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,minimum size=22pt] (b) at (3,2.5) {9}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); +\node[draw, circle,minimum size=22pt] (c) at (5,2.5) {9}; +\path[draw,thick,-] (c) -- (4.5,1); +\path[draw,thick,-] (c) -- (5.5,1); +\node[draw, circle,fill=gray!50,minimum size=22pt] (d) at (7,2.5) {8}; +\path[draw,thick,-] (d) -- (6.5,1); +\path[draw,thick,-] (d) -- (7.5,1); +\node[draw, circle,minimum size=22pt] (e) at (9,2.5) {8}; +\path[draw,thick,-] (e) -- (8.5,1); +\path[draw,thick,-] (e) -- (9.5,1); +\node[draw, circle] (f) at (11,2.5) {12}; +\path[draw,thick,-] (f) -- (10.5,1); +\path[draw,thick,-] (f) -- (11.5,1); +\node[draw, circle,fill=gray!50,minimum size=22pt] (g) at (13,2.5) {8}; +\path[draw,thick,-] (g) -- (12.5,1); +\path[draw,thick,-] (g) -- (13.5,1); +\node[draw, circle,minimum size=22pt] (h) at (15,2.5) {5}; +\path[draw,thick,-] (h) -- (14.5,1); +\path[draw,thick,-] (h) -- (15.5,1); + +\node[draw, circle] (i) at (2,4.5) {22}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\node[draw, circle] (j) at (6,4.5) {17}; +\path[draw,thick,-] (j) -- (c); +\path[draw,thick,-] (j) -- (d); +\node[draw, circle,fill=gray!50] (k) at (10,4.5) {20}; +\path[draw,thick,-] (k) -- (e); +\path[draw,thick,-] (k) -- (f); +\node[draw, circle] (l) at (14,4.5) {13}; +\path[draw,thick,-] (l) -- (g); +\path[draw,thick,-] (l) -- (h); + +\node[draw, circle] (m) at (4,6.5) {39}; +\path[draw,thick,-] (m) -- (i); +\path[draw,thick,-] (m) -- (j); +\node[draw, circle] (n) at (12,6.5) {33}; +\path[draw,thick,-] (n) -- (k); +\path[draw,thick,-] (n) -- (l); + +\node[draw, circle] (o) at (8,8.5) {72}; +\path[draw,thick,-] (o) -- (m); +\path[draw,thick,-] (o) -- (n); + +\path[draw=red,thick,->,line width=2pt] (o) -- (m); +\path[draw=red,thick,->,line width=2pt] (o) -- (n); + +\path[draw=red,thick,->,line width=2pt] (m) -- (j); +\path[draw=red,thick,->,line width=2pt] (j) -- (c); +\path[draw=red,thick,->,line width=2pt] (j) -- (d); +\path[draw=red,thick,->,line width=2pt] (c) -- (5.5,1); + +\path[draw=red,thick,->,line width=2pt] (n) -- (k); +\path[draw=red,thick,->,line width=2pt] (n) -- (l); + +\path[draw=red,thick,->,line width=2pt] (l) -- (g); + +\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)$. + +\section{Laiska eteneminen} + +\index{laiska eteneminen@laiska eteneminen} +\index{laiska segmenttipuu@laiska segmenttipuu} + +\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ä. + +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. + +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. + +\subsubsection{Laiska segmenttipuu} + +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 +\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. +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (16,1); + +\node[anchor=center] at (0.5, 0.5) {5}; +\node[anchor=center] at (1.5, 0.5) {8}; +\node[anchor=center] at (2.5, 0.5) {6}; +\node[anchor=center] at (3.5, 0.5) {3}; +\node[anchor=center] at (4.5, 0.5) {2}; +\node[anchor=center] at (5.5, 0.5) {7}; +\node[anchor=center] at (6.5, 0.5) {2}; +\node[anchor=center] at (7.5, 0.5) {6}; +\node[anchor=center] at (8.5, 0.5) {7}; +\node[anchor=center] at (9.5, 0.5) {1}; +\node[anchor=center] at (10.5, 0.5) {7}; +\node[anchor=center] at (11.5, 0.5) {5}; +\node[anchor=center] at (12.5, 0.5) {6}; +\node[anchor=center] at (13.5, 0.5) {2}; +\node[anchor=center] at (14.5, 0.5) {3}; +\node[anchor=center] at (15.5, 0.5) {2}; + +\node[draw, circle] (a) at (1,2.5) {13/0}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,minimum size=32pt] (b) at (3,2.5) {9/0}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); +\node[draw, circle,minimum size=32pt] (c) at (5,2.5) {9/0}; +\path[draw,thick,-] (c) -- (4.5,1); +\path[draw,thick,-] (c) -- (5.5,1); +\node[draw, circle,minimum size=32pt] (d) at (7,2.5) {8/0}; +\path[draw,thick,-] (d) -- (6.5,1); +\path[draw,thick,-] (d) -- (7.5,1); +\node[draw, circle,minimum size=32pt] (e) at (9,2.5) {8/0}; +\path[draw,thick,-] (e) -- (8.5,1); +\path[draw,thick,-] (e) -- (9.5,1); +\node[draw, circle] (f) at (11,2.5) {12/0}; +\path[draw,thick,-] (f) -- (10.5,1); +\path[draw,thick,-] (f) -- (11.5,1); +\node[draw, circle,minimum size=32pt] (g) at (13,2.5) {8/0}; +\path[draw,thick,-] (g) -- (12.5,1); +\path[draw,thick,-] (g) -- (13.5,1); +\node[draw, circle,minimum size=32pt] (h) at (15,2.5) {5/0}; +\path[draw,thick,-] (h) -- (14.5,1); +\path[draw,thick,-] (h) -- (15.5,1); + +\node[draw, circle] (i) at (2,4.5) {22/0}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\node[draw, circle] (j) at (6,4.5) {17/0}; +\path[draw,thick,-] (j) -- (c); +\path[draw,thick,-] (j) -- (d); +\node[draw, circle] (k) at (10,4.5) {20/0}; +\path[draw,thick,-] (k) -- (e); +\path[draw,thick,-] (k) -- (f); +\node[draw, circle] (l) at (14,4.5) {13/0}; +\path[draw,thick,-] (l) -- (g); +\path[draw,thick,-] (l) -- (h); + +\node[draw, circle] (m) at (4,6.5) {39/0}; +\path[draw,thick,-] (m) -- (i); +\path[draw,thick,-] (m) -- (j); +\node[draw, circle] (n) at (12,6.5) {33/0}; +\path[draw,thick,-] (n) -- (k); +\path[draw,thick,-] (n) -- (l); + +\node[draw, circle] (o) at (8,8.5) {72/0}; +\path[draw,thick,-] (o) -- (m); +\path[draw,thick,-] (o) -- (n); +\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: + +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: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[color=gray!50] (5,0) rectangle (6,1); +\draw (0,0) grid (16,1); + +\node[anchor=center] at (0.5, 0.5) {5}; +\node[anchor=center] at (1.5, 0.5) {8}; +\node[anchor=center] at (2.5, 0.5) {6}; +\node[anchor=center] at (3.5, 0.5) {3}; +\node[anchor=center] at (4.5, 0.5) {2}; +\node[anchor=center] at (5.5, 0.5) {9}; +\node[anchor=center] at (6.5, 0.5) {2}; +\node[anchor=center] at (7.5, 0.5) {6}; +\node[anchor=center] at (8.5, 0.5) {7}; +\node[anchor=center] at (9.5, 0.5) {1}; +\node[anchor=center] at (10.5, 0.5) {7}; +\node[anchor=center] at (11.5, 0.5) {5}; +\node[anchor=center] at (12.5, 0.5) {6}; +\node[anchor=center] at (13.5, 0.5) {2}; +\node[anchor=center] at (14.5, 0.5) {3}; +\node[anchor=center] at (15.5, 0.5) {2}; + +\node[draw, circle] (a) at (1,2.5) {13/0}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,minimum size=32pt] (b) at (3,2.5) {9/0}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); +\node[draw, circle,minimum size=32pt] (c) at (5,2.5) {11/0}; +\path[draw,thick,-] (c) -- (4.5,1); +\path[draw,thick,-] (c) -- (5.5,1); +\node[draw, circle,fill=gray!50,minimum size=32pt] (d) at (7,2.5) {8/2}; +\path[draw,thick,-] (d) -- (6.5,1); +\path[draw,thick,-] (d) -- (7.5,1); +\node[draw, circle,minimum size=32pt] (e) at (9,2.5) {8/0}; +\path[draw,thick,-] (e) -- (8.5,1); +\path[draw,thick,-] (e) -- (9.5,1); +\node[draw, circle] (f) at (11,2.5) {12/0}; +\path[draw,thick,-] (f) -- (10.5,1); +\path[draw,thick,-] (f) -- (11.5,1); +\node[draw, circle,fill=gray!50,minimum size=32pt] (g) at (13,2.5) {8/2}; +\path[draw,thick,-] (g) -- (12.5,1); +\path[draw,thick,-] (g) -- (13.5,1); +\node[draw, circle,minimum size=32pt] (h) at (15,2.5) {5/0}; +\path[draw,thick,-] (h) -- (14.5,1); +\path[draw,thick,-] (h) -- (15.5,1); + +\node[draw, circle] (i) at (2,4.5) {22/0}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\node[draw, circle] (j) at (6,4.5) {23/0}; +\path[draw,thick,-] (j) -- (c); +\path[draw,thick,-] (j) -- (d); +\node[draw, circle,fill=gray!50] (k) at (10,4.5) {20/2}; +\path[draw,thick,-] (k) -- (e); +\path[draw,thick,-] (k) -- (f); +\node[draw, circle] (l) at (14,4.5) {17/0}; +\path[draw,thick,-] (l) -- (g); +\path[draw,thick,-] (l) -- (h); + +\node[draw, circle] (m) at (4,6.5) {45/0}; +\path[draw,thick,-] (m) -- (i); +\path[draw,thick,-] (m) -- (j); +\node[draw, circle] (n) at (12,6.5) {45/0}; +\path[draw,thick,-] (n) -- (k); +\path[draw,thick,-] (n) -- (l); + +\node[draw, circle] (o) at (8,8.5) {90/0}; +\path[draw,thick,-] (o) -- (m); +\path[draw,thick,-] (o) -- (n); + +\path[draw=red,thick,->,line width=2pt] (o) -- (m); +\path[draw=red,thick,->,line width=2pt] (o) -- (n); + +\path[draw=red,thick,->,line width=2pt] (m) -- (j); +\path[draw=red,thick,->,line width=2pt] (j) -- (c); +\path[draw=red,thick,->,line width=2pt] (j) -- (d); +\path[draw=red,thick,->,line width=2pt] (c) -- (5.5,1); + +\path[draw=red,thick,->,line width=2pt] (n) -- (k); +\path[draw=red,thick,->,line width=2pt] (n) -- (l); + +\path[draw=red,thick,->,line width=2pt] (l) -- (g); + +\draw [decoration={brace}, decorate, line width=0.5mm] (14,-0.25) -- (5,-0.25); +\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. + +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. + +Seuraava kuva näyttää, kuinka äskeinen puu muuttuu, +kun siitä lasketaan puun alle merkityn välin summa: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (16,1); + +\node[anchor=center] at (0.5, 0.5) {5}; +\node[anchor=center] at (1.5, 0.5) {8}; +\node[anchor=center] at (2.5, 0.5) {6}; +\node[anchor=center] at (3.5, 0.5) {3}; +\node[anchor=center] at (4.5, 0.5) {2}; +\node[anchor=center] at (5.5, 0.5) {9}; +\node[anchor=center] at (6.5, 0.5) {2}; +\node[anchor=center] at (7.5, 0.5) {6}; +\node[anchor=center] at (8.5, 0.5) {7}; +\node[anchor=center] at (9.5, 0.5) {1}; +\node[anchor=center] at (10.5, 0.5) {7}; +\node[anchor=center] at (11.5, 0.5) {5}; +\node[anchor=center] at (12.5, 0.5) {6}; +\node[anchor=center] at (13.5, 0.5) {2}; +\node[anchor=center] at (14.5, 0.5) {3}; +\node[anchor=center] at (15.5, 0.5) {2}; + +\node[draw, circle] (a) at (1,2.5) {13/0}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,minimum size=32pt] (b) at (3,2.5) {9/0}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); +\node[draw, circle,minimum size=32pt] (c) at (5,2.5) {11/0}; +\path[draw,thick,-] (c) -- (4.5,1); +\path[draw,thick,-] (c) -- (5.5,1); +\node[draw, circle,minimum size=32pt] (d) at (7,2.5) {8/2}; +\path[draw,thick,-] (d) -- (6.5,1); +\path[draw,thick,-] (d) -- (7.5,1); +\node[draw, circle,minimum size=32pt] (e) at (9,2.5) {8/2}; +\path[draw,thick,-] (e) -- (8.5,1); +\path[draw,thick,-] (e) -- (9.5,1); +\node[draw, circle,fill=gray!50,] (f) at (11,2.5) {12/2}; +\path[draw,thick,-] (f) -- (10.5,1); +\path[draw,thick,-] (f) -- (11.5,1); +\node[draw, circle,fill=gray!50,minimum size=32pt] (g) at (13,2.5) {8/2}; +\path[draw,thick,-] (g) -- (12.5,1); +\path[draw,thick,-] (g) -- (13.5,1); +\node[draw, circle,minimum size=32pt] (h) at (15,2.5) {5/0}; +\path[draw,thick,-] (h) -- (14.5,1); +\path[draw,thick,-] (h) -- (15.5,1); + +\node[draw, circle] (i) at (2,4.5) {22/0}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\node[draw, circle] (j) at (6,4.5) {23/0}; +\path[draw,thick,-] (j) -- (c); +\path[draw,thick,-] (j) -- (d); +\node[draw, circle] (k) at (10,4.5) {28/0}; +\path[draw,thick,-] (k) -- (e); +\path[draw,thick,-] (k) -- (f); +\node[draw, circle] (l) at (14,4.5) {17/0}; +\path[draw,thick,-] (l) -- (g); +\path[draw,thick,-] (l) -- (h); + +\node[draw, circle] (m) at (4,6.5) {45/0}; +\path[draw,thick,-] (m) -- (i); +\path[draw,thick,-] (m) -- (j); +\node[draw, circle] (n) at (12,6.5) {45/0}; +\path[draw,thick,-] (n) -- (k); +\path[draw,thick,-] (n) -- (l); + +\node[draw, circle] (o) at (8,8.5) {90/0}; +\path[draw,thick,-] (o) -- (m); +\path[draw,thick,-] (o) -- (n); + +\path[draw=red,thick,->,line width=2pt] (o) -- (n); + +\path[draw=red,thick,->,line width=2pt] (n) -- (k); +\path[draw=red,thick,->,line width=2pt] (n) -- (l); + +\path[draw=red,thick,->,line width=2pt] (k) -- (f); +\path[draw=red,thick,->,line width=2pt] (l) -- (g); + +\draw [decoration={brace}, decorate, line width=0.5mm] (14,-0.25) -- (10,-0.25); + +\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. + +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$. + +\subsubsection{Polynomimuutos} + +Laiskaa segmenttipuuta voi yleistää niin, +että väliä muuttaa polynomi +\[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. + +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. + +Nyt välin $[x,y]$ summa on +\[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 +\[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 +\[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} + +\index{dynaaminen segmenttipuu@dynaaminen segmenttipuu} + +Tavallinen segmenttipuu on staattinen, +eli jokaiselle solmulle on paikka taulukossa +ja puu vie kiinteän määrän muistia. +Tämä toteutus kuitenkin tuhlaa muistia, +jos suurin osa puun solmuista on tyhjiä. +\key{Dynaaminen segmenttipuu} varaa muistia vain +niille solmuille, joita todella tarvitaan. + +Solmut on kätevää tallentaa tietueina tähän tapaan: + +\begin{lstlisting} +struct node { + int s; + int x, y; + node *l, *r; + node(int x, int a, int b) : s(s), x(x), y(y) {} +}; +\end{lstlisting} +Tässä $s$ on solmussa oleva arvo, +$[x,y]$ on solmua vastaava väli +ja $l$ ja $r$ osoittavat +solmun vasempaan ja oikeaan alipuuhun. + +Tämän jälkeen solmuja voi käsitellä seuraavasti: + +\begin{lstlisting} +// uuden solmun luonti +node *u = new node(0, 0, 15); +// kentän muuttaminen +u->s = 5; +\end{lstlisting} + +\subsubsection{Harva segmenttipuu} + +\index{harva segmenttipuu@harva segmenttipuu} + +Dynaaminen segmenttipuu on hyödyllinen, +jos puun indeksialue $[0,N-1]$ on \textit{harva} +eli $N$ on suuri mutta vain +pieni osa indekseistä on käytössä. +Siinä missä tavallinen segmenttipuu +vie muistia $O(N)$, +dynaaminen segmenttipuu vie muistia +vain $O(n \log N)$, missä $n$ on +käytössä olevien indeksien määrä. + +\key{Harva segmenttipuu} aluksi tyhjä +ja sen ainoa solmu on $[0,N-1]$. +Kun puu muuttuu, siihen lisätään +solmuja dynaamisesti sitä mukaa kuin niitä tarvitaan +uusien indeksien vuoksi. +Esimerkiksi jos $N=16$ ja indeksejä +3 ja 10 on muutettu, +puu sisältää seuraavat solmut: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\scriptsize +\node[draw, circle,minimum size=35pt] (1) at (0,0) {$[0,15]$}; +\node[draw, circle,minimum size=35pt] (2) at (-4,-2) {$[0,7]$}; +\node[draw, circle,minimum size=35pt] (3) at (-6,-4) {$[0,3]$}; +\node[draw, circle,minimum size=35pt] (4) at (-4,-6) {$[2,3]$}; +\node[draw, circle,minimum size=35pt] (5) at (-2,-8) {$[3]$}; +\node[draw, circle,minimum size=35pt] (6) at (4,-2) {$[8,15]$}; +\node[draw, circle,minimum size=35pt] (7) at (2,-4) {$[8,11]$}; +\node[draw, circle,minimum size=35pt] (8) at (4,-6) {$[10,11]$}; +\node[draw, circle,minimum size=35pt] (9) at (2,-8) {$[10]$}; + +\path[draw,thick,->] (1) -- (2); +\path[draw,thick,->] (2) -- (3); +\path[draw,thick,->] (3) -- (4); +\path[draw,thick,->] (4) -- (5); + +\path[draw,thick,->] (1) -- (6); +\path[draw,thick,->] (6) -- (7); +\path[draw,thick,->] (7) -- (8); +\path[draw,thick,->] (8) -- (9); +\end{tikzpicture} +\end{center} + +Reitti puun juuresta lehteen sisältää +$O(\log N)$ solmua, +joten jokainen muutos puuhun lisää +enintään $O(\log N)$ uutta solmua puuhun. +Niinpä $n$ muutoksen jälkeen puussa +on enintään $O(n \log N)$ solmua. + +Huomaa, että jos kaikki tarvittavat indeksit +ovat tiedossa +algoritmin alussa, dynaamisen segmenttipuun +sijasta voi käyttää tavallista segmenttipuuta +ja indeksien pakkausta (luku 9.4). +Tämä ei ole kuitenkaan mahdollista, +jos indeksit syntyvät vasta algoritmin aikana. + +\subsubsection{Persistentti segmenttipuu} + +\index{persistentti segmenttipuu@persistentti segmenttipuu} +\index{muutoshistoria@muutoshistoria} + +Dynaamisen toteutuksen avulla on myös +mahdollista luoda \key{persistentti segmenttipuu}, +joka säilyttää puun muutoshistorian. +Tällöin muistissa on jokainen +segmenttipuun vaihe, joka on esiintynyt +algoritmin suorituksen aikana. + +Muutoshistorian hyötynä on, +että kaikkia vanhoja puita voi käsitellä +segmenttipuun tapaan, +koska niiden rakenne on edelleen olemassa. +Vanhoista puista voi myös johtaa uusia +puita, joita voi muokata edelleen. + +Tarkastellaan esimerkiksi seuraavaa muutossarjaa, +jossa punaiset solmut muuttuvat päivityksessä +ja muut solmut säilyvät ennallaan: + +\begin{center} +\begin{tikzpicture}[scale=0.8] +\node[draw, circle,minimum size=13pt] (1a) at (3,0) {}; +\node[draw, circle,minimum size=13pt] (2a) at (2,-1) {}; +\node[draw, circle,minimum size=13pt] (3a) at (4,-1) {}; +\node[draw, circle,minimum size=13pt] (4a) at (1.5,-2) {}; +\node[draw, circle,minimum size=13pt] (5a) at (2.5,-2) {}; +\node[draw, circle,minimum size=13pt] (6a) at (3.5,-2) {}; +\node[draw, circle,minimum size=13pt] (7a) at (4.5,-2) {}; +\path[draw,thick,->] (1a) -- (2a); +\path[draw,thick,->] (1a) -- (3a); +\path[draw,thick,->] (2a) -- (4a); +\path[draw,thick,->] (2a) -- (5a); +\path[draw,thick,->] (3a) -- (6a); +\path[draw,thick,->] (3a) -- (7a); + +\node[draw, circle,minimum size=13pt,fill=red] (1b) at (3+5,0) {}; +\node[draw, circle,minimum size=13pt,fill=red] (2b) at (2+5,-1) {}; +\node[draw, circle,minimum size=13pt] (3b) at (4+5,-1) {}; +\node[draw, circle,minimum size=13pt] (4b) at (1.5+5,-2) {}; +\node[draw, circle,minimum size=13pt,fill=red] (5b) at (2.5+5,-2) {}; +\node[draw, circle,minimum size=13pt] (6b) at (3.5+5,-2) {}; +\node[draw, circle,minimum size=13pt] (7b) at (4.5+5,-2) {}; +\path[draw,thick,->] (1b) -- (2b); +\path[draw,thick,->] (1b) -- (3b); +\path[draw,thick,->] (2b) -- (4b); +\path[draw,thick,->] (2b) -- (5b); +\path[draw,thick,->] (3b) -- (6b); +\path[draw,thick,->] (3b) -- (7b); + +\node[draw, circle,minimum size=13pt,fill=red] (1c) at (3+10,0) {}; +\node[draw, circle,minimum size=13pt] (2c) at (2+10,-1) {}; +\node[draw, circle,minimum size=13pt,fill=red] (3c) at (4+10,-1) {}; +\node[draw, circle,minimum size=13pt] (4c) at (1.5+10,-2) {}; +\node[draw, circle,minimum size=13pt] (5c) at (2.5+10,-2) {}; +\node[draw, circle,minimum size=13pt] (6c) at (3.5+10,-2) {}; +\node[draw, circle,minimum size=13pt,fill=red] (7c) at (4.5+10,-2) {}; +\path[draw,thick,->] (1c) -- (2c); +\path[draw,thick,->] (1c) -- (3c); +\path[draw,thick,->] (2c) -- (4c); +\path[draw,thick,->] (2c) -- (5c); +\path[draw,thick,->] (3c) -- (6c); +\path[draw,thick,->] (3c) -- (7c); + +\node at (3,-3) {vaihe 1}; +\node at (3+5,-3) {vaihe 2}; +\node at (3+10,-3) {vaihe 3}; +\end{tikzpicture} +\end{center} +Jokaisen muutoksen jälkeen suurin osa puun +solmuista säilyy ennallaan, +joten muistia säästävä tapa tallentaa muutoshistoria +on käyttää mahdollisimman paljon hyväksi +puun vanhoja osia muutoksissa. +Tässä tapauksessa muutoshistorian voi +tallentaa seuraavasti: +\begin{center} +\begin{tikzpicture}[scale=0.8] +\path[use as bounding box] (0, 1) rectangle (16, -3.5); + +\node[draw, circle,minimum size=13pt] (1a) at (3,0) {}; +\node[draw, circle,minimum size=13pt] (2a) at (2,-1) {}; +\node[draw, circle,minimum size=13pt] (3a) at (4,-1) {}; +\node[draw, circle,minimum size=13pt] (4a) at (1.5,-2) {}; +\node[draw, circle,minimum size=13pt] (5a) at (2.5,-2) {}; +\node[draw, circle,minimum size=13pt] (6a) at (3.5,-2) {}; +\node[draw, circle,minimum size=13pt] (7a) at (4.5,-2) {}; +\path[draw,thick,->] (1a) -- (2a); +\path[draw,thick,->] (1a) -- (3a); +\path[draw,thick,->] (2a) -- (4a); +\path[draw,thick,->] (2a) -- (5a); +\path[draw,thick,->] (3a) -- (6a); +\path[draw,thick,->] (3a) -- (7a); + +\node[draw, circle,minimum size=13pt,fill=red] (1b) at (3+5,0) {}; +\node[draw, circle,minimum size=13pt,fill=red] (2b) at (2+5,-1) {}; +\node[draw, circle,minimum size=13pt,fill=red] (5b) at (2.5+5,-2) {}; +\path[draw,thick,->] (1b) -- (2b); + +\draw[thick,->] (1b) .. controls (3+5+2,0-1) and (3+5,2.5) .. (3a); + +\draw[thick,->] (2b) .. controls (2+5-0.5,-1-0.5) and (2,4.5) .. (4a); + + +\path[draw,thick,->] (2b) -- (5b); + +\node[draw, circle,minimum size=13pt,fill=red] (1c) at (3+10,0) {}; +\node[draw, circle,minimum size=13pt,fill=red] (3c) at (4+10,-1) {}; +\node[draw, circle,minimum size=13pt,fill=red] (7c) at (4.5+10,-2) {}; +\path[draw,thick,->] (1c) -- (2b); +\path[draw,thick,->] (1c) -- (3c); + +\draw[thick,->] (3c) .. controls (2.5+5,-3) and (3.5,-3) .. (6a); + +\path[draw,thick,->] (3c) -- (7c); + +\node at (3,-3) {vaihe 1}; +\node at (3+5,-3) {vaihe 2}; +\node at (3+10,-3) {vaihe 3}; +\end{tikzpicture} +\end{center} + +Nyt muistissa on jokaisesta puun vaiheesta +puun juuri, jonka avulla pystyy selvittämään +koko puun rakenteen kyseisellä hetkellä. +Jokainen muutos tuo vain $O(\log N)$ uutta solmua puuhun, +kun puun indeksialue on $[0,N-1]$, +joten koko muutoshistorian pitäminen muistissa on mahdollista. + +\section{Tietorakenteet} + +Segmenttipuun solmussa voi olla +yksittäisen arvon +sijasta myös jokin tietorakenne, +joka pitää yllä tietoa solmua vastaavasta välistä. +Tällöin segmenttipuun operaatiot vievät aikaa +$O(f(n) \log n)$, missä $f(n)$ on +yksittäisen solmun tietorakenteen +käsittelyyn kuluva aika. + +Tarkastellaan esimerkkinä segmenttipuuta, +jonka avulla voi laskea, montako kertaa +luku $x$ esiintyy taulukon välillä $[a,b]$. +Esimerkiksi seuraavassa taulukossa +luku 1 esiintyy kolme kertaa +merkityllä välillä: + +\begin{center} +\begin{tikzpicture}[scale=0.7] +\fill[lightgray] (1,0) rectangle (6,1); +\draw (0,0) grid (8,1); + +\node[anchor=center] at (0.5, 0.5) {3}; +\node[anchor=center] at (1.5, 0.5) {1}; +\node[anchor=center] at (2.5, 0.5) {2}; +\node[anchor=center] at (3.5, 0.5) {3}; +\node[anchor=center] at (4.5, 0.5) {1}; +\node[anchor=center] at (5.5, 0.5) {1}; +\node[anchor=center] at (6.5, 0.5) {1}; +\node[anchor=center] at (7.5, 0.5) {2}; +\end{tikzpicture} +\end{center} + +Ideana on toteuttaa segmenttipuu, jonka +jokaisessa solmussa on tietorakenne, +josta voi kysyä, +montako kertaa luku $x$ esiintyy solmun välillä. +Tällaisen segmenttipuun avulla +vastaus kyselyyn syntyy laskemalla yhteen +esiintymismäärät väleiltä, joista $[a,b]$ muodostuu. + +Esimerkiksi yllä olevasta taulukosta syntyy +seuraava segmenttipuu: +\begin{center} +\begin{tikzpicture}[scale=0.7] + +\node[draw, rectangle] (a) at (1,2.5) +{ +\footnotesize +\begin{tabular}{r} +3 \\ +\hline +1 \\ +\end{tabular}}; +\node[draw, rectangle] (b) at (3,2.5) +{ +\footnotesize +\begin{tabular}{r} +1 \\ +\hline +1 \\ +\end{tabular}}; +\node[draw, rectangle] (c) at (5,2.5) +{ +\footnotesize +\begin{tabular}{r} +2 \\ +\hline +1 \\ +\end{tabular}}; +\node[draw, rectangle] (d) at (7,2.5) +{ +\footnotesize +\begin{tabular}{r} +3 \\ +\hline +1 \\ +\end{tabular}}; +\node[draw, rectangle] (e) at (9,2.5) +{ +\footnotesize +\begin{tabular}{r} +1 \\ +\hline +1 \\ +\end{tabular}}; +\node[draw, rectangle] (f) at (11,2.5) +{ +\footnotesize +\begin{tabular}{r} +1 \\ +\hline +1 \\ +\end{tabular}}; +\node[draw, rectangle] (g) at (13,2.5) +{ +\footnotesize +\begin{tabular}{r} +1 \\ +\hline +1 \\ +\end{tabular}}; +\node[draw, rectangle] (h) at (15,2.5) +{ +\footnotesize +\begin{tabular}{r} +2 \\ +\hline +1 \\ +\end{tabular}}; + +\node[draw, rectangle] (i) at (2,4.5) +{ +\footnotesize +\begin{tabular}{rr} +1 & 3 \\ +\hline +1 & 1 \\ +\end{tabular}}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\node[draw, rectangle] (j) at (6,4.5) +{ +\footnotesize +\begin{tabular}{rr} +2 & 3 \\ +\hline +1 & 1 \\ +\end{tabular}}; +\path[draw,thick,-] (j) -- (c); +\path[draw,thick,-] (j) -- (d); +\node[draw, rectangle] (k) at (10,4.5) +{ +\footnotesize +\begin{tabular}{r} +1 \\ +\hline +2 \\ +\end{tabular}}; +\path[draw,thick,-] (k) -- (e); +\path[draw,thick,-] (k) -- (f); +\node[draw, rectangle] (l) at (14,4.5) +{ +\footnotesize +\begin{tabular}{rr} +1 & 2 \\ +\hline +1 & 1 \\ +\end{tabular}}; +\path[draw,thick,-] (l) -- (g); +\path[draw,thick,-] (l) -- (h); + +\node[draw, rectangle] (m) at (4,6.5) +{ +\footnotesize +\begin{tabular}{rrr} +1 & 2 & 3 \\ +\hline +1 & 1 & 2 \\ +\end{tabular}}; +\path[draw,thick,-] (m) -- (i); +\path[draw,thick,-] (m) -- (j); +\node[draw, rectangle] (n) at (12,6.5) +{ +\footnotesize +\begin{tabular}{rr} +1 & 2 \\ +\hline +3 & 1 \\ +\end{tabular}}; +\path[draw,thick,-] (n) -- (k); +\path[draw,thick,-] (n) -- (l); + +\node[draw, rectangle] (o) at (8,8.5) +{ +\footnotesize +\begin{tabular}{rrr} +1 & 2 & 3 \\ +\hline +4 & 2 & 2 \\ +\end{tabular}}; +\path[draw,thick,-] (o) -- (m); +\path[draw,thick,-] (o) -- (n); +\end{tikzpicture} +\end{center} + + +Sopiva tietorakenne segmenttipuun toteuttamiseen on +hakemistorakenne, joka pitää kirjaa välillä esiintyvien +lukujen määrästä. +Esimerkiksi \texttt{map}-ra\-ken\-net\-ta käyttäen +yhden solmun käsittely vie aikaa $O(\log n)$, +minkä seurauksena kyselyn aikavaativuus on $O(\log^2 n)$. + +Solmuissa olevat tietorakenteet kasvattavat +segmenttipuun muistinkäyttöä. +Tässä tapauksessa +segmenttipuu vie tilaa $O(n \log n)$, +koska siinä on $O(\log n)$ tasoa, joista +jokaisella hakemistorakenteet sisältävät $O(n)$ lukua. + +\section{Kaksiulotteisuus} + +\index{kaksiulotteinen segmenttipuu@kaksiulotteinen segmenttipuu} + +\key{Kaksiulotteinen segmenttipuu} mahdollistaa +kaksiulotteisen taulukon +suorakulmaisia alueita koskevat kyselyt. +Tällaisen segmenttipuun voi toteuttaa +sisäkkäisinä segmenttipuina: +suuri puu vastaa taulukon rivejä +ja sen kussakin solmussa on pieni puu, +joka vastaa taulukon sarakkeita. + +Esimerkiksi taulukon +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) grid (4,4); + +\node[anchor=center] at (0.5, 0.5) {8}; +\node[anchor=center] at (1.5, 0.5) {5}; +\node[anchor=center] at (2.5, 0.5) {3}; +\node[anchor=center] at (3.5, 0.5) {8}; + +\node[anchor=center] at (0.5, 1.5) {3}; +\node[anchor=center] at (1.5, 1.5) {9}; +\node[anchor=center] at (2.5, 1.5) {7}; +\node[anchor=center] at (3.5, 1.5) {1}; + +\node[anchor=center] at (0.5, 2.5) {8}; +\node[anchor=center] at (1.5, 2.5) {7}; +\node[anchor=center] at (2.5, 2.5) {5}; +\node[anchor=center] at (3.5, 2.5) {2}; + +\node[anchor=center] at (0.5, 3.5) {7}; +\node[anchor=center] at (1.5, 3.5) {6}; +\node[anchor=center] at (2.5, 3.5) {1}; +\node[anchor=center] at (3.5, 3.5) {6}; +\end{tikzpicture} +\end{center} +alueiden summia voi laskea seuraavasta segmenttipuusta: +\begin{center} +\begin{tikzpicture}[scale=0.4] +\footnotesize +\begin{scope}[shift={(-12,0)}] +\draw (-1,-1) rectangle (5,6); +\draw (0,0) grid (4,1); +\node[anchor=center,scale=0.8] at (0.5, 0.5) {7}; +\node[anchor=center,scale=0.8] at (1.5, 0.5) {6}; +\node[anchor=center,scale=0.8] at (2.5, 0.5) {1}; +\node[anchor=center,scale=0.8] at (3.5, 0.5) {6}; + +\node[draw, circle,scale=0.8,inner sep=1pt] (a) at (1,2.5) {13}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,scale=0.8,inner sep=2.5pt] (b) at (3,2.5) {7}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); + +\node[draw, circle,scale=0.8,inner sep=1pt] (i) at (2,4.5) {20}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\end{scope} +\begin{scope}[shift={(-4,0)}] +\draw (-1,-1) rectangle (5,6); +\draw (0,0) grid (4,1); +\node[anchor=center,scale=0.8] at (0.5, 0.5) {8}; +\node[anchor=center,scale=0.8] at (1.5, 0.5) {7}; +\node[anchor=center,scale=0.8] at (2.5, 0.5) {5}; +\node[anchor=center,scale=0.8] at (3.5, 0.5) {2}; + +\node[draw, circle,scale=0.8,inner sep=1pt] (a) at (1,2.5) {15}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,scale=0.8,inner sep=2.5pt] (b) at (3,2.5) {7}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); + +\node[draw, circle,scale=0.8,inner sep=1pt] (i) at (2,4.5) {22}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\end{scope} +\begin{scope}[shift={(4,0)}] +\draw (-1,-1) rectangle (5,6); +\draw (0,0) grid (4,1); +\node[anchor=center,scale=0.8] at (0.5, 0.5) {3}; +\node[anchor=center,scale=0.8] at (1.5, 0.5) {9}; +\node[anchor=center,scale=0.8] at (2.5, 0.5) {7}; +\node[anchor=center,scale=0.8] at (3.5, 0.5) {1}; + +\node[draw, circle,scale=0.8,inner sep=1pt] (a) at (1,2.5) {12}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,scale=0.8,inner sep=2.5pt] (b) at (3,2.5) {8}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); + +\node[draw, circle,scale=0.8,inner sep=1pt] (i) at (2,4.5) {20}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\end{scope} +\begin{scope}[shift={(12,0)}] +\draw (-1,-1) rectangle (5,6); +\draw (0,0) grid (4,1); +\node[anchor=center,scale=0.8] at (0.5, 0.5) {8}; +\node[anchor=center,scale=0.8] at (1.5, 0.5) {5}; +\node[anchor=center,scale=0.8] at (2.5, 0.5) {3}; +\node[anchor=center,scale=0.8] at (3.5, 0.5) {8}; + +\node[draw, circle,scale=0.8,inner sep=1pt] (a) at (1,2.5) {13}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,scale=0.8,inner sep=1pt] (b) at (3,2.5) {11}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); + +\node[draw, circle,scale=0.8,inner sep=1pt] (i) at (2,4.5) {24}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\end{scope} +\begin{scope}[shift={(-8,10)}] +\draw (-1,-1) rectangle (5,6); +\draw (0,0) grid (4,1); +\node[anchor=center,scale=0.8] at (0.5, 0.5) {15}; +\node[anchor=center,scale=0.8] at (1.5, 0.5) {13}; +\node[anchor=center,scale=0.8] at (2.5, 0.5) {6}; +\node[anchor=center,scale=0.8] at (3.5, 0.5) {8}; + +\node[draw, circle,scale=0.8,inner sep=1pt] (a) at (1,2.5) {28}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,scale=0.8,inner sep=1pt] (b) at (3,2.5) {14}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); + +\node[draw, circle,scale=0.8,inner sep=1pt] (i) at (2,4.5) {42}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\end{scope} +\begin{scope}[shift={(8,10)}] +\draw (-1,-1) rectangle (5,6); +\draw (0,0) grid (4,1); +\node[anchor=center,scale=0.8] at (0.5, 0.5) {11}; +\node[anchor=center,scale=0.8] at (1.5, 0.5) {14}; +\node[anchor=center,scale=0.8] at (2.5, 0.5) {10}; +\node[anchor=center,scale=0.8] at (3.5, 0.5) {9}; + +\node[draw, circle,scale=0.8,inner sep=1pt] (a) at (1,2.5) {25}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,scale=0.8,inner sep=1pt] (b) at (3,2.5) {19}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); + +\node[draw, circle,scale=0.8,inner sep=1pt] (i) at (2,4.5) {44}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\end{scope} +\begin{scope}[shift={(0,20)}] +\draw (-1,-1) rectangle (5,6); +\draw (0,0) grid (4,1); +\node[anchor=center,scale=0.8] at (0.5, 0.5) {26}; +\node[anchor=center,scale=0.8] at (1.5, 0.5) {27}; +\node[anchor=center,scale=0.8] at (2.5, 0.5) {16}; +\node[anchor=center,scale=0.8] at (3.5, 0.5) {17}; + +\node[draw, circle,scale=0.8,inner sep=1pt] (a) at (1,2.5) {53}; +\path[draw,thick,-] (a) -- (0.5,1); +\path[draw,thick,-] (a) -- (1.5,1); +\node[draw, circle,scale=0.8,inner sep=1pt] (b) at (3,2.5) {33}; +\path[draw,thick,-] (b) -- (2.5,1); +\path[draw,thick,-] (b) -- (3.5,1); + +\node[draw, circle,scale=0.8,inner sep=1pt] (i) at (2,4.5) {86}; +\path[draw,thick,-] (i) -- (a); +\path[draw,thick,-] (i) -- (b); +\end{scope} +\path[draw,thick,-] (2,19) -- (-6,16); +\path[draw,thick,-] (2,19) -- (10,16); +\path[draw,thick,-] (-6,9) -- (-10,6); +\path[draw,thick,-] (-6,9) -- (-2,6); +\path[draw,thick,-] (10,9) -- (6,6); +\path[draw,thick,-] (10,9) -- (14,6); +\end{tikzpicture} +\end{center} + +Kaksiulotteisen +segmenttipuun operaatiot vievät aikaa +$O(\log^2 n)$, +koska suuressa puussa ja kussakin +pienessä puussa on $O(\log n)$ tasoa. +Segmenttipuu vie muistia $O(n^2)$, +koska jokainen pieni puu +vie muistia $O(n)$. + +Vastaavalla tavalla voi luoda myös segmenttipuita, +joissa on vielä enemmän ulottuvuuksia, +mutta tälle on harvoin tarvetta. + diff --git a/luku29.tex b/luku29.tex new file mode 100644 index 0000000..8c9112b --- /dev/null +++ b/luku29.tex @@ -0,0 +1,738 @@ +\chapter{Geometry} + +\index{geometria@geometria} + +Geometrian tehtävissä on usein haasteena keksiä, +mistä suunnasta ongelmaa kannattaa lähestyä, +jotta ratkaisun saa koodattua mukavasti ja +erikoistapauksia tulee mahdollisimman vähän. + +Tarkastellaan esimerkkinä tehtävää, +jossa annettuna on nelikulmion kulmapisteet +ja tehtävänä on laskea sen pinta-ala. +Esimerkiksi syötteenä voi olla +seuraavanlainen nelikulmio: + +\begin{center} +\begin{tikzpicture}[scale=0.45] + +\draw[fill] (6,2) circle [radius=0.1]; +\draw[fill] (5,6) circle [radius=0.1]; +\draw[fill] (2,5) circle [radius=0.1]; +\draw[fill] (1,1) circle [radius=0.1]; +\draw[thick] (6,2) -- (5,6) -- (2,5) -- (1,1) -- (6,2); +\end{tikzpicture} +\end{center} +Yksi tapa lähestyä tehtävää on jakaa nelikulmio +kahdeksi kolmioksi vetämällä jakoviiva kahden +vastakkaisen kulmapisteen välille: +\begin{center} +\begin{tikzpicture}[scale=0.45] + +\draw[fill] (6,2) circle [radius=0.1]; +\draw[fill] (5,6) circle [radius=0.1]; +\draw[fill] (2,5) circle [radius=0.1]; +\draw[fill] (1,1) circle [radius=0.1]; + +\draw[thick] (6,2) -- (5,6) -- (2,5) -- (1,1) -- (6,2); +\draw[dashed,thick] (2,5) -- (6,2); +\end{tikzpicture} +\end{center} +Tämän jälkeen riittää laskea yhteen kolmioiden +pinta-alat. Kolmion pinta-alan voi laskea +esimerkiksi \key{Heronin kaavalla} +\[ \sqrt{s (s-a) (s-b) (s-c)},\] +kun kolmion sivujen pituudet ovat +$a$, $b$ ja $c$ ja $s=(a+b+c)/2$. +\index{Heronin kaava@Heronin kaava} + +Tämä on mahdollinen tapa ratkaista tehtävä, +mutta siinä on ongelma: +miten löytää kelvollinen tapa vetää jakoviiva? +Osoittautuu, että +mitkä tahansa vastakkaiset pisteet eivät kelpaa. +Esimerkiksi seuraavassa nelikulmiossa +jakoviiva menee nelikulmion ulkopuolelle: +\begin{center} +\begin{tikzpicture}[scale=0.45] + +\draw[fill] (6,2) circle [radius=0.1]; +\draw[fill] (3,2) circle [radius=0.1]; +\draw[fill] (2,5) circle [radius=0.1]; +\draw[fill] (1,1) circle [radius=0.1]; +\draw[thick] (6,2) -- (3,2) -- (2,5) -- (1,1) -- (6,2); + +\draw[dashed,thick] (2,5) -- (6,2); +\end{tikzpicture} +\end{center} +Toinen tapa vetää jakoviiva on kuitenkin toimiva: +\begin{center} +\begin{tikzpicture}[scale=0.45] + +\draw[fill] (6,2) circle [radius=0.1]; +\draw[fill] (3,2) circle [radius=0.1]; +\draw[fill] (2,5) circle [radius=0.1]; +\draw[fill] (1,1) circle [radius=0.1]; +\draw[thick] (6,2) -- (3,2) -- (2,5) -- (1,1) -- (6,2); + +\draw[dashed,thick] (3,2) -- (1,1); +\end{tikzpicture} +\end{center} +Ihmiselle on selvää, kumpi jakoviiva jakaa nelikulmion +kahdeksi kolmioksi, mutta tietokoneen kannalta +tilanne on hankala. + +Osoittautuu, että tehtävän ratkaisuun on olemassa +paljon helpommin toteutettava tapa, +jossa ei tarvitse miettiä erikoistapauksia. +Nelikulmion pinta-alan laskemiseen +on nimittäin yleinen kaava +\[x_1y_2-x_2y_1+x_2y_3-x_3y_2+x_3y_4-x_4y_3+x_4y_1-x_1y_4,\] +kun kulmapisteet ovat +$(x_1,y_1)$, +$(x_2,y_2)$, +$(x_3,y_3)$ ja +$(x_4,y_4)$. +Tämä kaava on helppo laskea, siinä ei ole erikoistapauksia +ja osoittautuu, että kaava on mahdollista yleistää +\textit{kaikille} monikulmioille. + +\section{Kompleksiluvut} + +\index{kompleksiluku@kompleksiluku} +\index{piste@piste} +\index{vektori@vektori} + +\key{Kompleksiluku} on luku muotoa $x+y i$, missä $i = \sqrt{-1}$ +on \key{imaginääriyksikkö}. +Kompleksiluvun luonteva geometrinen tulkinta on, +että se esittää kaksiulotteisen tason pistettä $(x,y)$ +tai vektoria origosta pisteeseen $(x,y)$. + +Esimerkiksi luku $4+2i$ tarkoittaa seuraavaa +pistettä ja vektoria: + +\begin{center} +\begin{tikzpicture}[scale=0.45] + +\draw[->,thick] (-5,0)--(5,0); +\draw[->,thick] (0,-5)--(0,5); + +\draw[fill] (4,2) circle [radius=0.1]; +\draw[->,thick] (0,0)--(4-0.1,2-0.1); + +\node at (4,2.8) {$(4,2)$}; +\end{tikzpicture} +\end{center} + +\index{complex@\texttt{complex}} + +C++:ssa on kompleksilukujen käsittelyyn luokka \texttt{complex}, +josta on hyötyä geometriassa. +Luokan avulla voi esittää pisteen tai vektorin +kompleksilukuna, ja luokassa on valmiita +geometriaan soveltuvia työkaluja. + +Seuraavassa koodissa \texttt{C} on koordinaatin tyyppi +ja \texttt{P} on pisteen tai vektorin tyyppi. +Lisäksi koodi määrittelee +lyhennysmerkinnät \texttt{X} ja \texttt{Y}, +joiden avulla pystyy viittaamaan x- ja y-koordinaatteihin. + +\begin{lstlisting} +typedef long long C; +typedef complex P; +#define X real() +#define Y imag() +\end{lstlisting} + +Esimerkiksi seuraava koodi määrittelee pisteen $p=(4,2)$ +ja ilmoittaa sen x- ja y-koordinaatin: + +\begin{lstlisting} +P p = {4,2}; +cout << p.X << " " << p.Y << "\n"; // 4 2 +\end{lstlisting} + +Seuraava koodi määrittelee vektorit $v=(3,1)$ +ja $u=(2,2)$ ja laskee sitten niiden summan $s=v+u$: + +\begin{lstlisting} +P v = {3,1}; +P u = {2,2}; +P s = v+u; +cout << s.X << " " << s.Y << "\n"; // 5 3 +\end{lstlisting} + +Sopiva koordinaatin tyyppi \texttt{C} on tilanteesta +riippuen \texttt{long long} (kokonaisluku) +tai \texttt{long double} (liukuluku). +Kokonaislukuja kannattaa käyttää aina kun mahdollista, +koska silloin laskenta on tarkkaa. + +Jos koordinaatit ovat liukulukuja, +niiden vertailussa täytyy ottaa huomioon epätarkkuus. +Turvallinen tapa tarkistaa, +ovatko liukuluvut $a$ ja $b$ samat +on käyttää vertailua $|a-b|<\epsilon$, jossa $\epsilon$ +on pieni luku (esimerkiksi $\epsilon=10^{-9}$). + +\subsubsection*{Funktioita} + +Seuraavissa esimerkeissä koordinaatin tyyppinä on +\texttt{long double}. + +Funktio \texttt{abs(v)} laskee vektorin $v=(x,y)$ +pituuden $|v|$ kaavalla $\sqrt{x^2+y^2}$. +Sillä voi laskea myös pisteiden $(x_1,y_1)$ +ja $(x_2,y_2)$ etäisyyden, +koska pisteiden etäisyys +on sama kuin vektorin $(x_2-x_1,y_2-y_1)$ pituus. +Joskus hyödyllinen on myös funktio \texttt{norm(v)}, +joka laskee vektorin $v=(x,y)$ pituuden neliön $|v|^2$. + +Seuraava koodi laskee +pisteiden $(4,2)$ ja $(3,-1)$ etäisyyden: +\begin{lstlisting} +P a = {4,2}; +P b = {3,-1}; +cout << abs(b-a) << "\n"; // 3.60555 +\end{lstlisting} + +Funktio \texttt{arg(v)} laskee vektorin $v=(x,y)$ +kulman radiaaneina suhteessa x-akseliin. +Radiaaneina ilmoitettu kulma $r$ vastaa asteina +kulmaa $180 r/\pi$ astetta. +Jos vektori osoittaa suoraan oikealle, +sen kulma on 0. +Kulma kasvaa vastapäivään ja vähenee myötäpäivään +liikuttaessa. + +Funktio \texttt{polar(s,a)} muodostaa vektorin, +jonka pituus on $s$ ja joka osoittaa kulmaan $a$. +Lisäksi vektoria pystyy kääntämään kulman $a$ +verran kertomalla se vektorilla, +jonka pituus on 1 ja kulma on $a$. + +Seuraava koodi laskee vektorin $(4,2)$ kulman, +kääntää sitä sitten $1/2$ radiaania vastapäivään +ja laskee uuden kulman: + +\begin{lstlisting} +P v = {4,2}; +cout << arg(v) << "\n"; // 0.463648 +v *= polar(1.0,0.5); +cout << arg(v) << "\n"; // 0.963648 +\end{lstlisting} + +\section{Pisteet ja suorat} + +\index{ristitulo@ristitulo} +Vektorien +$a=(x_1,y_1)$ ja $b=(x_2,y_2)$ \key{ristitulo} $a \times b$ +lasketaan kaavalla $x_1 y_2 - x_2 y_1$. +Ristitulo ilmaisee, mihin suuntaan vektori $b$ +kääntyy, jos se laitetaan vektorin $a$ perään. +Positiivinen ristitulo tarkoittaa käännöstä vasemmalle, +negatiivinen käännöstä oikealle, ja nolla tarkoittaa, +että vektorit ovat samalla suoralla. + +Seuraava kuva näyttää kolme esimerkkiä ristitulosta: +\begin{center} +\begin{tikzpicture}[scale=0.45] + +\draw[->,thick] (0,0)--(4,2); +\draw[->,thick] (4,2)--(4+1,2+2); + +\node at (2.5,0.5) {$a$}; +\node at (5,2.5) {$b$}; + +\node at (3,-2) {$a \times b = 6$}; + +\draw[->,thick] (8+0,0)--(8+4,2); +\draw[->,thick] (8+4,2)--(8+4+2,2+1); + +\node at (8+2.5,0.5) {$a$}; +\node at (8+5,1.5) {$b$}; + +\node at (8+3,-2) {$a \times b = 0$}; + +\draw[->,thick] (16+0,0)--(16+4,2); +\draw[->,thick] (16+4,2)--(16+4+2,2-1); + +\node at (16+2.5,0.5) {$a$}; +\node at (16+5,2.5) {$b$}; + +\node at (16+3,-2) {$a \times b = -8$}; +\end{tikzpicture} +\end{center} + +\noindent +Esimerkiksi vasemmassa kuvassa +$a=(4,2)$ ja $b=(1,2)$. +Seuraava koodi laskee vastaavan ristitulon +luokkaa \texttt{complex} käyttäen: + +\begin{lstlisting} +P a = {4,2}; +P b = {1,2}; +C r = (conj(a)*b).Y; // 6 +\end{lstlisting} + +Tämä perustuu siihen, että funktio \texttt{conj} +muuttaa vektorin y-koordinaatin käänteiseksi +ja kompleksilukujen kertolaskun seurauksena +vektorien $(x_1,-y_1)$ ja $(x_2,y_2)$ +kertolaskun y-koordinaatti on $x_1 y_2 - x_2 y_1$. + +\subsubsection{Pisteen sijainti suoraan nähden} + +Ristitulon avulla voi selvittää, +kummalla puolella suoraa tutkittava piste sijaitsee. +Oletetaan, että suora kulkee pisteiden +$s_1$ ja $s_2$ kautta, katsontasuunta on +pisteestä $s_1$ pisteeseen $s_2$ ja +tutkittava piste on $p$. + +Esimerkiksi seuraavassa kuvassa piste $p$ +on suoran vasemmalla puolella: +\begin{center} +\begin{tikzpicture}[scale=0.45] +\draw[dashed,thick,->] (0,-3)--(12,6); +\draw[fill] (4,0) circle [radius=0.1]; +\draw[fill] (8,3) circle [radius=0.1]; +\draw[fill] (5,3) circle [radius=0.1]; +\node at (4,-1) {$s_1$}; +\node at (8,2) {$s_2$}; +\node at (5,4) {$p$}; +\end{tikzpicture} +\end{center} + +Nyt ristitulo $(p-s_1) \times (p-s_2)$ +kertoo, kummalla puolella suoraa piste $p$ sijaitsee. +Jos ristitulo on positiivinen, +piste $p$ on suoran vasemmalla puolella, +ja jos ristitulo on negatiivinen, +piste $p$ on suoran oikealla puolella. +Jos taas ristitulo on nolla, +piste $p$ on pisteiden $s_1$ ja $s_2$ +kanssa suoralla. + +\subsubsection{Janojen leikkaaminen} + +\index{leikkauspiste@leikkauspiste} + +Tarkastellaan tilannetta, jossa tasossa on kaksi +janaa $ab$ ja $cd$ ja tehtävänä on selvittää, +leikkaavatko janat. Mahdolliset tapaukset ovat seuraavat: + +\textit{Tapaus 1:} +Janat ovat samalla suoralla ja ne sivuavat toisiaan. +Tällöin janoilla on ääretön määrä leikkauspisteitä. +Esimerkiksi seuraavassa kuvassa janat leikkaavat +pisteestä $c$ pisteeseen $b$: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\draw (1.5,1.5)--(6,3); +\draw (0,1)--(4.5,2.5); +\draw[fill] (0,1) circle [radius=0.05]; +\node at (0,0.5) {$a$}; +\draw[fill] (1.5,1.5) circle [radius=0.05]; +\node at (6,2.5) {$d$}; +\draw[fill] (4.5,2.5) circle [radius=0.05]; +\node at (1.5,1) {$c$}; +\draw[fill] (6,3) circle [radius=0.05]; +\node at (4.5,2) {$b$}; +\end{tikzpicture} +\end{center} + +Tässä tapauksessa ristitulon avulla voi tarkastaa, +ovatko kaikki pisteet samalla suoralla. +Tämän jälkeen riittää järjestää pisteet ja +tarkastaa, menevätkö janat toistensa päälle. + +\textit{Tapaus 2:} +Janoilla on yhteinen päätepiste, joka on +ainoa leikkauspiste. +Esimerkiksi seuraavassa kuvassa +janat leikkaavat pisteessä $b=c$: + +\begin{center} +\begin{tikzpicture}[scale=0.9] +\draw (0,0)--(4,2); +\draw (4,2)--(6,1); +\draw[fill] (0,0) circle [radius=0.05]; +\draw[fill] (4,2) circle [radius=0.05]; +\draw[fill] (6,1) circle [radius=0.05]; + +\node at (0,0.5) {$a$}; +\node at (4,2.5) {$b=c$}; +\node at (6,1.5) {$d$}; +\end{tikzpicture} +\end{center} + +Tämä tapaus on helppoa tarkastaa, +koska mahdolliset vaihtoehdot +yhteiselle päätepisteelle ovat +$a=c$, $a=d$, $b=c$ ja $b=d$. + +\textit{Tapaus 3:} +Janoilla on yksi leikkauspiste, +joka ei ole mikään janojen päätepisteistä. +Seuraavassa kuvassa leikkauspiste on $p$: +\begin{center} +\begin{tikzpicture}[scale=0.9] +\draw (0,1)--(6,3); +\draw (2,4)--(4,0); +\draw[fill] (0,1) circle [radius=0.05]; +\node at (0,0.5) {$c$}; +\draw[fill] (6,3) circle [radius=0.05]; +\node at (6,2.5) {$d$}; +\draw[fill] (2,4) circle [radius=0.05]; +\node at (1.5,3.5) {$a$}; +\draw[fill] (4,0) circle [radius=0.05]; +\node at (4,-0.4) {$b$}; +\draw[fill] (3,2) circle [radius=0.05]; +\node at (3,1.5) {$p$}; +\end{tikzpicture} +\end{center} + +Tässä tapauksessa janat leikkaavat +tarkalleen silloin, kun samaan aikaan +pisteet $c$ ja $d$ ovat eri puolilla +$a$:sta $b$:hen kulkevaa suoraa +ja pisteet $a$ ja $b$ +ovat eri puolilla +$c$:stä $d$:hen kulkevaa suoraa. +Niinpä janojen leikkaamisen voi tarkastaa +ristitulon avulla. + +% Janojen leikkauspiste $p$ selviää etsimällä +% parametrit $t$ ja $u$ niin, että +% +% \[ p = a+t(b-a) = c+u(d-c). \] + + +\subsubsection{Pisteen etäisyys suorasta} + +Kolmion pinta-ala voidaan lausua +ristitulon avulla +\[\frac{| (a-c) \times (b-c) |}{2},\] +missä $a$, $b$ ja $c$ ovat kolmion kärkipisteet. + +Tämän kaavan avulla on mahdollista laskea, +kuinka kaukana annettu piste on suorasta. +Esimerkiksi seuraavassa kuvassa $d$ +on lyhin etäisyys pisteestä $p$ suoralle, +jonka määrittävät pisteet $s_1$ ja $s_2$: +\begin{center} +\begin{tikzpicture}[scale=0.75] +\draw (-2,-1)--(6,3); +\draw[dashed] (1,4)--(2.40,1.2); +\node at (0,-0.5) {$s_1$}; +\node at (4,1.5) {$s_2$}; +\node at (0.5,4) {$p$}; +\node at (2,2.7) {$d$}; +\draw[fill] (0,0) circle [radius=0.05]; +\draw[fill] (4,2) circle [radius=0.05]; +\draw[fill] (1,4) circle [radius=0.05]; +\end{tikzpicture} +\end{center} + +Pisteiden $s_1$, $s_2$ ja $p$ muodostaman kolmion +pinta-ala on toisaalta $\frac{1}{2} |s_2-s_1| d$ ja toisaalta +$\frac{1}{2} ((s_1-p) \times (s_2-p))$. +Niinpä haluttu etäisyys on + +\[ d = \frac{(s_1-p) \times (s_2-p)}{|s_2-s_1|} .\] + + +\subsubsection{Piste monikulmiossa} + +Tarkastellaan sitten tehtävää, jossa +tulee selvittää, onko annettu piste +monikulmion sisäpuolella vai ulkopuolella. +Esimerkiksi seuraavassa kuvassa piste $a$ on +sisäpuolella ja piste $b$ on +ulkopuolella. + +\begin{center} +\begin{tikzpicture}[scale=0.75] +%\draw (0,0)--(2,-2)--(3,1)--(5,1)--(2,3)--(1,2)--(-1,2)--(1,4)--(-2,4)--(-2,1)--(-3,3)--(-4,0)--(0,0); +\draw (0,0)--(2,2)--(5,1)--(2,3)--(1,2)--(-1,2)--(1,4)--(-2,4)--(-2,1)--(-3,3)--(-4,0)--(0,0); + +\draw[fill] (-3,1) circle [radius=0.05]; +\node at (-3,0.5) {$a$}; +\draw[fill] (1,3) circle [radius=0.05]; +\node at (1,2.5) {$b$}; +\end{tikzpicture} +\end{center} + +Kätevä ratkaisu tehtävään +on lähettää pisteestä säde +satunnaiseen suuntaan ja laskea, +montako kertaa se osuu monikulmion reunaan. +Jos kertoja on pariton määrä, +niin piste on sisäpuolella, +ja jos taas kertoja on parillinen määrä, +niin piste on ulkopuolella. + +\begin{samepage} +Äskeisessä tilanteessa säteitä +voisi lähteä seuraavasti: +\begin{center} +\begin{tikzpicture}[scale=0.75] +\draw (0,0)--(2,2)--(5,1)--(2,3)--(1,2)--(-1,2)--(1,4)--(-2,4)--(-2,1)--(-3,3)--(-4,0)--(0,0); + +\draw[fill] (-3,1) circle [radius=0.05]; +\node at (-3,0.5) {$a$}; +\draw[fill] (1,3) circle [radius=0.05]; +\node at (1,2.5) {$b$}; + +\draw[dashed,->] (-3,1)--(-6,0); +\draw[dashed,->] (-3,1)--(0,5); + +\draw[dashed,->] (1,3)--(3.5,0); +\draw[dashed,->] (1,3)--(3,4); +\end{tikzpicture} +\end{center} +\end{samepage} + +Pisteestä $a$ lähtevät säteet osuvat 1 ja 3 +kertaa monikulmion reunaan, +joten piste on sisäpuolella. +Vastaavasti pisteestä $b$ lähtevät +säteet osuvat 0 ja 2 kertaa monikulmion reunaan, +joten piste on ulkopuolella. + +\section{Monikulmion pinta-ala} + +Yleinen kaava monikulmion pinta-alan laskemiseen on +\[\frac{1}{2} |\sum_{i=1}^{n-1} (p_i \times p_{i+1})| = +\frac{1}{2} |\sum_{i=1}^{n-1} (x_i y_{i+1} - x_{i+1} y_i)|, \] +kun kärkipisteet ovat +$p_1=(x_1,y_1)$, $p_2=(x_2,y_2)$, $\ldots$, $p_n=(x_n,y_n)$ +järjestettynä niin, +että $p_i$ ja $p_{i+1}$ ovat vierekkäiset kärkipisteet +monikulmion reunalla +ja ensimmäinen ja viimeinen kärkipiste ovat samat eli $p_1=p_n$. + +Esimerkiksi monikulmion +\begin{center} +\begin{tikzpicture}[scale=0.7] +\filldraw (4,1.4) circle (2pt); +\filldraw (7,3.4) circle (2pt); +\filldraw (5,5.4) circle (2pt); +\filldraw (2,4.4) circle (2pt); +\filldraw (4,3.4) circle (2pt); +\node (1) at (4,1) {(4,1)}; +\node (2) at (7.2,3) {(7,3)}; +\node (3) at (5,5.8) {(5,5)}; +\node (4) at (2,4) {(2,4)}; +\node (5) at (3.5,3) {(4,3)}; +\path[draw] (4,1.4) -- (7,3.4) -- (5,5.4) -- (2,4.4) -- (4,3.4) -- (4,1.4); +\end{tikzpicture} +\end{center} +pinta-ala on +\[\frac{|(2\cdot5-4\cdot5)+(5\cdot3-5\cdot7)+(7\cdot1-3\cdot4)+(4\cdot3-1\cdot4)+(4\cdot4-3\cdot2)|}{2} = 17/2.\] +Kaavassa on ideana käydä läpi puolisuunnikkaita, +joiden yläreuna on yksi monikulmion sivuista ja +alareuna on vaakataso. Esimerkiksi: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\path[draw,fill=lightgray] (5,5.4) -- (7,3.4) -- (7,0) -- (5,0) -- (5,5.4); +\filldraw (4,1.4) circle (2pt); +\filldraw (7,3.4) circle (2pt); +\filldraw (5,5.4) circle (2pt); +\filldraw (2,4.4) circle (2pt); +\filldraw (4,3.4) circle (2pt); +\node (1) at (4,1) {(4,1)}; +\node (2) at (7.2,3) {(7,3)}; +\node (3) at (5,5.8) {(5,5)}; +\node (4) at (2,4) {(2,4)}; +\node (5) at (3.5,3) {(4,3)}; +\path[draw] (4,1.4) -- (7,3.4) -- (5,5.4) -- (2,4.4) -- (4,3.4) -- (4,1.4); +\draw (0,0) -- (10,0); +\end{tikzpicture} +\end{center} +Tällaisen puolisuunnikkaan pinta-ala on +\[(x_{i+1}-x_{i}) \frac{y_i+y_{i+1}}{2},\] +kun kärkipisteet ovat $p_i$ ja $p_{i+1}$. +Jos $x_{i+1}>x_{i}$, niin pinta-ala on positiivinen, +ja jos $x_{i+1}] (0.5,0.5) -- (16.5,0.5); +\draw (0,0) rectangle (17,-6.5); +\path[draw,thick,-] (10,-1) -- (15,-1); +\path[draw,thick,-] (6,-2.5) -- (12,-2.5); +\path[draw,thick,-] (14,-4) -- (16,-4); +\path[draw,thick,-] (5,-5.5) -- (13,-5.5); + +\draw[fill] (10,-1) circle [radius=0.05]; +\draw[fill] (15,-1) circle [radius=0.05]; +\draw[fill] (6,-2.5) circle [radius=0.05]; +\draw[fill] (12,-2.5) circle [radius=0.05]; +\draw[fill] (14,-4) circle [radius=0.05]; +\draw[fill] (16,-4) circle [radius=0.05]; +\draw[fill] (5,-5.5) circle [radius=0.05]; +\draw[fill] (13,-5.5) circle [radius=0.05]; + +\node at (2,-1) {Uolevi}; +\node at (2,-2.5) {Maija}; +\node at (2,-4) {Kaaleppi}; +\node at (2,-5.5) {Liisa}; + +\path[draw,dashed] (10,0)--(10,-6.5); +\path[draw,dashed] (15,0)--(15,-6.5); +\path[draw,dashed] (6,0)--(6,-6.5); +\path[draw,dashed] (12,0)--(12,-6.5); +\path[draw,dashed] (14,0)--(14,-6.5); +\path[draw,dashed] (16,0)--(16,-6.5); +\path[draw,dashed] (5,0)--(5,-6.5); +\path[draw,dashed] (13,0)--(13,-6.5); + +\node at (10,-7) {$+$}; +\node at (15,-7) {$-$}; +\node at (6,-7) {$+$}; +\node at (12,-7) {$-$}; +\node at (14,-7) {$+$}; +\node at (16,-7) {$-$}; +\node at (5,-7) {$+$}; +\node at (13,-7) {$-$}; + +\node at (10,-8) {$3$}; +\node at (15,-8) {$1$}; +\node at (6,-8) {$2$}; +\node at (12,-8) {$2$}; +\node at (14,-8) {$2$}; +\node at (16,-8) {$0$}; +\node at (5,-8) {$1$}; +\node at (13,-8) {$1$}; +\end{tikzpicture} +\end{center} +Kuvan alareunan merkinnät $+$ ja $-$ +tarkoittavat, että laskurin arvo kasvaa +ja vähenee yhdellä. +Niiden alapuolella on laskurin uusi arvo. +Laskurin suurin arvo 3 on voimassa +Uolevi tulohetken ja Maijan lähtöhetken välillä. + +Ratkaisun aikavaativuus on $O(n \log n)$, +koska tapahtumien järjestäminen vie aikaa $O(n \log n)$ +ja pyyhkäisyviivan läpikäynti vie aikaa $O(n)$. + +\section{Janojen leikkauspisteet} + +\index{leikkauspiste@leikkauspiste} + +Annettuna on $n$ janaa, joista jokainen +on vaaka- tai pystysuuntainen. +Tehtävänä on laskea tehokkaasti, monessako pisteessä +kaksi janaa leikkaavat toisensa. +Esimerkiksi tilanteessa +\begin{center} +\begin{tikzpicture}[scale=0.5] +\path[draw,thick,-] (0,2) -- (5,2); +\path[draw,thick,-] (1,4) -- (6,4); +\path[draw,thick,-] (6,3) -- (10,3); +\path[draw,thick,-] (2,1) -- (2,6); +\path[draw,thick,-] (8,2) -- (8,5); +\end{tikzpicture} +\end{center} +leikkauspisteitä on kolme: +\begin{center} +\begin{tikzpicture}[scale=0.5] +\path[draw,thick,-] (0,2) -- (5,2); +\path[draw,thick,-] (1,4) -- (6,4); +\path[draw,thick,-] (6,3) -- (10,3); +\path[draw,thick,-] (2,1) -- (2,6); +\path[draw,thick,-] (8,2) -- (8,5); + +\draw[fill] (2,2) circle [radius=0.15]; +\draw[fill] (2,4) circle [radius=0.15]; +\draw[fill] (8,3) circle [radius=0.15]; + +\end{tikzpicture} +\end{center} + + +Tehtävä on helppoa ratkaista ajassa $O(n^2)$, +koska riittää käydä läpi kaikki mahdolliset janaparit +ja tarkistaa, moniko leikkaa toisiaan. +Seuraavaksi ratkaisemme tehtävän +ajassa $O(n \log n)$ pyyhkäisyviivan avulla. + +Ideana on luoda janoista kolmenlaisia tapahtumia: +\begin{enumerate}[noitemsep] +\item[(1)] vaakajana alkaa +\item[(2)] vaakajana päättyy +\item[(3)] pystyjana +\end{enumerate} + +Esimerkkitilannetta vastaava pistejoukko on seuraava: +\begin{center} +\begin{tikzpicture}[scale=0.6] +\path[draw,dashed] (0,2) -- (5,2); +\path[draw,dashed] (1,4) -- (6,4); +\path[draw,dashed] (6,3) -- (10,3); +\path[draw,dashed] (2,1) -- (2,6); +\path[draw,dashed] (8,2) -- (8,5); + +\node at (0,2) {$1$}; +\node at (5,2) {$2$}; +\node at (1,4) {$1$}; +\node at (6,4) {$2$}; +\node at (6,3) {$1$}; +\node at (10,3) {$2$}; + +\node at (2,3.5) {$3$}; +\node at (8,3.5) {$3$}; +\end{tikzpicture} +\end{center} + +Algoritmi käy läpi pisteet vasemmalta oikealle +ja pitää yllä tietorakennetta y-koordinaateista, +joissa on tällä hetkellä aktiivinen vaakajana. +Tapahtuman 1 kohdalla vaakajanan y-koordinaatti +lisätään joukkoon ja tapahtuman 2 kohdalla +vaakajanan y-koordinaatti poistetaan joukosta. + +Algoritmi laskee janojen leikkauspisteet +tapahtumien 3 kohdalla. +Kun pystyjana kulkee y-koordinaattien +$y_1 \ldots y_2$ välillä, +algoritmi laskee tietorakenteesta, +monessako vaakajanassa on y-koordinaatti +välillä $y_1 \ldots y_2$ ja kasvattaa +leikkauspisteiden määrää tällä arvolla. + +Sopiva tietorakenne vaakajanojen y-koordinaattien +tallentamiseen on bi\-nää\-ri-indeksipuu tai segmenttipuu, +johon on tarvittaessa yhdistetty indeksien pakkaus. +Tällöin jokaisen pisteen käsittely +vie aikaa $O(\log n)$, joten algoritmin +kokonaisaikavaativuus on $O(n \log n)$. + +\section{Lähin pistepari} + +\index{lzhin pistepari@lähin pistepari} + +Seuraava tehtävämme on etsiä $n$ pisteen +joukosta kaksi pistettä, jotka ovat +mahdollisimman lähellä toisiaan. +Esimerkiksi tilanteessa +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0)--(12,0)--(12,4)--(0,4)--(0,0); + +\draw (1,2) circle [radius=0.1]; +\draw (3,1) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5.5,1.5) circle [radius=0.1]; +\draw (6,2.5) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (9,1.5) circle [radius=0.1]; +\draw (10,2) circle [radius=0.1]; +\draw (1.5,3.5) circle [radius=0.1]; +\draw (1.5,1) circle [radius=0.1]; +\draw (2.5,3) circle [radius=0.1]; +\draw (4.5,1.5) circle [radius=0.1]; +\draw (5.25,0.5) circle [radius=0.1]; +\draw (6.5,2) circle [radius=0.1]; +\end{tikzpicture} +\end{center} +\begin{samepage} +lähin pistepari on seuraava: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0)--(12,0)--(12,4)--(0,4)--(0,0); + +\draw (1,2) circle [radius=0.1]; +\draw (3,1) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5.5,1.5) circle [radius=0.1]; +\draw[fill] (6,2.5) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (9,1.5) circle [radius=0.1]; +\draw (10,2) circle [radius=0.1]; +\draw (1.5,3.5) circle [radius=0.1]; +\draw (1.5,1) circle [radius=0.1]; +\draw (2.5,3) circle [radius=0.1]; +\draw (4.5,1.5) circle [radius=0.1]; +\draw (5.25,0.5) circle [radius=0.1]; +\draw[fill] (6.5,2) circle [radius=0.1]; +\end{tikzpicture} +\end{center} +\end{samepage} + +Tämäkin tehtävä ratkeaa +$O(n \log n)$-ajassa pyyhkäisyviivan avulla. +Algoritmi käy pisteet läpi vasemmalta oikealle +ja pitää yllä arvoa $d$, +joka on pienin kahden +pisteen etäisyys. +Kunkin pisteen kohdalla algoritmi +etsii lähimmän toisen pisteen vasemmalta. +Jos etäisyys tähän pisteeseen on alle $d$, +tämä on uusi pienin kahden pisteen etäisyys +ja algoritmi päivittää $d$:n arvon. + +Jos käsiteltävä piste on $(x,y)$ +ja jokin vasemmalla oleva piste on +alle $d$:n etäisyydellä, +sen x-koordinaatin +tulee olla välillä $[x-d,x]$ +ja y-koordinaatin tulee olla välillä $[y-d,y+d]$. +Algoritmin riittää siis tarkistaa +ainoastaan pisteet, jotka osuvat tälle välille, +mikä tehostaa hakua merkittävästi. + +Esimerkiksi seuraavassa kuvassa +katkoviiva-alue sisältää pisteet, +jotka voivat olla alle $d$:n etäisyydellä +tummennetusta pisteestä. +\\ +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0)--(12,0)--(12,4)--(0,4)--(0,0); + +\draw (1,2) circle [radius=0.1]; +\draw (3,1) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5.5,1.5) circle [radius=0.1]; +\draw (6,2.5) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (9,1.5) circle [radius=0.1]; +\draw (10,2) circle [radius=0.1]; +\draw (1.5,3.5) circle [radius=0.1]; +\draw (1.5,1) circle [radius=0.1]; +\draw (2.5,3) circle [radius=0.1]; +\draw (4.5,1.5) circle [radius=0.1]; +\draw (5.25,0.5) circle [radius=0.1]; +\draw[fill] (6.5,2) circle [radius=0.1]; + +\draw[dashed] (6.5,0.75)--(6.5,3.25); +\draw[dashed] (5.25,0.75)--(5.25,3.25); +\draw[dashed] (5.25,0.75)--(6.5,0.75); +\draw[dashed] (5.25,3.25)--(6.5,3.25); + +\draw [decoration={brace}, decorate, line width=0.3mm] (5.25,3.5) -- (6.5,3.5); +\node at (5.875,4) {$d$}; +\draw [decoration={brace}, decorate, line width=0.3mm] (6.75,3.25) -- (6.75,2); +\node at (7.25,2.625) {$d$}; +\end{tikzpicture} +\end{center} + +Algoritmin tehokkuus perustuu siihen, +että $d$:n rajoittamalla alueella +on aina vain $O(1)$ pistettä. +Nämä pisteet pystyy käymään läpi +$O(\log n)$-aikaisesti +pitämällä algoritmin aikana yllä joukkoa pisteistä, +joiden x-koordinaatti on välillä $[x-d,x]$ +ja jotka on järjestetty y-koordinaatin mukaan. + +Algoritmin aikavaativuus on $O(n \log n)$, +koska se käy läpi $n$ pistettä +ja etsii jokaiselle lähimmän +edeltävän pisteen ajassa $O(\log n)$. + +\section{Konveksi peite} + +\key{Konveksi peite} +on pienin konveksi monikulmio, +joka ympäröi kaikki pistejoukon pisteet. +Konveksius tarkoittaa, +että minkä tahansa kahden kärkipisteen välinen jana +kulkee monikulmion sisällä. +Hyvä mielikuva asiasta on, +että pistejoukko ympäröidään tiukasti +viritetyllä narulla. + +\begin{samepage} +Esimerkiksi pistejoukon +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; +\end{tikzpicture} +\end{center} +\end{samepage} +konveksi peite on seuraava: +\begin{center} +\begin{tikzpicture}[scale=0.7] +\draw (0,0)--(4,-1)--(7,1)--(6,3)--(2,4)--(0,2)--(0,0); + +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; +\end{tikzpicture} +\end{center} + +\index{Andrew'n algoritmi} + +Tehokas ja helposti toteutettava menetelmä +konveksin peitteen muodostamiseen +on \key{Andrew'n algoritmi}, +jonka aikavaativuus on $O(n \log n)$. +Algoritmi muodostaa konveksin peitteen kahdessa +osassa: ensin peitteen yläosan ja sitten peitteen alaosan. +Kummankin osan muodostaminen tapahtuu samalla tavalla, +minkä vuoksi voimme keskittyä yläosan muodostamiseen. + +Algoritmi järjestää ensin pisteet ensisijaisesti x-koordinaatin +ja toissijaisesti y-koordinaatin mukaan. +Tämän jälkeen se käy pisteet läpi järjestyksessä +ja lisää aina uuden pisteen osaksi peitettä. +Aina pisteen lisäämisen jälkeen algoritmi tarkastaa +ristitulon avulla, +muodostavatko kolme viimeistä pistettä peitteessä +vasemmalle kääntyvän osan. +Jos näin on, +algoritmi poistaa näistä keskimmäisen pisteen. +Tämän jälkeen algoritmi tarkastaa uudestaan +kolme viimeistä pistettä ja poistaa taas tarvittaessa +keskimmäisen pisteen. +Sama jatkuu, kunnes kolme viimeistä pistettä +eivät muodosta vasemmalle kääntyvää osaa. + +Seuraava kuvasarja esittää Andrew'n algoritmin toimintaa: +\\ +\begin{tabular}{ccccccc} +\\ +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(1,1); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(1,1)--(2,2); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,2); +\end{tikzpicture} +\\ +1 & & 2 & & 3 & & 4 \\ +\end{tabular} +\\ +\begin{tabular}{ccccccc} +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,2)--(2,4); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(3,2); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(3,2)--(4,-1); +\end{tikzpicture} +\\ +5 & & 6 & & 7 & & 8 \\ +\end{tabular} +\\ +\begin{tabular}{ccccccc} +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(3,2)--(4,-1)--(4,0); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(3,2)--(4,0); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(3,2)--(4,0)--(4,3); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(3,2)--(4,3); +\end{tikzpicture} +\\ +9 & & 10 & & 11 & & 12 \\ +\end{tabular} +\\ +\begin{tabular}{ccccccc} +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(4,3); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(4,3)--(5,2); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(4,3)--(5,2)--(6,1); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(4,3)--(5,2)--(6,1)--(6,3); +\end{tikzpicture} +\\ +13 & & 14 & & 15 & & 16 \\ +\end{tabular} +\\ +\begin{tabular}{ccccccc} +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(4,3)--(5,2)--(6,3); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(4,3)--(6,3); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(6,3); +\end{tikzpicture} +& \hspace{0.1cm} & +\begin{tikzpicture}[scale=0.3] +\draw (-1,-2)--(8,-2)--(8,5)--(-1,5)--(-1,-2); +\draw (0,0) circle [radius=0.1]; +\draw (4,-1) circle [radius=0.1]; +\draw (7,1) circle [radius=0.1]; +\draw (6,3) circle [radius=0.1]; +\draw (2,4) circle [radius=0.1]; +\draw (0,2) circle [radius=0.1]; + +\draw (1,1) circle [radius=0.1]; +\draw (2,2) circle [radius=0.1]; +\draw (3,2) circle [radius=0.1]; +\draw (4,0) circle [radius=0.1]; +\draw (4,3) circle [radius=0.1]; +\draw (5,2) circle [radius=0.1]; +\draw (6,1) circle [radius=0.1]; + +\draw (0,0)--(0,2)--(2,4)--(6,3)--(7,1); +\end{tikzpicture} +\\ +17 & & 18 & & 19 & & 20 +\end{tabular} + + + +