First commit

This commit is contained in:
Antti H S Laaksonen 2016-12-29 00:54:51 +02:00
commit c210d9497b
32 changed files with 24432 additions and 0 deletions

26
johdanto.tex Normal file
View File

@ -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}.

113
kkkk.tex Normal file
View File

@ -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}

835
luku01.tex Normal file
View File

@ -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 <bits/stdc++.h>
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<int> vi;
typedef pair<int,int> 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$.

551
luku02.tex Normal file
View File

@ -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.

972
luku03.tex Normal file
View File

@ -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<b$ ja $\texttt{t}[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<y?$'' tarkoittaa taulukon alkioiden
$x$ ja $y$ vertailua.
Jos $x<y$, prosessi jatkaa vasemmalle,
ja muuten oikealle.
Prosessin tulokset ovat taulukon mahdolliset
järjestykset, joita on kaikkiaan $n!$ erilaista.
Puun korkeuden tulee olla tämän vuoksi vähintään
\[ \log_2(n!) = \log_2(1)+\log_2(2)+\cdots+\log_2(n).\]
Voimme arvioida tätä summaa alaspäin
valitsemalla summasta $n/2$
viimeistä termiä ja muuttamalla kunkin
termin arvoksi $\log_2(n/2)$.
Tästä saadaan arvio
\[ \log_2(n!) \ge (n/2) \cdot \log_2(n/2),\]
eli puun korkeus ja sen myötä
pienin mahdollinen järjestämisalgoritmin askelten
määrä on pahimmassa tapauksessa ainakin luokkaa $n \log n$.
\subsubsection{Laskemisjärjestäminen}
\index{laskemisjxrjestxminen@laskemisjärjestäminen}
Järjestämisen alaraja $n \log n$ ei koske algoritmeja,
jotka eivät perustu alkioiden vertailemiseen
vaan hyödyntävät jotain muuta tietoa alkioista.
Esimerkki tällaisesta algoritmista on
\key{laskemisjärjestäminen}, jonka avulla
on mahdollista järjestää
taulukko ajassa $O(n)$ olettaen,
että jokainen taulukon alkio on
kokonaisluku välillä $0 \ldots c$,
missä $c$ on pieni vakio.
Algoritmin ideana on luoda \emph{kirjanpito}, josta selviää,
montako kertaa mikäkin alkio esiintyy taulukossa.
Kirjanpito on taulukko, jonka indeksit ovat alkuperäisen
taulukon alkioita.
Jokaisen indeksin kohdalla lukee, montako kertaa
kyseinen alkio esiintyy alkuperäisessä taulukossa.
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) {$6$};
\node at (3.5,0.5) {$9$};
\node at (4.5,0.5) {$9$};
\node at (5.5,0.5) {$3$};
\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}
syntyy seuraava kirjanpito:
\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) {$0$};
\node at (2.5,0.5) {$2$};
\node at (3.5,0.5) {$0$};
\node at (4.5,0.5) {$1$};
\node at (5.5,0.5) {$1$};
\node at (6.5,0.5) {$0$};
\node at (7.5,0.5) {$0$};
\node at (8.5,0.5) {$3$};
\footnotesize
\node at (0.5,1.5) {$1$};
\node at (1.5,1.5) {$2$};
\node at (2.5,1.5) {$3$};
\node at (3.5,1.5) {$4$};
\node at (4.5,1.5) {$5$};
\node at (5.5,1.5) {$6$};
\node at (6.5,1.5) {$7$};
\node at (7.5,1.5) {$8$};
\node at (8.5,1.5) {$9$};
\end{tikzpicture}
\end{center}
Esimerkiksi kirjanpidossa lukee indeksin 3 kohdalla 2,
koska luku 3 esiintyy kahdesti alkuperäisessä
taulukossa (indekseissä 2 ja 6).
Kirjanpidon muodostus vie aikaa $O(n)$,
koska riittää käydä taulukko läpi kerran.
Tämän jälkeen järjestetyn taulukon luominen
vie myös aikaa $O(n)$, koska kunkin alkion
määrän saa selville suoraan kirjanpidosta.
Niinpä laskemisjärjestämisen
kokonaisaikavaativuus on $O(n)$.
Laskemisjärjestäminen on hyvin tehokas algoritmi,
mutta sen käyttäminen vaatii,
että vakio $c$ on niin pieni,
että taulukon alkioita voi käyttää
kirjanpidon taulukon indeksöinnissä.
\section{Järjestäminen C++:ssa}
\index{sort@\texttt{sort}}
Järjestämisalgoritmia
ei yleensä koskaan kannata koodata itse,
vaan on parempi ratkaisu käyttää
ohjelmointikielen valmista toteutusta.
Esimerkiksi
C++:n standardikirjastossa on funktio \texttt{sort},
jonka avulla voi järjestää helposti taulukoita
ja muita tietorakenteita.
Valmiin toteutuksen käyttämisessä on monia etuja.
Ensinnäkin se säästää aikaa, koska järjestämisalgoritmia
ei tarvitse kirjoittaa itse.
Lisäksi valmis toteutus on varmasti tehokas ja toimiva:
vaikka järjestämisalgoritmin toteuttaisi itse,
siitä tulisi tuskin valmista toteutusta parempi.
Tutustumme seuraavaksi C++:n \texttt{sort}-funktion
käyttämiseen.
Seuraava koodi järjestää vektorin \texttt{v}
luvut pienimmästä suurimpaan:
\begin{lstlisting}
vector<int> 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<pair<int,int>> 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<tuple<int,int,int>> 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<k$ ja \texttt{true} aina kun $x \geq k$.
% Toisin sanoen haluamme löytää funktion \texttt{ok} \emph{muutoskohdan},
% jossa arvosta \texttt{false} tulee arvo \texttt{true}.
Tilanne näyttää seuraavalta:
\begin{center}
\begin{tabular}{r|rrrrrrrr}
$x$ & 0 & 1 & $\cdots$ & $k-1$ & $k$ & $k+1$ & $\cdots$ \\
\hline
$\texttt{ok}(x)$ & \texttt{false} & \texttt{false}
& $\cdots$ & \texttt{false} & \texttt{true} & \texttt{true} & $\cdots$ \\
\end{tabular}
\end{center}
\noindent
Nyt muutoskohta on mahdollista etsiä käyttämällä
binäärihakua:
\begin{lstlisting}
int x = -1;
for (int b = z; b >= 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$, ja
\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+1)$.
Tällöin $k=x+1$,
koska pätee $f(x+1)>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.

774
luku04.tex Normal file
View File

@ -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<int> 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<int> 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<int> 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<int> v(10);
\end{lstlisting}
\begin{lstlisting}
// koko 10, alkuarvo 5
vector<int> 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<int> 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<int> 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<int> 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<int> 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<string,int> 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<string,int> 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<int>::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<int> 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<int> 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<int> 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<int> 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<int,vector<int>,greater<int>> 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 45 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.

756
luku05.tex Normal file
View File

@ -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<<n); b++) {
// käsittele osajoukko b
}
\end{lstlisting}
Seuraava koodi muodostaa jokaisen osajoukon
kohdalla vektorin \texttt{v},
joka sisältää osajoukossa olevat luvut.
Ne saadaan selville tutkimalla, mitkä bitit ovat
ykkösiä osajoukon bittiesityksessä.
\begin{lstlisting}
for (int b = 0; b < (1<<n); b++) {
vector<int> v;
for (int i = 0; i < n; i++) {
if (b&(1<<i)) v.push_back(i+1);
}
}
\end{lstlisting}
\section{Permutaatioiden läpikäynti}
\index{permutaatio@permutaatio}
Toinen usein esiintyvä tilanne on,
että tehtävän ratkaisut ovat $n$-alkioisen
joukon permutaatioita,
jolloin täydellisen haun tulee
käydä läpi $n!$ mahdollista permutaatiota.
Myös tässä tapauksessa on kaksi luontevaa
menetelmää täydellisen haun toteuttamiseen.
\subsubsection{Menetelmä 1}
Osajoukkojen tavoin permutaatioita voi muodostaa
rekursiivisesti.
Seuraava funktio \texttt{haku} käy läpi
joukon $\{1,2,\ldots,n\}$ permutaatiot.
Funktio muodostaa kunkin permutaation
vuorollaan vektoriin \texttt{v}.
Permutaatioiden muodostus alkaa kutsumalla
funktiota ilman parametreja.
\begin{lstlisting}
void haku() {
if (v.size() == n) {
// käsittele permutaatio v
} else {
for (int i = 1; i <= n; i++) {
if (p[i]) continue;
p[i] = 1;
v.push_back(i);
haku();
p[i] = 0;
v.pop_back();
}
}
}
\end{lstlisting}
Funktion jokainen kutsu lisää uuden
luvun permutaatioon vektoriin \texttt{v}.
Taulukko \texttt{p} kertoo, mitkä luvut on jo
valittu permutaatioon.
Jos $\texttt{p}[k]=0$, luku $k$ ei ole mukana,
ja jos $\texttt{p}[k]=1$, luku $k$ on mukana.
Jos vektorin \texttt{v} koko on sama kuin
joukon koko $n$, permutaatio on tullut valmiiksi.
\subsubsection{Menetelmä 2}
\index{next\_permutation@\texttt{next\_permutation}}
Toinen ratkaisu on aloittaa permutaatiosta
$\{1,2,\ldots,n\}$ ja muodostaa joka askeleella
järjestyksessä seuraava permutaatio.
C++:n standardikirjastossa on funktio
\texttt{next\_permutation}, joka tekee tämän muunnoksen.
Seuraava koodi käy läpi joukon $\{1,2,\ldots,n\}$
permutaatiot funktion avulla:
\begin{lstlisting}
vector<int> 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.

789
luku06.tex Normal file
View File

@ -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}\]

976
luku07.tex Normal file
View File

@ -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<k$
ja $x_i<x_k$.
Pisin nouseva alijono saadaan liittämällä
kohtaan $i$ päättyvän pisimmän nousevan alijonon perään luku $x_k$.
Tällöin $f(k)=f(i)+1$.
\end{enumerate}
Tarkastellaan esimerkkinä arvon $f(7)$ laskemista.
Paras ratkaisu on ottaa pohjaksi kohtaan 5
päättyvä pisin nouseva alijono $[2,5,7]$
ja lisätä sen perään luku $x_7=8$.
Tuloksena on alijono $[2,5,7,8]$ ja $f(7)=f(5)+1=4$.
Suoraviivainen tapa toteuttaa algoritmi on
käydä kussakin kohdassa $k$ läpi kaikki kohdat
$i=1,2,\ldots,k-1$, joissa voi olla alijonon
edellinen luku.
Tällaisen algoritmin aikavaativuus on $O(n^2)$.
Yllättävää kyllä, algoritmin voi toteuttaa myös
ajassa $O(n \log n)$, mutta tämä on vaikeampaa.
\section{Reitinhaku ruudukossa}
Seuraava tehtävämme on etsiä reitti
$n \times n$ -ruudukon vasemmasta yläkulmasta
oikeaan alakulmaan.
Jokaisessa ruudussa on luku, ja reitti
tulee muodostaa niin, että reittiin kuuluvien
lukujen summa on mahdollisimman suuri.
Rajoituksena ruudukossa on mahdollista
liikkua vain oikealla ja alaspäin.
Seuraavassa ruudukossa paras reitti
on merkitty harmaalla taustalla:
\begin{center}
\begin{tikzpicture}[scale=.65]
\begin{scope}
\fill [color=lightgray] (0, 9) rectangle (1, 8);
\fill [color=lightgray] (0, 8) rectangle (1, 7);
\fill [color=lightgray] (1, 8) rectangle (2, 7);
\fill [color=lightgray] (1, 7) rectangle (2, 6);
\fill [color=lightgray] (2, 7) rectangle (3, 6);
\fill [color=lightgray] (3, 7) rectangle (4, 6);
\fill [color=lightgray] (4, 7) rectangle (5, 6);
\fill [color=lightgray] (4, 6) rectangle (5, 5);
\fill [color=lightgray] (4, 5) rectangle (5, 4);
\draw (0, 4) grid (5, 9);
\node at (0.5,8.5) {3};
\node at (1.5,8.5) {7};
\node at (2.5,8.5) {9};
\node at (3.5,8.5) {2};
\node at (4.5,8.5) {7};
\node at (0.5,7.5) {9};
\node at (1.5,7.5) {8};
\node at (2.5,7.5) {3};
\node at (3.5,7.5) {5};
\node at (4.5,7.5) {5};
\node at (0.5,6.5) {1};
\node at (1.5,6.5) {7};
\node at (2.5,6.5) {9};
\node at (3.5,6.5) {8};
\node at (4.5,6.5) {5};
\node at (0.5,5.5) {3};
\node at (1.5,5.5) {8};
\node at (2.5,5.5) {6};
\node at (3.5,5.5) {4};
\node at (4.5,5.5) {10};
\node at (0.5,4.5) {6};
\node at (1.5,4.5) {3};
\node at (2.5,4.5) {9};
\node at (3.5,4.5) {7};
\node at (4.5,4.5) {8};
\end{scope}
\end{tikzpicture}
\end{center}
Tällä reitillä lukujen summa on $3+9+8+7+9+8+5+10+8=67$,
joka on suurin mahdollinen summa vasemmasta yläkulmasta
oikeaan alakulmaan.
Hyvä lähestymistapa tehtävään on laskea
kuhunkin ruutuun $(y,x)$ suurin summa
reitillä vasemmasta yläkulmasta kyseiseen ruutuun.
Merkitään tätä suurinta summaa $f(y,x)$,
jolloin $f(n,n)$ on suurin summa
reitillä vasemmasta yläkulmasta oikeaan alakulmaan.
Rekursio syntyy havainnosta,
että ruutuun $(y,x)$ saapuvan reitin
täytyy tulla joko vasemmalta ruudusta $(y,x-1)$
tai ylhäältä ruudusta $(y-1,x)$:
\begin{center}
\begin{tikzpicture}[scale=.65]
\begin{scope}
\fill [color=lightgray] (3, 7) rectangle (4, 6);
\draw (0, 4) grid (5, 9);
\node at (2.5,6.5) {$\rightarrow$};
\node at (3.5,7.5) {$\downarrow$};
\end{scope}
\end{tikzpicture}
\end{center}
Kun $r(y,x)$
on ruudukon luku kohdassa $(y,x)$,
rekursion pohjatapaukset ovat seuraavat:
\[
\begin{array}{lcl}
f(1,1) & = & r(1,1) \\
f(1,x) & = & f(1,x-1)+r(1,x) \\
f(y,1) & = & f(y-1,1)+r(y,1)\\
\end{array}
\]
Yleisessä tapauksessa valittavana on
kaksi reittiä,
joista kannattaa valita se,
joka tuottaa suuremman summan:
\[ f(y,x) = \max(f(y,x-1),f(y-1,x))+r(y,x)\]
Ratkaisun aikavaativuus on $O(n^2)$, koska jokaisessa
ruudussa $f(y,x)$ saadaan laskettua vakioajassa
viereisten ruutujen arvoista.
\section{Repunpakkaus}
\index{repunpakkaus@repunpakkaus}
\key{Repunpakkaus} on klassinen ongelma,
jossa annettuna on $n$ tavaraa,
joiden painot ovat
$p_1,p_2,\ldots,p_n$ ja arvot ovat
$a_1,a_2,\ldots,a_n$.
Tehtävänä on valita reppuun pakattavat tavarat
niin, että tavaroiden
painojen summa on enintään $x$
ja tavaroiden arvojen summa on mahdollisimman suuri.
\begin{samepage}
Esimerkiksi jos tavarat ovat
\begin{center}
\begin{tabular}{rrr}
tavara & paino & arvo \\
\hline
A & 5 & 1 \\
B & 6 & 3 \\
C & 8 & 5 \\
D & 5 & 3 \\
\end{tabular}
\end{center}
\end{samepage}
ja suurin sallittu yhteispaino on 12,
niin paras ratkaisu on pakata reppuun tavarat $B$ ja $D$.
Niiden yhteispaino $6+5=11$ ei ylitä rajaa 12
ja arvojen summa
on $3+3=6$, mikä on paras mahdollinen tulos.
Tämä tehtävä on mahdollista ratkaista kahdella eri
tavalla dynaamisella ohjelmoinnilla
riippuen siitä, tarkastellaanko ongelmaa
maksimointina vai minimointina.
Käymme seuraavaksi läpi molemmat ratkaisut.
\subsubsection{Ratkaisu 1}
\textit{Maksimointi:} Merkitään $f(k,u)$
suurinta mahdollista tavaroiden yhteisarvoa,
kun reppuun pakataan jokin osajoukko
tavaroista $1 \ldots k$,
jossa tavaroiden yhteispaino on $u$.
Ratkaisu tehtävään on suurin arvo
$f(n,u)$, kun $0 \le u \le x$.
Rekursiivinen kaava funktion laskemiseksi on
\[f(k,u) = \max(f(k-1,u),f(k-1,u-p_k)+a_k),\]
koska kohdassa $k$ oleva tavara joko otetaan tai ei oteta
mukaan ratkaisuun.
Pohjatapauksina on $f(0,0)=0$ ja $f(0,u)=-\infty$,
kun $u \neq 0$. Tämän ratkaisun aikavaativuus on $O(nx)$.
Esimerkin tilanteessa optimiratkaisu on
$f(4,11)=6$, joka muodostuu seuraavan ketjun kautta:
\[f(4,11)=f(3,6)+3=f(2,6)+3=f(1,0)+3+3=f(0,0)+3+3=6.\]
\subsubsection{Ratkaisu 2}
\textit{Minimointi:} Merkitään $f(k,u)$
pienintä mahdollista tavaroiden yhteispainoa,
kun reppuun pakataan jokin osajoukko
tavaroista $1 \ldots k$,
jossa tavaroiden yhteisarvo on $u$.
Ratkaisu tehtävään on suurin arvo $u$,
jolle pätee $0 \le u \le s$ ja $f(n,u) \le x$,
missä $s=\sum_{i=1}^n a_i$.
Rekursiivinen kaava funktion laskemiseksi on
\[f(k,u) = \min(f(k-1,u),f(k-1,u-a_k)+p_k)\]
ratkaisua 1 vastaavasti.
Pohjatapauksina on $f(0,0)=0$ ja $f(0,u)=\infty$, kun $u \neq 0$.
Tämän ratkaisun aikavaativuus on $O(ns)$.
Esimerkin tilanteessa optimiratkaisu on
$f(4,6)=11$, joka muodostuu seuraavan ketjun kautta:
\[f(4,6)=f(3,3)+5=f(2,3)+5=f(1,0)+6+5=f(0,0)+6+5=11.\]
~\\
Kiinnostava seikka on, että eri asiat syötteessä
vaikuttavat ratkaisuiden tehokkuuteen.
Ratkaisussa 1 tavaroiden painot vaikuttavat tehokkuuteen
mutta arvoilla ei ole merkitystä.
Ratkaisussa 2 puolestaan tavaroiden arvot vaikuttavat
tehokkuuteen mutta painoilla ei ole merkitystä.
\section{Editointietäisyys}
\index{editointietxisyys@editointietäisyys}
\index{Levenšteinin etäisyys}
\key{Editointietäisyys} eli
\key{Levenšteinin etäisyys}
kuvaa, kuinka kaukana kaksi merkkijonoa ovat toisistaan.
Se on pienin määrä editointioperaatioita,
joilla ensimmäisen merkkijonon saa muutettua toiseksi.
Sallitut operaatiot ovat:
\begin{itemize}
\item merkin lisäys (esim. \texttt{ABC} $\rightarrow$ \texttt{ABCA})
\item merkin poisto (esim. \texttt{ABC} $\rightarrow$ \texttt{AC})
\item merkin muutos (esim. \texttt{ABC} $\rightarrow$ \texttt{ADC})
\end{itemize}
Esimerkiksi merkkijonojen \texttt{TALO} ja \texttt{PALLO}
editointietäisyys on 2, koska voimme tehdä ensin
operaation \texttt{TALO} $\rightarrow$ \texttt{TALLO}
(merkin lisäys) ja sen jälkeen operaation
\texttt{TALLO} $\rightarrow$ \texttt{PALLO}
(merkin muutos).
Tämä on pienin mahdollinen määrä operaatioita, koska
selvästikään yksi operaatio ei riitä.
Oletetaan, että annettuna on merkkijonot
\texttt{x} (pituus $n$ merkkiä) ja
\texttt{y} (pituus $m$ merkkiä),
ja haluamme laskea niiden editointietäisyyden.
Tämä onnistuu tehokkaasti dynaamisella
ohjelmoinnilla ajassa $O(nm)$.
Merkitään funktiolla $f(a,b)$
editointietäisyyttä \texttt{x}:n $a$
ensimmäisen merkin sekä
\texttt{y}:n $b$:n ensimmäisen merkin välillä.
Tätä funktiota käyttäen
merkkijonojen
\texttt{x} ja \texttt{y} editointietäisyys
on $f(n,m)$, ja funktio kertoo myös tarvittavat
editointioperaatiot.
Funktion pohjatapaukset ovat
\[
\begin{array}{lcl}
f(0,b) & = & b \\
f(a,0) & = & a \\
\end{array}
\]
ja yleisessä tapauksessa pätee kaava
\[ f(a,b) = \min(f(a,b-1)+1,f(a-1,b)+1,f(a-1,b-1)+c),\]
missä $c=0$, jos \texttt{x}:n merkki $a$
ja \texttt{y}:n merkki $b$ ovat samat,
ja muussa tapauksessa $c=1$.
Kaava käy läpi mahdollisuudet lyhentää merkkijonoja:
\begin{itemize}
\item $f(a,b-1)$ tarkoittaa, että $x$:ään lisätään merkki
\item $f(a-1,b)$ tarkoittaa, että $x$:stä poistetaan merkki
\item $f(a-1,b-1)$ tarkoittaa, että $x$:ssä ja $y$:ssä on
sama merkki ($c=0$) tai $x$:n merkki muutetaan $y$:n merkiksi ($c=1$)
\end{itemize}
Seuraava taulukko sisältää funktion $f$ arvot
esimerkin tapauksessa:
\begin{center}
\begin{tikzpicture}[scale=.65]
\begin{scope}
%\fill [color=lightgray] (5, -3) rectangle (6, -4);
\draw (1, -1) grid (7, -6);
\node at (0.5,-2.5) {\texttt{T}};
\node at (0.5,-3.5) {\texttt{A}};
\node at (0.5,-4.5) {\texttt{L}};
\node at (0.5,-5.5) {\texttt{O}};
\node at (2.5,-0.5) {\texttt{P}};
\node at (3.5,-0.5) {\texttt{A}};
\node at (4.5,-0.5) {\texttt{L}};
\node at (5.5,-0.5) {\texttt{L}};
\node at (6.5,-0.5) {\texttt{O}};
\node at (1.5,-1.5) {$0$};
\node at (1.5,-2.5) {$1$};
\node at (1.5,-3.5) {$2$};
\node at (1.5,-4.5) {$3$};
\node at (1.5,-5.5) {$4$};
\node at (2.5,-1.5) {$1$};
\node at (2.5,-2.5) {$1$};
\node at (2.5,-3.5) {$2$};
\node at (2.5,-4.5) {$3$};
\node at (2.5,-5.5) {$4$};
\node at (3.5,-1.5) {$2$};
\node at (3.5,-2.5) {$2$};
\node at (3.5,-3.5) {$1$};
\node at (3.5,-4.5) {$2$};
\node at (3.5,-5.5) {$3$};
\node at (4.5,-1.5) {$3$};
\node at (4.5,-2.5) {$3$};
\node at (4.5,-3.5) {$2$};
\node at (4.5,-4.5) {$1$};
\node at (4.5,-5.5) {$2$};
\node at (5.5,-1.5) {$4$};
\node at (5.5,-2.5) {$4$};
\node at (5.5,-3.5) {$3$};
\node at (5.5,-4.5) {$2$};
\node at (5.5,-5.5) {$2$};
\node at (6.5,-1.5) {$5$};
\node at (6.5,-2.5) {$5$};
\node at (6.5,-3.5) {$4$};
\node at (6.5,-4.5) {$3$};
\node at (6.5,-5.5) {$2$};
\end{scope}
\end{tikzpicture}
\end{center}
Taulukon oikean alanurkan ruutu
kertoo, että merkkijonojen \texttt{TALO}
ja \texttt{PALLO} editointietäisyys on 2.
Taulukosta pystyy myös
lukemaan, miten pienimmän editointietäisyyden
voi saavuttaa.
Tässä tapauksessa polku on seuraava:
\begin{center}
\begin{tikzpicture}[scale=.65]
\begin{scope}
\draw (1, -1) grid (7, -6);
\node at (0.5,-2.5) {\texttt{T}};
\node at (0.5,-3.5) {\texttt{A}};
\node at (0.5,-4.5) {\texttt{L}};
\node at (0.5,-5.5) {\texttt{O}};
\node at (2.5,-0.5) {\texttt{P}};
\node at (3.5,-0.5) {\texttt{A}};
\node at (4.5,-0.5) {\texttt{L}};
\node at (5.5,-0.5) {\texttt{L}};
\node at (6.5,-0.5) {\texttt{O}};
\node at (1.5,-1.5) {$0$};
\node at (1.5,-2.5) {$1$};
\node at (1.5,-3.5) {$2$};
\node at (1.5,-4.5) {$3$};
\node at (1.5,-5.5) {$4$};
\node at (2.5,-1.5) {$1$};
\node at (2.5,-2.5) {$1$};
\node at (2.5,-3.5) {$2$};
\node at (2.5,-4.5) {$3$};
\node at (2.5,-5.5) {$4$};
\node at (3.5,-1.5) {$2$};
\node at (3.5,-2.5) {$2$};
\node at (3.5,-3.5) {$1$};
\node at (3.5,-4.5) {$2$};
\node at (3.5,-5.5) {$3$};
\node at (4.5,-1.5) {$3$};
\node at (4.5,-2.5) {$3$};
\node at (4.5,-3.5) {$2$};
\node at (4.5,-4.5) {$1$};
\node at (4.5,-5.5) {$2$};
\node at (5.5,-1.5) {$4$};
\node at (5.5,-2.5) {$4$};
\node at (5.5,-3.5) {$3$};
\node at (5.5,-4.5) {$2$};
\node at (5.5,-5.5) {$2$};
\node at (6.5,-1.5) {$5$};
\node at (6.5,-2.5) {$5$};
\node at (6.5,-3.5) {$4$};
\node at (6.5,-4.5) {$3$};
\node at (6.5,-5.5) {$2$};
\path[draw=red,thick,-,line width=2pt] (6.5,-5.5) -- (5.5,-4.5);
\path[draw=red,thick,-,line width=2pt] (5.5,-4.5) -- (4.5,-4.5);
\path[draw=red,thick,->,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.

839
luku08.tex Normal file
View File

@ -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<n && s+t[b+1] <= x) {
% b++;
% s += t[b];
% }
% if (s == x) {
% // ratkaisu löytyi
% }
% s -= t[a];
% }
% \end{lstlisting}
%
% Muuttujat $a$ ja $b$ sisältävät vasemman ja oikean
% osoittimen kohdan.
% Muuttuja $s$ taas laskee lukujen summan välillä.
% Joka askeleella $a$ liikkuu askeleen eteenpäin
% ja $b$ liikkuu niin kauan kuin summa on enintään $x$.
Algoritmin aikavaativuus riippuu siitä,
kauanko oikean osoittimen liikkuminen vie aikaa.
Tämä vaihtelee, koska oikea osoitin voi liikkua
minkä tahansa matkan eteenpäin taulukossa.
Kuitenkin oikea osoitin liikkuu \textit{yhteensä}
$O(n)$ askelta algoritmin aikana, koska se voi
liikkua vain eteenpäin.
Koska sekä vasen että oikea osoitin liikkuvat
$O(n)$ askelta algoritmin aikana,
algoritmin aikavaativuus on $O(n)$.
\subsubsection{Kahden luvun summa}
\index{2SUM-ongelma}
Annettuna on taulukko, jossa on $n$ kokonaislukua,
sekä kokonaisluku $x$.
Tehtävänä on etsiä taulukosta kaksi lukua,
joiden summa on $x$, tai todeta,
että tämä ei ole mahdollista.
Tämä ongelma tunnetaan tunnetaan nimellä
\key{2SUM} ja se ratkeaa tehokkaasti
kahden osoittimen tekniikalla.
Taulukon luvut järjestetään ensin
pienimmästä suurimpaan, minkä jälkeen
taulukkoa aletaan käydä läpi kahdella osoittimella,
jotka lähtevät liikkelle taulukon molemmista päistä.
Vasen osoitin aloittaa taulukon alusta ja
liikkuu joka vaiheessa askeleen eteenpäin.
Oikea osoitin taas aloittaa taulukon lopusta
ja peruuttaa vuorollaan taaksepäin, kunnes osoitinten
määrittämän välin lukujen summa on enintään $x$.
Jos summa on tarkalleen $x$, ratkaisu on löytynyt.
Tarkastellaan algoritmin toimintaa
seuraavassa taulukossa, kun tavoitteena on muodostaa
summa $x=12$:
\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) {$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$};
\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}
Seuraavassa on algoritmin aloitustilanne.
Lukujen summa on $1+10=11$, joka on pienempi
kuin $x$:n arvo.
\begin{center}
\begin{tikzpicture}[scale=0.7]
\fill[color=lightgray] (0,0) rectangle (1,1);
\fill[color=lightgray] (7,0) rectangle (8,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,->] (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)$.

1425
luku09.tex Normal file

File diff suppressed because it is too large Load Diff

562
luku10.tex Normal file
View File

@ -0,0 +1,562 @@
\chapter{Bit manipulation}
Tietokone käsittelee tietoa sisäisesti bitteinä
eli numeroina 0 ja 1.
Tässä luvussa tutustumme tarkemmin kokonaisluvun
bittiesitykseen sekä bittioperaatioihin,
jotka muokkaavat luvun bittejä.
Osoittautuu, että näistä operaatioista on
monenlaista hyötyä algoritmien ohjelmoinnissa.
\section{Luvun bittiesitys}
\index{bittiesitys@bittiesitys}
Luvun \key{bittiesitys} ilmaisee, mistä 2:n potensseista
luku muodostuu. Esimerkiksi luvun 43 bittiesitys on 101011, koska
$43 = 2^5 + 2^3 + 2^1 + 2^0$
eli oikealta lukien bitit 0, 1, 3 ja 5 ovat ykkösiä
ja kaikki muut bitit ovat nollia.
Tietokoneessa luvun bittiesityksen
bittien määrä on kiinteä ja riippuu käytetystä tietotyypistä.
Esimerkiksi C++:n \texttt{int}-tyyppi on tavallisesti 32-bittinen,
jolloin \texttt{int}-luku tallennetaan 32 bittinä.
Tällöin esimerkiksi luvun 43 bittiesitys \texttt{int}-lukuna on seuraava:
\[00000000000000000000000000101011\]
Luvun bittiesitys on joko \key{etumerkillinen}
tai \key{etumerkitön}.
Etumerkillisen bittiesityksen ensimmäinen bitti on etumerkki
($+$ tai $-$) ja $n$ bitillä voi esittää luvut $-2^{n-1} \ldots 2^{n-1}-1$.
Jos taas bittiesitys on etumerkitön,
kaikki bitit kuuluvat lukuun ja $n$ bitillä voi esittää luvut $0 \ldots 2^n-1$.
Etumerkillisessä bittiesityksessä ei-negatiivisen luvun
ensimmäinen bitti on 0 ja negatiivisen luvun
ensimmäinen bitti on 1.
Bittiesityksenä on \key{kahden komplementti},
jossa luvun luvun vastaluvun saa laskettua
muuttamalla
kaikki bitit käänteiseksi ja lisäämällä
tulokseen yksi.
Esimerkiksi luvun $-43$ esitys \texttt{int}-lukuna on seuraava:
\[11111111111111111111111111010101\]
Etumerkillisen ja etumerkittömän bittiesityksen
yhteys on, että etumerkillisen luvun $-x$
ja etumerkittömän luvun $2^n-x$ bittiesitykset ovat samat.
Niinpä yllä oleva bittiesitys tarkoittaa
etumerkittömänä lukua $2^{32}-43$.
C++:ssa luvut ovat oletuksena etumerkillisiä,
mutta avainsanan \texttt{unsigned} avulla
luvusta saa etumerkittömän.
Esimerkiksi koodissa
\begin{lstlisting}
int x = -43;
unsigned int y = x;
cout << x << "\n"; // -43
cout << y << "\n"; // 4294967253
\end{lstlisting}
etumerkillistä lukua $x=-43$ vastaa etumerkitön luku $y=2^{32}-43$.
Jos luvun suuruus menee käytössä
olevan bittiesityksen ulkopuolelle,
niin luku pyörähtää ympäri.
Etumerkillisessä bittiesityksessä
luvusta $2^{n-1}-1$ seuraava luku on $-2^{n-1}$
ja vastaavasti etumerkittömässä bittiesityksessä
luvusta $2^n-1$ seuraava luku on $0$.
Esimerkiksi koodissa
\begin{lstlisting}
int x = 2147483647
cout << x << "\n"; // 2147483647
x++;
cout << x << "\n"; // -2147483648
\end{lstlisting}
muuttuja $x$ pyörähtää ympäri luvusta $2^{31}-1$ lukuun $-2^{31}$.
\section{Bittioperaatiot}
\newcommand\XOR{\mathbin{\char`\^}}
\subsubsection{And-operaatio}
\index{and-operaatio}
And-operaatio $x$ \& $y$ tuottaa luvun,
jossa on ykkösbitti niissä kohdissa,
joissa molemmissa luvuissa $x$ ja $y$ on ykkösbitti.
Esimerkiksi $22$ \& $26$ = 18, koska
\begin{center}
\begin{tabular}{rrr}
& 10110 & (22)\\
\& & 11010 & (26) \\
\hline
= & 10010 & (18) \\
\end{tabular}
\end{center}
And-operaation avulla voi tarkastaa luvun parillisuuden,
koska $x$ \& $1$ = 0, jos luku on parillinen,
ja $x$ \& $1$ = 1, jos luku on pariton.
\subsubsection{Or-operaatio}
\index{or-operaatio}
Or-operaatio $x$ | $y$ tuottaa luvun,
jossa on ykkösbitti niissä kohdissa,
joissa ainakin toisessa luvuista $x$ ja $y$ on ykkösbitti.
Esimerkiksi $22$ | $26$ = 30, koska
\begin{center}
\begin{tabular}{rrr}
& 10110 & (22)\\
| & 11010 & (26) \\
\hline
= & 11110 & (30) \\
\end{tabular}
\end{center}
\subsubsection{Xor-operaatio}
\index{xor-operaatio}
Xor-operaatio $x$ $\XOR$ $y$ tuottaa luvun,
jossa on ykkösbitti niissä kohdissa,
joissa tarkalleen toisessa luvuista $x$ ja $y$ on ykkösbitti.
Esimerkiksi $22$ $\XOR$ $26$ = 12, koska
\begin{center}
\begin{tabular}{rrr}
& 10110 & (22)\\
$\XOR$ & 11010 & (26) \\
\hline
= & 01100 & (12) \\
\end{tabular}
\end{center}
\subsubsection{Not-operaatio}
\index{not-operaatio}
Not-operaatio \textasciitilde$x$ tuottaa luvun,
jossa kaikki $x$:n bitit on muutettu käänteisiksi.
Operaatiolle pätee kaava \textasciitilde$x = -x-1$,
esimerkiksi \textasciitilde$29 = -30$.
Not-operaation toiminta bittitasolla riippuu siitä,
montako bittiä luvun bittiesityksessä on,
koska operaatio vaikuttaa kaikkiin luvun bitteihin.
Esimerkiksi 32-bittisenä \texttt{int}-lukuna
tilanne on seuraava:
\begin{center}
\begin{tabular}{rrrr}
$x$ & = & 29 & 00000000000000000000000000011101 \\
\textasciitilde$x$ & = & $-30$ & 11111111111111111111111111100010 \\
\end{tabular}
\end{center}
\subsubsection{Bittisiirrot}
\index{bittisiirto@bittisiirto}
Vasen bittisiirto $x < < k$ tuottaa luvun, jossa luvun $x$ bittejä
on siirretty $k$ askelta vasemmalle eli
luvun loppuun tulee $k$ nollabittiä.
Oikea bittisiirto $x > > 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<<i)) cout << i << " ";
}
cout << "\n";
\end{lstlisting}
Koodin tulostus on seuraava:
\begin{lstlisting}
1 3 4 8
\end{lstlisting}
Kun joukko on tallennettu bittiesityksenä,
niin joukko-operaatiot voi toteuttaa
tehokkaasti bittioperaatioiden avulla:
\begin{itemize}
\item $a$ \& $b$ on joukkojen $a$ ja $b$ leikkaus $a \cap b$
(tämä sisältää alkiot,
jotka ovat kummassakin joukossa)
\item $a$ | $b$ on joukkojen $a$ ja $b$ yhdiste $a \cup b$
(tämä sisältää alkiot,
jotka ovat ainakin toisessa joukossa)
\item $a$ \& (\textasciitilde$b$) on joukkojen $a$ ja $b$ erotus
$a \setminus b$ (tämä sisältää alkiot,
jotka ovat joukossa $a$ mutta eivät joukossa $b$)
\end{itemize}
Seuraava koodi muodostaa
joukkojen $\{1,3,4,8\}$ ja $\{3,6,8,9\}$ yhdisteen:
\begin{lstlisting}
// joukko {1,3,4,8}
int x = (1<<1)+(1<<3)+(1<<4)+(1<<8);
// joukko {3,6,8,9}
int y = (1<<3)+(1<<6)+(1<<8)+(1<<9);
// joukkojen yhdiste
int z = x|y;
// tulostetaan yhdisteen sisältö
for (int i = 0; i < 32; i++) {
if (z&(1<<i)) cout << i << " ";
}
cout << "\n";
\end{lstlisting}
Koodin tulostus on seuraava:
\begin{lstlisting}
1 3 4 6 8 9
\end{lstlisting}
\subsubsection{Osajoukkojen läpikäynti}
Seuraava koodi käy läpi joukon $\{0,1,\ldots,n-1\}$ osajoukot:
\begin{lstlisting}
for (int b = 0; b < (1<<n); b++) {
// osajoukon b käsittely
}
\end{lstlisting}
Seuraava koodi käy läpi
osajoukot, joissa on $k$ alkiota:
\begin{lstlisting}
for (int b = 0; b < (1<<n); b++) {
if (__builtin_popcount(b) == k) {
// osajoukon b käsittely
}
}
\end{lstlisting}
Seuraava koodi käy läpi joukon $x$ osajoukot:
\begin{lstlisting}
int b = 0;
do {
// osajoukon b käsittely
} while (b=(b-x)&x);
\end{lstlisting}
Esimerkiksi jos $x$ esittää joukkoa $\{2,5,7\}$,
niin koodi käy läpi osajoukot
$\emptyset$, $\{2\}$, $\{5\}$, $\{7\}$,
$\{2,5\}$, $\{2,7\}$, $\{5,7\}$ ja $\{2,5,7\}$.
\section{Dynaaminen ohjelmointi}
\subsubsection{Permutaatioista osajoukoiksi}
Dynaamisen ohjelmoinnin avulla on usein mahdollista
muuttaa permutaatioiden läpikäynti osajoukkojen läpikäynniksi.
Tällöin dynaamisen ohjelmoinnin tilana on
joukon osajoukko sekä mahdollisesti muuta tietoa.
Tekniikan hyötynä on,
että $n$-alkioisen joukon permutaatioiden määrä $n!$
on selvästi suurempi kuin osajoukkojen määrä $2^n$.
Esimerkiksi jos $n=20$, niin $n!=2432902008176640000$,
kun taas $2^n=1048576$.
Niinpä tietyillä $n$:n arvoilla permutaatioita ei ehdi
käydä läpi mutta osajoukot ehtii käydä läpi.
Lasketaan esimerkkinä, monessako
joukon $\{0,1,\ldots,n-1\}$
permutaatiossa ei ole
missään kohdassa kahta peräkkäistä lukua.
Esimerkiksi tapauksessa $n=4$ ratkaisuja on kaksi:
\begin{itemize}
\item $(1,3,0,2)$
\item $(2,0,3,1)$
\end{itemize}
Merkitään $f(x,k)$:llä,
monellako tavalla osajoukon
$x$ luvut voi järjestää niin,
että viimeinen luku on $k$ ja missään kohdassa
ei ole kahta peräkkäistä lukua.
Esimerkiksi $f(\{0,1,3\},1)=1$,
koska voidaan muodostaa permutaatio $(0,3,1)$,
ja $f(\{0,1,3\},3)=0$, koska 0 ja 1 eivät
voi olla peräkkäin alussa.
Funktion $f$ avulla ratkaisu tehtävään
on summa
\[ \sum_{i=0}^{n-1} f(\{0,1,\ldots,n-1\},i). \]
\noindent
Dynaamisen ohjelmoinnin tilat voi
tallentaa seuraavasti:
\begin{lstlisting}
long long d[1<<n][n];
\end{lstlisting}
\noindent
Perustapauksena $f(\{k\},k)=1$ kaikilla $k$:n arvoilla:
\begin{lstlisting}
for (int i = 0; i < n; i++) d[1<<i][i] = 1;
\end{lstlisting}
\noindent
Tämän jälkeen muut funktion arvot
saa laskettua seuraavasti:
\begin{lstlisting}
for (int b = 0; b < (1<<n); b++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (abs(i-j) > 1 && (b&(1<<i)) && (b&(1<<j))) {
d[b][i] += d[b^(1<<i)][j];
}
}
}
}
\end{lstlisting}
\noindent
Muuttujassa $b$ on osajoukon bittiesitys,
ja osajoukon luvuista muodostettu
permutaatio on muotoa $(\ldots,j,i)$.
Vaatimukset ovat, että lukujen $i$ ja $j$
etäisyyden tulee olla yli 1
ja lukujen tulee olla osajoukossa $b$.
Lopuksi ratkaisujen määrän saa laskettua näin
muuttujaan $s$:
\begin{lstlisting}
long long s = 0;
for (int i = 0; i < n; i++) {
s += d[(1<<n)-1][i];
}
\end{lstlisting}
\subsubsection{Osajoukkojen summat}
Oletetaan sitten, että jokaista
joukon $\{0,1,\ldots,n-1\}$
osajoukkoa $x$ vastaa arvo $c(x)$ ja
tehtävänä on laskea kullekin
osajoukolle $x$ summa
\[s(x)=\sum_{y \subset x} c(y) \]
eli bittimuodossa ilmaistuna
\[s(x)=\sum_{y \& x = y} c(y). \]
Seuraavassa on esimerkki funktioiden arvoista,
kun $n=3$:
\begin{center}
\begin{tabular}{rrr}
$x$ & $c(x)$ & $s(x)$ \\
\hline
000 & 2 & 2 \\
001 & 0 & 2 \\
010 & 1 & 3 \\
011 & 3 & 6 \\
100 & 0 & 2 \\
101 & 4 & 6 \\
110 & 2 & 5 \\
111 & 0 & 12 \\
\end{tabular}
\end{center}
Esimerkiksi $s(110)=c(000)+c(010)+c(100)+c(110)=5$.
Tehtävä on mahdollista ratkaista ajassa $O(2^n n)$
laskemalla arvoja funktiolle $f(x,k)$:
mikä on lukujen $c(y)$ summa, missä $x$:stä saa $y$:n
muuttamalla millä tahansa tavalla bittien $0,1,\ldots,k$
joukossa ykkösbittejä nollabiteiksi.
Tämän funktion avulla ilmaistuna $s(x)=f(x,n-1)$.
Funktion pohjatapaukset ovat:
\begin{equation*}
f(x,0) = \begin{cases}
c(x) & \textrm{jos $x$:n bitti 0 on 0}\\
c(x)+c(x \XOR 1) & \textrm{jos $x$:n bitti 0 on 1}\\
\end{cases}
\end{equation*}
Suuremmille $k$:n arvoille pätee seuraava rekursio:
\begin{equation*}
f(x,k) = \begin{cases}
f(x,k-1) & \textrm{jos $x$:n bitti $k$ on 0}\\
f(x,k-1)+f(x \XOR (1 < < k),k-1) & \textrm{jos $x$:n bitti $k$ on 1}\\
\end{cases}
\end{equation*}
Niinpä funktion arvot voi laskea seuraavasti
dynaamisella ohjelmoinnilla.
Koodi olettaa, että taulukko \texttt{c} sisältää
funktion $c$ arvot ja muodostaa taulukon \texttt{s},
jossa on funktion $s$ arvot.
\begin{lstlisting}
for (int x = 0; x < (1<<n); x++) {
f[x][0] = c[x];
if (x&1) f[x][0] += c[x^1];
}
for (int k = 1; k < n; k++) {
for (int x = 0; x < (1<<n); x++) {
f[x][k] = f[x][k-1];
if (b&(1<<k)) f[x][k] += f[x^(1<<k)][k-1];
}
if (k == n-1) s[x] = f[x][k];
}
\end{lstlisting}
Itse asiassa saman laskennan voi toteuttaa lyhyemmin
seuraavasti niin, että tulokset lasketaan
suoraan taulukkoon \texttt{s}:
\begin{lstlisting}
for (int x = 0; x < (1<<n); x++) s[x] = c[x];
for (int k = 0; k < n; k++) {
for (int x = 0; x < (1<<n); x++) {
if (x&(1<<k)) s[x] += s[x^(1<<k)];
}
}
\end{lstlisting}

688
luku11.tex Normal file
View File

@ -0,0 +1,688 @@
\chapter{Basics of graphs}
Monen ohjelmointitehtävän voi ratkaista tulkitsemalla
tehtävän verkko-on\-gel\-ma\-na ja käyttämällä
sopivaa verkkoalgoritmia.
Tyypillinen esimerkki verkosta on tieverkosto,
jonka rakenne muistuttaa luonnostaan verkkoa.
Joskus taas verkko kätkeytyy syvemmälle ongelmaan
ja sitä voi olla vaikeaa huomata.
Tässä kirjan osassa tutustumme verkkojen käsittelyyn
liittyviin tekniikoihin ja kisakoodauksessa
keskeisiin verkkoalgoritmeihin.
Aloitamme aiheeseen perehtymisen
käymällä läpi verkkoihin liittyviä käsitteitä
sekä erilaisia tapoja pitää verkkoa muistissa algoritmeissa.
\section{Käsitteitä}
\index{verkko@verkko}
\index{solmu@solmu}
\index{kaari@kaari}
\key{Verkko} muodostuu \key{solmuista}
ja niiden välisistä \key{kaarista}.
Merkitsemme tässä kirjassa
verkon solmujen määrää
muuttujalla $n$ ja kaarten määrää muuttujalla $m$.
Lisäksi numeroimme verkon solmut kokonaisluvuin
$1,2,\ldots,n$.
Esimerkiksi seuraavassa verkossa on 5 solmua ja 7 kaarta:
\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}
\index{polku@polku}
\key{Polku} on solmusta $a$ solmuun $b$
johtava reitti, joka kulkee verkon kaaria pitkin.
Polun \key{pituus} on kaarten määrä polulla.
Esimerkiksi yllä olevassa verkossa
polkuja solmusta 1 solmuun 5 ovat:
\begin{itemize}
\item $1 \rightarrow 2 \rightarrow 5$ (pituus 2)
\item $1 \rightarrow 4 \rightarrow 5$ (pituus 2)
\item $1 \rightarrow 2 \rightarrow 4 \rightarrow 5$ (pituus 3)
\item $1 \rightarrow 3 \rightarrow 4 \rightarrow 5$ (pituus 3)
\item $1 \rightarrow 4 \rightarrow 2 \rightarrow 5$ (pituus 3)
\item $1 \rightarrow 3 \rightarrow 4 \rightarrow 2 \rightarrow 5$ (pituus 4)
\end{itemize}
\subsubsection{Yhtenäisyys}
\index{yhtenxinen verkko@yhtenäinen verkko}
Verkko on \key{yhtenäinen}, jos siinä on polku
mistä tahansa solmusta mihin tahansa solmuun.
Esimerkiksi seuraava verkko on yhtenäinen:
\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$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (2) -- (3);
\path[draw,thick,-] (3) -- (4);
\path[draw,thick,-] (2) -- (4);
\end{tikzpicture}
\end{center}
Seuraava verkko taas ei ole yhtenäinen,
koska solmusta 4 ei pääse muihin verkon solmuihin.
\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$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (2) -- (3);
%\path[draw,thick,-] (3) -- (4);
%\path[draw,thick,-] (2) -- (4);
\end{tikzpicture}
\end{center}
Verkon yhtenäiset osat muodostavat sen
\key{komponentit}. \index{komponentti@komponentti}
Esimerkiksi seuraavassa verkossa on
kolme komponenttia:
$\{1,\,2,\,3\}$,
$\{4,\,5,\,6,\,7\}$ ja
$\{8\}$.
\begin{center}
\begin{tikzpicture}[scale=0.8]
\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] (6) at (6,1) {$6$};
\node[draw, circle] (7) at (9,1) {$7$};
\node[draw, circle] (4) at (6,3) {$4$};
\node[draw, circle] (5) at (9,3) {$5$};
\node[draw, circle] (8) at (11,2) {$8$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (2) -- (3);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (4) -- (5);
\path[draw,thick,-] (5) -- (7);
\path[draw,thick,-] (6) -- (7);
\path[draw,thick,-] (6) -- (4);
\end{tikzpicture}
\end{center}
\index{puu@puu}
\key{Puu} on yhtenäinen verkko,
jossa on $n$ solmua ja $n-1$ kaarta.
Puussa minkä tahansa kahden solmun välillä
on yksikäsitteinen polku.
Esimerkiksi seuraava verkko on puu:
\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,-] (2) -- (5);
\path[draw,thick,-] (2) -- (4);
%\path[draw,thick,-] (4) -- (5);
\end{tikzpicture}
\end{center}
\subsubsection{Kaarten suunnat}
\index{suunnattu verkko@suunnattu verkko}
Verkko on \key{suunnattu},
jos verkon kaaria pystyy
kulkemaan vain niiden merkittyyn suuntaan.
Esimerkiki seuraava verkko on suunnattu:
\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] (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<int> 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<pair<int,int>> 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<pair<int,int>> 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<pair<pair<int,int>,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}

640
luku12.tex Normal file
View File

@ -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<int> 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<int> 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.

818
luku13.tex Normal file
View File

@ -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{BellmanFordin algoritmi}
\index{BellmanFordin algoritmi}
\key{BellmanFordin 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 BellmanFordin
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 BellmanFordin algoritmin toteutus
etsii lyhimmät polut solmusta $x$
kaikkiin muihin verkon solmuihin.
Koodi olettaa, että verkko on tallennettuna
vieruslistoina taulukossa
\begin{lstlisting}
vector<pair<int,int>> 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}
BellmanFordin 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
BellmanFordin 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 BellmanFordin 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 BellmanFordin algoritmin.
\section{Dijkstran algoritmi}
\index{Dijkstran algoritmi@Dijkstran algoritmi}
\key{Dijkstran algoritmi} etsii BellmanFordin
algoritmin tavoin lyhimmät polut
alkusolmusta kaikkiin muihin solmuihin.
Dijkstran algoritmi on tehokkaampi kuin
BellmanFordin algoritmi,
minkä ansiosta se soveltuu suurten
verkkojen käsittelyyn.
Algoritmi vaatii kuitenkin,
ettei verkossa ole negatiivisia kaaria.
Dijkstran algoritmi vastaa
BellmanFordin 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}
BellmanFordin 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<pair<int,int>> 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{FloydWarshallin algoritmi}
\index{FloydWarshallin algoritmi}
\key{FloydWarshallin 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 FloydWarshallin
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}
FloydWarshallin 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 FloydWarshallin
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ä.

555
luku14.tex Normal file
View File

@ -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.

745
luku15.tex Normal file
View File

@ -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.

753
luku16.tex Normal file
View File

@ -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.

548
luku17.tex Normal file
View File

@ -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.

912
luku18.tex Normal file
View File

@ -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$.

664
luku19.tex Normal file
View File

@ -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.

1360
luku20.tex Normal file

File diff suppressed because it is too large Load Diff

718
luku21.tex Normal file
View File

@ -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<int> tekijat(int n) {
vector<int> 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<m<n$, $n$ ja $m$ ovat suhteelliset
alkuluvut ja ainakin toinen luvuista $n$ ja $m$ on parillinen.
Esimerkiksi valitsemalla $m=1$ ja $n=2$ syntyy
pienin mahdollinen Pythagoraan kolmikko
\[(2^2-1^2,2\cdot2\cdot1,2^2+1^2)=(3,4,5).\]
\subsubsection{Wilsonin lause}
\index{Wilsonin lause@Wilsonin lause}
\key{Wilsonin lauseen} mukaan luku $n$ on alkuluku
tarkalleen silloin, kun
\[(n-1)! \bmod n = n-1.\]
Esimerkiksi luku 11 on alkuluku, koska
\[10! \bmod 11 = 10,\]
ja luku 12 ei ole alkuluku, koska
\[11! \bmod 12 = 0 \neq 11.\]
Wilsonin lauseen avulla voi siis tutkia, onko luku alkuluku.
Tämä ei ole kuitenkaan käytännössä hyvä tapa,
koska luvun $(n-1)!$ laskeminen on työlästä,
jos $n$ on suuri luku.

831
luku22.tex Normal file
View File

@ -0,0 +1,831 @@
\chapter{Combinatorics}
\index{kombinatoriikka@kombinatoriikka}
\key{Kombinatoriikka} tarkoittaa yhdistelmien määrän laskemista.
Yleensä tavoitteena on toteuttaa laskenta
tehokkaasti niin, että jokaista yhdistelmää
ei tarvitse muodostaa erikseen.
Tarkastellaan esimerkkinä tehtävää,
jossa laskettavana on,
monellako tavalla luvun $n$ voi esittää positiivisten
kokonaislukujen summana.
Esimerkiksi luvun 4 voi esittää 8 tavalla
seuraavasti:
\begin{multicols}{2}
\begin{itemize}
\item $1+1+1+1$
\item $1+1+2$
\item $1+2+1$
\item $2+1+1$
\item $2+2$
\item $3+1$
\item $1+3$
\item $4$
\end{itemize}
\end{multicols}
Kombinatorisen tehtävän ratkaisun voi usein
laskea rekursiivisen funktion avulla.
Tässä tapauksessa voimme määritellä funktion $f(n)$,
joka laskee luvun $n$ esitystapojen määrän.
Esimerkiksi $f(4)=8$ yllä olevan esimerkin mukaisesti.
Funktion voi laskea rekursiivisesti seuraavasti:
\begin{equation*}
f(n) = \begin{cases}
1 & n = 1\\
f(1)+f(2)+\ldots+f(n-1)+1 & n > 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}$.

858
luku23.tex Normal file
View File

@ -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}.\]

686
luku24.tex Normal file
View File

@ -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<k-1$, etsitään rekursiivisesti
oikeasta osasta, mikä on kohdassa $k-a-1$ oleva alkio.
Haku jatkuu vastaavalla tavalla rekursiivisesti,
kunnes haluttu alkio on löytynyt.
Kun alkiot $x$ valitaan satunnaisesti,
taulukon koko suunnilleen puolittuu
joka vaiheessa, joten kohdassa $k$ olevan
alkion etsiminen vie aikaa
\[n+n/2+n/4+n/8+\cdots=O(n).\]
Algoritmin pahin tapaus on silti $O(n^2)$,
koska on mahdollista,
että $x$ valitaan sattumalta aina niin,
että se on taulukon pienin alkio.
Silloin taulukko pienenee joka vaiheessa
vain yhden alkion verran.
Tämän todennäköisyys on kuitenkin erittäin pieni,
eikä näin tapahdu käytännössä.
\subsubsection{Matriisitulon tarkastaminen}
\index{matriisitulo@matriisitulo}
Seuraava tehtävämme on \emph{tarkastaa},
päteekö matriisitulo $AB=C$, kun $A$, $B$ ja $C$
ovat $n \times n$ -kokoisia matriiseja.
Tehtävän voi ratkaista laskemalla matriisitulon
$AB$ (perusalgoritmilla ajassa $O(n^3)$), mutta voisi toivoa,
että ratkaisun tarkastaminen olisi helpompaa
kuin sen laskeminen alusta alkaen uudestaan.
Osoittautuu, että tehtävän voi ratkaista
Monte Carlo -algoritmilla,
jonka aikavaativuus on vain $O(n^2)$.
Idea on yksinkertainen: valitaan satunnainen
$n \times 1$ -matriisi $X$ ja lasketaan
matriisit $ABX$ ja $CX$.
Jos $ABX=CX$, ilmoitetaan, että $AB=C$,
ja muuten ilmoitetaan, että $AB \neq C$.
Algoritmin aikavaativuus on $O(n^2)$,
koska matriisien $ABX$ ja $CX$ laskeminen
vie aikaa $O(n^2)$.
Matriisin $ABX$ tapauksessa laskennan
voi suorittaa osissa $A(BX)$, jolloin riittää
kertoa kahdesti $n \times n$- ja $n \times 1$-kokoiset
matriisit.
Algoritmin heikkoutena on, että on pieni mahdollisuus,
että algoritmi erehtyy, kun se ilmoittaa, että $AB=C$.
Esimerkiksi
\[
\begin{bmatrix}
2 & 4 \\
1 & 6 \\
\end{bmatrix}
\neq
\begin{bmatrix}
0 & 5 \\
7 & 4 \\
\end{bmatrix},
\]
mutta
\[
\begin{bmatrix}
2 & 4 \\
1 & 6 \\
\end{bmatrix}
\begin{bmatrix}
1 \\
3 \\
\end{bmatrix}
=
\begin{bmatrix}
0 & 5 \\
7 & 4 \\
\end{bmatrix}
\begin{bmatrix}
1 \\
3 \\
\end{bmatrix}.
\]
Käytännössä erehtymisen todennäköisyys on kuitenkin
pieni ja todennäköisyyttä voi pienentää lisää
tekemällä tarkastuksen usealla
satunnaisella matriisilla $X$ ennen vastauksen
$AB=C$ ilmoittamista.
\subsubsection{Verkon värittäminen}
\index{vxritys@väritys}
Annettuna on verkko, jossa on $n$ solmua ja $m$ kaarta.
Tehtävänä on etsiä tapa värittää verkon solmut kahdella värillä
niin, että ainakin $m/2$ kaaressa
päätesolmut ovat eri väriset.
Esimerkiksi verkossa
\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}
yksi kelvollinen väritys on seuraava:
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle, fill=blue!40] (1) at (1,3) {$1$};
\node[draw, circle, fill=red!40] (2) at (4,3) {$2$};
\node[draw, circle, fill=red!40] (3) at (1,1) {$3$};
\node[draw, circle, fill=blue!40] (4) at (4,1) {$4$};
\node[draw, circle, fill=blue!40] (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}
Yllä olevassa verkossa on 7 kaarta ja niistä 5:ssä
päätesolmut ovat eri väriset,
joten väritys on kelvollinen.
Tehtävä on mahdollista ratkaista Las Vegas -algoritmilla
muodostamalla satunnaisia värityksiä niin kauan,
kunnes syntyy kelvollinen väritys.
Satunnaisessa värityksessä jokaisen solmun väri on
valittu toisistaan riippumatta niin,
että kummankin värin todennäköisyys on $1/2$.
Satunnaisessa värityksessä todennäköisyys, että yksittäisen kaaren päätesolmut
ovat eri väriset on $1/2$. Niinpä odotusarvo, monessako kaaressa
päätesolmut ovat eri väriset, on $1/2 \cdot m = m/2$.
Koska satunnainen väritys on odotusarvoisesti kelvollinen,
jokin kelvollinen väritys löytyy käytännössä nopeasti.

786
luku25.tex Normal file
View File

@ -0,0 +1,786 @@
\chapter{Game theory}
Tässä luvussa keskitymme kahden pelaajan peleihin,
joissa molemmat pelaajat tekevät
samanlaisia siirtoja eikä pelissä ole satunnaisuutta.
Tavoitteemme on etsiä strategia, jota käyttäen
pelaaja pystyy voittamaan pelin toisen pelaajan
toimista riippumatta, jos tämä on mahdollista.
Osoittautuu, että kaikki tällaiset pelit ovat
pohjimmiltaan samanlaisia ja niiden analyysi on
mahdollista \key{nim-teorian} avulla.
Perehdymme aluksi yksinkertaisiin tikkupeleihin,
joissa pelaajat poistavat tikkuja kasoista,
ja yleistämme sitten näiden pelien teorian kaikkiin peleihin.
\section{Pelin tilat}
Tarkastellaan peliä, jossa kasassa on $n$ tikkua.
Pelaajat $A$ ja $B$ siirtävät vuorotellen ja
pelaaja $A$ aloittaa.
Jokaisella siirrolla pelaajan tulee poistaa
1, 2 tai 3 tikkua kasasta.
Pelin voittaa se pelaaja, joka poistaa viimeisen tikun.
Esimerkiksi jos $n=10$, peli saattaa edetä seuraavasti:
\begin{enumerate}[noitemsep]
\item Pelaaja $A$ poistaa 2 tikkua (jäljellä 8 tikkua).
\item Pelaaja $B$ poistaa 3 tikkua (jäljellä 5 tikkua).
\item Pelaaja $A$ poistaa 1 tikun (jäljellä 4 tikkua).
\item Pelaaja $B$ poistaa 2 tikkua (jäljellä 2 tikkua).
\item Pelaaja $A$ poistaa 2 tikkua ja voittaa.
\end{enumerate}
Tämä peli muodostuu tiloista $0,1,2,\ldots,n$,
missä tilan numero vastaa sitä, montako tikkua
kasassa on jäljellä.
Tietyssä tilassa olevan pelaajan valittavana on,
montako tikkua hän poistaa kasasta.
\subsubsection{Voittotila ja häviötila}
\index{voittotila@voittotila}
\index{hxviztila@häviötila}
\key{Voittotila} on tila, jossa oleva pelaaja voittaa
pelin varmasti, jos hän pelaa optimaalisesti.
Vastaavasti \key{häviötila} on tila,
jossa oleva pelaaja häviää varmasti, jos vastustaja
pelaa optimaalisesti.
Osoittautuu, että pelin tilat on mahdollista luokitella
niin, että jokainen tila on joko voittotila tai häviötila.
Yllä olevassa pelissä tila 0 on selkeästi häviötila,
koska siinä oleva pelaaja häviää pelin suoraan.
Tilat 1, 2 ja 3 taas ovat voittotiloja,
koska niissä oleva pelaaja voi poistaa
1, 2 tai 3 tikkua ja voittaa pelin.
Vastaavasti tila 4 on häviötila, koska mikä tahansa
siirto johtaa toisen pelaajan voittoon.
Yleisemmin voidaan havaita, että jos tilasta on
jokin häviötilaan johtava siirto, niin tila on voittotila,
ja muussa tapauksessa tila on häviötila.
Tämän ansiosta voidaan luokitella kaikki pelin tilat
alkaen varmoista häviötiloista, joista ei ole siirtoja
mihinkään muihin tiloihin.
Seuraavassa on pelin tilojen $0 \ldots 15$ luokittelu
($V$ tarkoittaa voittotilaa ja $H$ tarkoittaa häviötilaa):
\begin{center}
\begin{tikzpicture}[scale=0.7]
\draw (0,0) grid (16,1);
\node at (0.5,0.5) {$H$};
\node at (1.5,0.5) {$V$};
\node at (2.5,0.5) {$V$};
\node at (3.5,0.5) {$V$};
\node at (4.5,0.5) {$H$};
\node at (5.5,0.5) {$V$};
\node at (6.5,0.5) {$V$};
\node at (7.5,0.5) {$V$};
\node at (8.5,0.5) {$H$};
\node at (9.5,0.5) {$V$};
\node at (10.5,0.5) {$V$};
\node at (11.5,0.5) {$V$};
\node at (12.5,0.5) {$H$};
\node at (13.5,0.5) {$V$};
\node at (14.5,0.5) {$V$};
\node at (15.5,0.5) {$V$};
\footnotesize
\node at (0.5,1.4) {$0$};
\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$};
\node at (10.5,1.4) {$10$};
\node at (11.5,1.4) {$11$};
\node at (12.5,1.4) {$12$};
\node at (13.5,1.4) {$13$};
\node at (14.5,1.4) {$14$};
\node at (15.5,1.4) {$15$};
\end{tikzpicture}
\end{center}
Tämän pelin analyysi on yksinkertainen:
tila $k$ on häviötila, jos $k$ on jaollinen 4:llä,
ja muuten tila $k$ on voittotila.
Optimaalinen tapa pelata peliä on
valita aina sellainen siirto, että vastustajalle
jää 4:llä jaollinen määrä tikkuja,
kunnes lopulta tikut loppuvat ja vastustaja on hävinnyt.
Tämä pelitapa edellyttää luonnollisesti sitä,
että tikkujen määrä omalla siirrolla \emph{ei} ole
4:llä jaollinen. Jos näin kuitenkin on, mitään ei ole
tehtävissä vaan vastustaja voittaa
pelin varmasti, jos hän pelaa optimaalisesti.
\subsubsection{Tilaverkko}
Tarkastellaan sitten toisenlaista tikkupeliä,
jossa tilassa $k$ saa poistaa minkä tahansa
määrän tikkuja $x$, kunhan $k$ on jaollinen $x$:llä
ja $x$ on pienempi kuin $k$.
Esimerkiksi tilassa 8 on sallittua poistaa
1, 2 tai 4 tikkua, mutta tilassa 7
ainoa mahdollinen siirto on poistaa 1 tikku.
Seuraava kuva esittää pelin tilat $1 \ldots 9$ \key{tilaverkkona}, jossa solmut ovat pelin tiloja
ja kaaret kuvaavat mahdollisia siirtoja tilojen välillä:
\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 (3.5,-1) {$3$};
\node[draw, circle] (4) at (1.5,-2) {$4$};
\node[draw, circle] (5) at (3,-2.75) {$5$};
\node[draw, circle] (6) at (2.5,-4.5) {$6$};
\node[draw, circle] (7) at (0.5,-3.25) {$7$};
\node[draw, circle] (8) at (-1,-4) {$8$};
\node[draw, circle] (9) at (1,-5.5) {$9$};
\path[draw,thick,->,>=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{SpragueGrundyn lause}
\index{SpragueGrundyn lause}
\key{SpragueGrundyn 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$.

1078
luku26.tex Normal file

File diff suppressed because it is too large Load Diff

405
luku27.tex Normal file
View File

@ -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)$.

1175
luku28.tex Normal file

File diff suppressed because it is too large Load Diff

738
luku29.tex Normal file
View File

@ -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<C> 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}<x_{i}$, niin pinta-ala on negatiivinen.
Monikulmion pinta-ala saadaan laskemalla yhteen kaikkien
tällaisten puolisuunnikkaiden pinta-alat, mistä tulee:
\[|\sum_{i=1}^{n-1} (x_{i+1}-x_{i}) \frac{y_i+y_{i+1}}{2}| =
\frac{1}{2} |\sum_{i=1}^{n-1} (x_i y_{i+1} - x_{i+1} y_i)|.\]
Huomaa, että pinta-alan kaavassa on itseisarvo,
koska monikulmion kiertosuunnasta (myötä- tai vastapäivään)
riippuen tuloksena oleva pinta-ala on joko
positiivinen tai negatiivinen.
\subsubsection{Pickin lause}
\index{Pickin lause@Pickin lause}
\key{Pickin lause} on vaihtoehtoinen tapa laskea
monikulmion pinta-ala,
kun kaikki monikulmion kärkipisteet
ovat kokonaislukupisteissä.
Pickin lauseen mukaan monikulmion pinta-ala on
\[ a + b/2 -1,\]
missä $a$ on kokonaislukupisteiden määrä monikulmion sisällä
ja $b$ on kokonaislukupisteiden määrä monikulmion reunalla.
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);
\filldraw (2,4.4) circle (2pt);
\filldraw (3,4.4) circle (2pt);
\filldraw (4,4.4) circle (2pt);
\filldraw (5,4.4) circle (2pt);
\filldraw (6,4.4) circle (2pt);
\filldraw (4,3.4) circle (2pt);
\filldraw (5,3.4) circle (2pt);
\filldraw (6,3.4) circle (2pt);
\filldraw (7,3.4) circle (2pt);
\filldraw (4,2.4) circle (2pt);
\filldraw (5,2.4) circle (2pt);
\end{tikzpicture}
\end{center}
pinta-ala on $6+7/2-1=17/2$.
\section{Etäisyysmitat}
\index{etxisyysmitta@etäisyysmitta}
\index{Euklidinen etxisyys@Euklidinen etäisyys}
\index{Manhattan-etäisyys}
\key{Etäisyysmitta} määrittää tavan laskea kahden pisteen etäisyys.
Tavallisimmin geometriassa käytetty etäisyysmitta on
\key{euklidinen etäisyys}, jolloin pisteiden
$(x_1,y_1)$ ja $(x_2,y_2)$ etäisyys on
\[\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}.\]
Vaihtoehtoinen etäisyysmitta on \key{Manhattan-etäisyys},
jota käyttäen pisteiden
$(x_1,y_1)$ ja $(x_2,y_2)$ etäisyys on
\[|x_1-x_2|+|y_1-y_2|.\]
\begin{samepage}
Esimerkiksi kuvaparissa
\begin{center}
\begin{tikzpicture}
\draw[fill] (2,1) circle [radius=0.05];
\draw[fill] (5,2) circle [radius=0.05];
\node at (2,0.5) {$(2,1)$};
\node at (5,1.5) {$(5,2)$};
\draw[dashed] (2,1) -- (5,2);
\draw[fill] (5+2,1) circle [radius=0.05];
\draw[fill] (5+5,2) circle [radius=0.05];
\node at (5+2,0.5) {$(2,1)$};
\node at (5+5,1.5) {$(5,2)$};
\draw[dashed] (5+2,1) -- (5+2,2);
\draw[dashed] (5+2,2) -- (5+5,2);
\node at (3.5,-0.5) {euklidinen etäisyys};
\node at (5+3.5,-0.5) {Manhattan-etäisyys};
\end{tikzpicture}
\end{center}
\end{samepage}
pisteiden euklidinen etäisyys on
\[\sqrt{(5-2)^2+(2-1)^2}=\sqrt{10}\]
ja pisteiden Manhattan-etäisyys on
\[|5-2|+|2-1|=4.\]
Seuraava kuvapari näyttää alueen, joka on pisteestä etäisyyden 1
sisällä käyttäen euklidista ja Manhattan-etäisyyttä:
\begin{center}
\begin{tikzpicture}
\draw[fill=gray!20] (0,0) circle [radius=1];
\draw[fill] (0,0) circle [radius=0.05];
\node at (0,-1.5) {euklidinen etäisyys};
\draw[fill=gray!20] (5+0,1) -- (5-1,0) -- (5+0,-1) -- (5+1,0) -- (5+0,1);
\draw[fill] (5,0) circle [radius=0.05];
\node at (5,-1.5) {Manhattan-etäisyys};
\end{tikzpicture}
\end{center}
\subsubsection{Kaukaisimmat pisteet}
Joidenkin ongelmien ratkaiseminen on helpompaa, jos käytössä on
Manhattan-etäisyys euklidisen etäisyyden sijasta.
Tarkastellaan esimerkkinä tehtävää, jossa annettuna
on $n$ pistettä $(x_1,y_1),(x_2,y_2),\ldots,(x_n,y_n)$
ja tehtävänä on laskea, mikä on suurin mahdollinen etäisyys
kahden pisteen välillä.
Tämän tehtävän ratkaiseminen tehokkaasti on vaikeaa,
jos laskettavana on euklidinen etäisyys.
Sen sijaan suurin Manhattan-etäisyys on helppoa selvittää,
koska se on suurempi etäisyyksistä
\[\max A - \min A \hspace{20px} \textrm{ja} \hspace{20px} \max B - \min B,\]
missä
\[A = \{x_i+y_i : i = 1,2,\ldots,n\}\]
ja
\[B = \{x_i-y_i : i = 1,2,\ldots,n\}.\]
\begin{samepage}
Tämä johtuu siitä, että Manhattan-etäisyys
\[|x_a-x_b|+|y_a-y_b]\]
voidaan ilmaista muodossa
\begin{center}
\begin{tabular}{cl}
& $\max(x_a-x_b+y_a-y_b,\,x_a-x_b-y_a+y_b)$ \\
$=$ & $\max(x_a+y_a-(x_b+y_b),\,x_a-y_a-(x_b-y_b))$
\end{tabular}
\end{center}
olettaen, että $x_a \ge x_b$.
\end{samepage}
\begin{samepage}
\subsubsection{Koordinaatiston kääntäminen}
Kätevä tekniikka Manhattan-etäisyyden yhteydessä on myös
kääntää koordinaatistoa 45 astetta
niin, että pisteestä $(x,y)$ tulee piste $(a(x+y),a(y-x))$,
missä $a=1/\sqrt{2}$.
Kerroin $a$ on valittu niin, että
pisteiden etäisyydet säilyvät samana käännöksen jälkeen.
Tämän seurauksena pisteestä etäisyydellä $d$ oleva alue
on neliö, jonka sivut ovat vaaka- ja pystysuuntaisia,
mikä helpottaa alueen käsittelyä.
\begin{center}
\begin{tikzpicture}
\draw[fill=gray!20] (0,1) -- (-1,0) -- (0,-1) -- (1,0) -- (0,1);
\draw[fill] (0,0) circle [radius=0.05];
\node at (2.5,0) {$\Rightarrow$};
\draw[fill=gray!20] (5-0.71,0.71) -- (5-0.71,-0.71) -- (5+0.71,-0.71) -- (5+0.71,0.71) -- (5-0.71,0.71);
\draw[fill] (5,0) circle [radius=0.05];
\end{tikzpicture}
\end{center}
\end{samepage}

856
luku30.tex Normal file
View File

@ -0,0 +1,856 @@
\chapter{Sweep line}
\index{pyyhkxisyviiva@pyyhkäisyviiva}
\key{Pyyhkäisyviiva} on tason halki kulkeva viiva,
jonka avulla voi ratkaista useita geometrisia tehtäviä.
Ideana on esittää tehtävä joukkona tapahtumia,
jotka vastaavat tason pisteitä.
Kun pyyhkäisyviiva törmää pisteeseen,
tapahtuma käsitellään ja tehtävän ratkaisu edistyy.
Tarkastellaan esimerkkinä tekniikan
käyttämisestä tehtävää,
jossa yrityksessä on töissä $n$ henkilöä
ja jokaisesta henkilöstä tiedetään,
milloin hän tuli töihin ja lähti töistä
tiettynä päivänä.
Tehtävänä on laskea,
mikä on suurin määrä henkilöitä,
jotka olivat samaan aikaan töissä.
Tehtävän voi ratkaista mallintamalla tilanteen
niin, että jokaista henkilöä vastaa kaksi tapahtumaa:
tuloaika töihin ja lähtöaika töistä.
Pyyhkäisyviiva käy läpi tapahtumat aikajärjestyksessä
ja pitää kirjaa, montako henkilöä oli töissä milloinkin.
Esimerkiksi tilannetta
\begin{center}
\begin{tabular}{ccc}
henkilö & tuloaika & lähtöaika \\
\hline
Uolevi & 10 & 15 \\
Maija & 6 & 12 \\
Kaaleppi & 14 & 16 \\
Liisa & 5 & 13 \\
\end{tabular}
\end{center}
vastaavat seuraavat tapahtumat:
\begin{center}
\begin{tikzpicture}[scale=0.6]
\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};
\end{tikzpicture}
\end{center}
Pyyhkäisyviiva käy läpi tapahtumat vasemmalta oikealle
ja pitää yllä laskuria.
Aina kun henkilö tulee töihin, laskurin arvo
kasvaa yhdellä, ja kun henkilö lähtee töistä,
laskurin arvo vähenee yhdellä.
Tehtävän ratkaisu on suurin laskuri arvo
pyyhkäisyviivan kulun aikana.
Pyyhkäisyviiva kulkee seuraavasti tason halki:
\begin{center}
\begin{tikzpicture}[scale=0.6]
\path[draw,thick,->] (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}