First commit
This commit is contained in:
commit
c210d9497b
|
@ -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}.
|
|
@ -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}
|
|
@ -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$.
|
||||
|
|
@ -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.
|
|
@ -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.
|
||||
|
||||
|
||||
|
|
@ -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 4–5 kertaa
|
||||
nopeampi kuin ratkaisu 1.
|
||||
|
||||
Tehokkain ratkaisu on kuitenkin järjestämistä
|
||||
käyttävä ratkaisu 3, joka on vielä puolet
|
||||
nopeampi kuin ratkaisu 2.
|
||||
Kiinnostavaa on, että sekä ratkaisun 1 että
|
||||
ratkaisun 3 aikavaativuus on $O(n \log n)$,
|
||||
mutta siitä huolimatta
|
||||
ratkaisu 3 vie aikaa vain kymmenesosan.
|
||||
Tämän voi selittää sillä, että
|
||||
järjestäminen on kevyt
|
||||
operaatio ja se täytyy tehdä vain kerran
|
||||
ratkaisussa 3 algoritmin alussa,
|
||||
minkä jälkeen algoritmin loppuosa on lineaarinen.
|
||||
Ratkaisu 1 taas pitää yllä monimutkaista
|
||||
tasapainoista binääripuuta koko algoritmin ajan.
|
|
@ -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.
|
|
@ -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}\]
|
|
@ -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.
|
||||
|
||||
|
|
@ -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)$.
|
||||
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -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}
|
||||
|
|
@ -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}
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,818 @@
|
|||
\chapter{Shortest paths}
|
||||
|
||||
\index{lyhin polku@lyhin polku}
|
||||
|
||||
Lyhimmän polun etsiminen alkusolmusta loppusolmuun
|
||||
on keskeinen verkko-ongelma, joka esiintyy usein
|
||||
käytännön tilanteissa.
|
||||
Esimerkiksi tieverkostossa
|
||||
luonteva ongelma on selvittää,
|
||||
mikä on lyhin reitti kahden kaupungin välillä,
|
||||
kun tiedossa ovat kaupunkien väliset tiet ja niiden pituudet.
|
||||
|
||||
Jos verkon kaarilla ei ole painoja,
|
||||
polun pituus on sama kuin kaarten
|
||||
määrä polulla, jolloin lyhimmän polun
|
||||
voi etsiä leveyshaulla.
|
||||
Tässä luvussa keskitymme kuitenkin
|
||||
tapaukseen, jossa kaarilla on painot.
|
||||
Tällöin lyhimpien polkujen etsimiseen
|
||||
tarvitaan kehittyneempiä algoritmeja.
|
||||
|
||||
\section{Bellman–Fordin algoritmi}
|
||||
|
||||
\index{Bellman–Fordin algoritmi}
|
||||
|
||||
\key{Bellman–Fordin algoritmi} etsii
|
||||
lyhimmän polun alkusolmusta
|
||||
kaikkiin muihin verkon solmuihin.
|
||||
Algoritmi toimii kaikenlaisissa verkoissa,
|
||||
kunhan verkossa ei ole sykliä,
|
||||
jonka kaarten yhteispaino on negatiivinen.
|
||||
Jos verkossa on negatiivinen sykli,
|
||||
algoritmi huomaa tilanteen.
|
||||
|
||||
Algoritmi pitää yllä etäisyysarvioita
|
||||
alkusolmusta kaikkiin muihin verkon solmuihin.
|
||||
Alussa alkusolmun etäisyysarvio on 0
|
||||
ja muiden solmujen etäisyys\-arvio on ääretön.
|
||||
Algoritmi parantaa arvioita
|
||||
etsimällä verkosta kaaria,
|
||||
jotka lyhentävät polkuja,
|
||||
kunnes mitään arviota ei voi enää parantaa.
|
||||
|
||||
\subsubsection{Esimerkki}
|
||||
|
||||
Tarkastellaan Bellman–Fordin
|
||||
algoritmin toimintaa seuraavassa verkossa:
|
||||
\begin{center}
|
||||
\begin{tikzpicture}
|
||||
\node[draw, circle] (1) at (1,3) {1};
|
||||
\node[draw, circle] (2) at (4,3) {2};
|
||||
\node[draw, circle] (3) at (1,1) {3};
|
||||
\node[draw, circle] (4) at (4,1) {4};
|
||||
\node[draw, circle] (5) at (6,2) {5};
|
||||
\node[color=red] at (1,3+0.55) {$0$};
|
||||
\node[color=red] at (4,3+0.55) {$\infty$};
|
||||
\node[color=red] at (1,1-0.55) {$\infty$};
|
||||
\node[color=red] at (4,1-0.55) {$\infty$};
|
||||
\node[color=red] at (6,2-0.55) {$\infty$};
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:2] {} (2);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=left:3] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:$-2$] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=left:3] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (5);
|
||||
\path[draw,thick,-] (4) -- node[font=\small,label=below:2] {} (5);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (4);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
Verkon jokaiseen solmun viereen on merkitty etäisyysarvio.
|
||||
Alussa alkusolmun etäisyysarvio on 0
|
||||
ja muiden solmujen etäisyysarvio on
|
||||
ääretön.
|
||||
|
||||
Algoritmi etsii verkosta kaaria,
|
||||
jotka parantavat etäisyysarvioita.
|
||||
Aluksi kaikki solmusta 0 lähtevät kaaret
|
||||
parantavat arvioita:
|
||||
\begin{center}
|
||||
\begin{tikzpicture}
|
||||
\node[draw, circle] (1) at (1,3) {1};
|
||||
\node[draw, circle] (2) at (4,3) {2};
|
||||
\node[draw, circle] (3) at (1,1) {3};
|
||||
\node[draw, circle] (4) at (4,1) {4};
|
||||
\node[draw, circle] (5) at (6,2) {5};
|
||||
\node[color=red] at (1,3+0.55) {$0$};
|
||||
\node[color=red] at (4,3+0.55) {$2$};
|
||||
\node[color=red] at (1,1-0.55) {$3$};
|
||||
\node[color=red] at (4,1-0.55) {$7$};
|
||||
\node[color=red] at (6,2-0.55) {$\infty$};
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:2] {} (2);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=left:3] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:$-2$] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=left:3] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (5);
|
||||
\path[draw,thick,-] (4) -- node[font=\small,label=below:2] {} (5);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (4);
|
||||
|
||||
\path[draw=red,thick,->,line width=2pt] (1) -- (2);
|
||||
\path[draw=red,thick,->,line width=2pt] (1) -- (3);
|
||||
\path[draw=red,thick,->,line width=2pt] (1) -- (4);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
Sitten kaaret $2 \rightarrow 5$ ja $3 \rightarrow 4$
|
||||
parantavat arvioita:
|
||||
\begin{center}
|
||||
\begin{tikzpicture}
|
||||
\node[draw, circle] (1) at (1,3) {1};
|
||||
\node[draw, circle] (2) at (4,3) {2};
|
||||
\node[draw, circle] (3) at (1,1) {3};
|
||||
\node[draw, circle] (4) at (4,1) {4};
|
||||
\node[draw, circle] (5) at (6,2) {5};
|
||||
\node[color=red] at (1,3+0.55) {$0$};
|
||||
\node[color=red] at (4,3+0.55) {$2$};
|
||||
\node[color=red] at (1,1-0.55) {$3$};
|
||||
\node[color=red] at (4,1-0.55) {$1$};
|
||||
\node[color=red] at (6,2-0.55) {$7$};
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:2] {} (2);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=left:3] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:$-2$] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=left:3] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (5);
|
||||
\path[draw,thick,-] (4) -- node[font=\small,label=below:2] {} (5);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (4);
|
||||
|
||||
\path[draw=red,thick,->,line width=2pt] (2) -- (5);
|
||||
\path[draw=red,thick,->,line width=2pt] (3) -- (4);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
Lopuksi tulee vielä yksi parannus:
|
||||
\begin{center}
|
||||
\begin{tikzpicture}
|
||||
\node[draw, circle] (1) at (1,3) {1};
|
||||
\node[draw, circle] (2) at (4,3) {2};
|
||||
\node[draw, circle] (3) at (1,1) {3};
|
||||
\node[draw, circle] (4) at (4,1) {4};
|
||||
\node[draw, circle] (5) at (6,2) {5};
|
||||
\node[color=red] at (1,3+0.55) {$0$};
|
||||
\node[color=red] at (4,3+0.55) {$2$};
|
||||
\node[color=red] at (1,1-0.55) {$3$};
|
||||
\node[color=red] at (4,1-0.55) {$1$};
|
||||
\node[color=red] at (6,2-0.55) {$3$};
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:2] {} (2);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=left:3] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:$-2$] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=left:3] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (5);
|
||||
\path[draw,thick,-] (4) -- node[font=\small,label=below:2] {} (5);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (4);
|
||||
|
||||
\path[draw=red,thick,->,line width=2pt] (4) -- (5);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
|
||||
Tämän jälkeen mikään kaari
|
||||
ei paranna etäisyysarvioita.
|
||||
Tämä tarkoittaa, että etäisyydet
|
||||
ovat lopulliset, eli joka solmussa
|
||||
on nyt pienin etäisyys alkusolmusta
|
||||
kyseiseen solmuun.
|
||||
|
||||
Esimerkiksi pienin etäisyys 3
|
||||
solmusta 1 solmuun 5 toteutuu käyttämällä
|
||||
seuraavaa polkua:
|
||||
|
||||
\begin{center}
|
||||
\begin{tikzpicture}
|
||||
\node[draw, circle] (1) at (1,3) {1};
|
||||
\node[draw, circle] (2) at (4,3) {2};
|
||||
\node[draw, circle] (3) at (1,1) {3};
|
||||
\node[draw, circle] (4) at (4,1) {4};
|
||||
\node[draw, circle] (5) at (6,2) {5};
|
||||
\node[color=red] at (1,3+0.55) {$0$};
|
||||
\node[color=red] at (4,3+0.55) {$2$};
|
||||
\node[color=red] at (1,1-0.55) {$3$};
|
||||
\node[color=red] at (4,1-0.55) {$1$};
|
||||
\node[color=red] at (6,2-0.55) {$3$};
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:2] {} (2);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=left:3] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:$-2$] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=left:3] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:5] {} (5);
|
||||
\path[draw,thick,-] (4) -- node[font=\small,label=below:2] {} (5);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (4);
|
||||
|
||||
\path[draw=red,thick,->,line width=2pt] (1) -- (3);
|
||||
\path[draw=red,thick,->,line width=2pt] (3) -- (4);
|
||||
\path[draw=red,thick,->,line width=2pt] (4) -- (5);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
|
||||
\subsubsection{Toteutus}
|
||||
|
||||
Seuraava Bellman–Fordin algoritmin toteutus
|
||||
etsii lyhimmät polut solmusta $x$
|
||||
kaikkiin muihin verkon solmuihin.
|
||||
Koodi olettaa, että verkko on tallennettuna
|
||||
vieruslistoina taulukossa
|
||||
\begin{lstlisting}
|
||||
vector<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}
|
||||
|
||||
Bellman–Fordin algoritmin avulla voi myös tarkastaa,
|
||||
onko verkossa sykliä,
|
||||
jonka pituus on negatiivinen.
|
||||
Esimerkiksi verkossa
|
||||
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[scale=0.9]
|
||||
\node[draw, circle] (1) at (0,0) {$1$};
|
||||
\node[draw, circle] (2) at (2,1) {$2$};
|
||||
\node[draw, circle] (3) at (2,-1) {$3$};
|
||||
\node[draw, circle] (4) at (4,0) {$4$};
|
||||
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:$3$] {} (2);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:$1$] {} (4);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=below:$5$] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:$-7$] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=right:$2$] {} (3);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
\noindent
|
||||
on negatiivinen sykli $2 \rightarrow 3 \rightarrow 4 \rightarrow 2$,
|
||||
jonka pituus on $-4$.
|
||||
|
||||
Jos verkossa on negatiivinen sykli,
|
||||
sen kautta kulkevaa polkua voi lyhentää äärettömästi
|
||||
toistamalla negatiivista sykliä uudestaan ja uudestaan,
|
||||
minkä vuoksi lyhimmän polun käsite ei ole mielekäs.
|
||||
|
||||
Negatiivisen syklin voi tunnistaa
|
||||
Bellman–Fordin algoritmilla
|
||||
suorittamalla algoritmia $n$ kierrosta.
|
||||
Jos viimeinen kierros parantaa jotain
|
||||
etäisyysarviota, verkossa on negatiivinen sykli.
|
||||
Huomaa, että algoritmi etsii negatiivista sykliä
|
||||
koko verkon alueelta alkusolmusta välittämättä.
|
||||
|
||||
\subsubsection{SPFA-algoritmi}
|
||||
|
||||
\index{SPFA-algoritmi}
|
||||
|
||||
\key{SPFA-algoritmi} (''Shortest Path Faster Algorithm'')
|
||||
on Bellman–Fordin algoritmin muunnelma,
|
||||
joka on usein alkuperäistä algoritmia tehokkaampi.
|
||||
Se ei tutki joka kierroksella koko verkkoa läpi
|
||||
parantaakseen etäisyysarvioita, vaan valitsee
|
||||
tutkittavat kaaret älykkäämmin.
|
||||
|
||||
Algoritmi pitää yllä jonoa solmuista,
|
||||
joiden kautta saattaa pystyä parantamaan etäisyysarvioita.
|
||||
Algoritmi lisää jonoon aluksi alkusolmun $x$
|
||||
ja valitsee aina seuraavan
|
||||
tutkittavan solmun $a$ jonon alusta.
|
||||
Aina kun kaari $a \rightarrow b$ parantaa
|
||||
etäisyysarviota, algoritmi lisää jonoon solmun $b$.
|
||||
|
||||
Seuraavassa toteutuksessa jonona on \texttt{queue}-rakenne
|
||||
\texttt{q}. Lisäksi taulukko \texttt{z} kertoo,
|
||||
onko solmu valmiina jonossa, jolloin algoritmi ei
|
||||
lisää solmua jonoon uudestaan.
|
||||
|
||||
\begin{lstlisting}
|
||||
for (int i = 1; i <= n; i++) e[i] = 1e9;
|
||||
e[x] = 0;
|
||||
q.push(x);
|
||||
while (!q.empty()) {
|
||||
int a = q.front(); q.pop();
|
||||
z[a] = 0;
|
||||
for (auto b : v[a]) {
|
||||
if (e[a]+b.second < e[b.first]) {
|
||||
e[b.first] = e[a]+b.second;
|
||||
if (!z[b]) {q.push(b); z[b] = 1;}
|
||||
}
|
||||
}
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
SPFA-algoritmin tehokkuus riippuu verkon rakenteesta:
|
||||
algoritmi on keskimäärin hyvin tehokas, mutta
|
||||
sen pahimman tapauksen aikavaativuus on edelleen
|
||||
$O(nm)$ ja on mahdollista
|
||||
laatia syötteitä, jotka saavat algoritmin yhtä hitaaksi
|
||||
kuin tavallisen Bellman–Fordin algoritmin.
|
||||
|
||||
\section{Dijkstran algoritmi}
|
||||
|
||||
\index{Dijkstran algoritmi@Dijkstran algoritmi}
|
||||
|
||||
\key{Dijkstran algoritmi} etsii Bellman–Fordin
|
||||
algoritmin tavoin lyhimmät polut
|
||||
alkusolmusta kaikkiin muihin solmuihin.
|
||||
Dijkstran algoritmi on tehokkaampi kuin
|
||||
Bellman–Fordin algoritmi,
|
||||
minkä ansiosta se soveltuu suurten
|
||||
verkkojen käsittelyyn.
|
||||
Algoritmi vaatii kuitenkin,
|
||||
ettei verkossa ole negatiivisia kaaria.
|
||||
|
||||
Dijkstran algoritmi vastaa
|
||||
Bellman–Fordin algoritmia siinä,
|
||||
että se pitää
|
||||
yllä etäisyysarvioita solmuihin
|
||||
ja parantaa niitä algoritmin aikana.
|
||||
Algoritmin tehokkuus perustuu
|
||||
siihen, että sen riittää käydä läpi
|
||||
verkon kaaret vain kerran
|
||||
hyödyntäen tietoa,
|
||||
ettei verkossa ole negatiivisia kaaria.
|
||||
|
||||
\subsubsection{Esimerkki}
|
||||
|
||||
Tarkastellaan Dijkstran algoritmin toimintaa
|
||||
seuraavassa verkossa, kun alkusolmuna
|
||||
on solmu 1:
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[scale=0.9]
|
||||
\node[draw, circle] (1) at (1,3) {3};
|
||||
\node[draw, circle] (2) at (4,3) {4};
|
||||
\node[draw, circle] (3) at (1,1) {2};
|
||||
\node[draw, circle] (4) at (4,1) {1};
|
||||
\node[draw, circle] (5) at (6,2) {5};
|
||||
|
||||
\node[color=red] at (1,3+0.6) {$\infty$};
|
||||
\node[color=red] at (4,3+0.6) {$\infty$};
|
||||
\node[color=red] at (1,1-0.6) {$\infty$};
|
||||
\node[color=red] at (4,1-0.6) {$0$};
|
||||
\node[color=red] at (6,2-0.6) {$\infty$};
|
||||
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:6] {} (2);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5);
|
||||
\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
Bellman–Fordin algoritmin tavoin
|
||||
alkusolmun etäisyysarvio on 0
|
||||
ja kaikissa muissa solmuissa etäisyysarvio
|
||||
on aluksi ääretön.
|
||||
|
||||
Dijkstran algoritmi
|
||||
ottaa joka askeleella käsittelyyn
|
||||
sellaisen solmun,
|
||||
jota ei ole vielä käsitelty
|
||||
ja jonka etäisyysarvio on
|
||||
mahdollisimman pieni.
|
||||
Alussa tällainen solmu on solmu 1,
|
||||
jonka etäisyysarvio on 0.
|
||||
|
||||
Kun solmu tulee käsittelyyn,
|
||||
algoritmi käy läpi kaikki
|
||||
siitä lähtevät kaaret ja
|
||||
parantaa etäisyysarvioita
|
||||
niiden avulla:
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[scale=0.9]
|
||||
\node[draw, circle] (1) at (1,3) {3};
|
||||
\node[draw, circle] (2) at (4,3) {4};
|
||||
\node[draw, circle] (3) at (1,1) {2};
|
||||
\node[draw, circle, fill=lightgray] (4) at (4,1) {1};
|
||||
\node[draw, circle] (5) at (6,2) {5};
|
||||
|
||||
\node[color=red] at (1,3+0.6) {$\infty$};
|
||||
\node[color=red] at (4,3+0.6) {$9$};
|
||||
\node[color=red] at (1,1-0.6) {$5$};
|
||||
\node[color=red] at (4,1-0.6) {$0$};
|
||||
\node[color=red] at (6,2-0.6) {$1$};
|
||||
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:6] {} (2);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5);
|
||||
\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5);
|
||||
|
||||
\path[draw=red,thick,->,line width=2pt] (4) -- (2);
|
||||
\path[draw=red,thick,->,line width=2pt] (4) -- (3);
|
||||
\path[draw=red,thick,->,line width=2pt] (4) -- (5);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
Solmun 1 käsittely paransi etäisyysarvioita
|
||||
solmuihin 2, 4 ja 5,
|
||||
joiden uudet etäisyydet ovat nyt 5, 9 ja 1.
|
||||
|
||||
Seuraavaksi käsittelyyn tulee solmu 5,
|
||||
jonka etäisyys on 1:
|
||||
\begin{center}
|
||||
\begin{tikzpicture}
|
||||
\node[draw, circle] (1) at (1,3) {3};
|
||||
\node[draw, circle] (2) at (4,3) {4};
|
||||
\node[draw, circle] (3) at (1,1) {2};
|
||||
\node[draw, circle, fill=lightgray] (4) at (4,1) {1};
|
||||
\node[draw, circle, fill=lightgray] (5) at (6,2) {5};
|
||||
|
||||
\node[color=red] at (1,3+0.6) {$\infty$};
|
||||
\node[color=red] at (4,3+0.6) {$3$};
|
||||
\node[color=red] at (1,1-0.6) {$5$};
|
||||
\node[color=red] at (4,1-0.6) {$0$};
|
||||
\node[color=red] at (6,2-0.6) {$1$};
|
||||
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:6] {} (2);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5);
|
||||
\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5);
|
||||
|
||||
\path[draw=red,thick,->,line width=2pt] (5) -- (2);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
Tämän jälkeen vuorossa on solmu 4:
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[scale=0.9]
|
||||
\node[draw, circle] (1) at (1,3) {3};
|
||||
\node[draw, circle, fill=lightgray] (2) at (4,3) {4};
|
||||
\node[draw, circle] (3) at (1,1) {2};
|
||||
\node[draw, circle, fill=lightgray] (4) at (4,1) {1};
|
||||
\node[draw, circle, fill=lightgray] (5) at (6,2) {5};
|
||||
|
||||
\node[color=red] at (1,3+0.6) {$9$};
|
||||
\node[color=red] at (4,3+0.6) {$3$};
|
||||
\node[color=red] at (1,1-0.6) {$5$};
|
||||
\node[color=red] at (4,1-0.6) {$0$};
|
||||
\node[color=red] at (6,2-0.6) {$1$};
|
||||
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:6] {} (2);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5);
|
||||
\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5);
|
||||
|
||||
\path[draw=red,thick,->,line width=2pt] (2) -- (1);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
|
||||
Dijkstran algoritmissa on hienoutena,
|
||||
että aina kun solmu tulee käsittelyyn,
|
||||
sen etäisyysarvio on siitä lähtien lopullinen.
|
||||
Esimerkiksi tässä vaiheessa
|
||||
etäisyydet 0, 1 ja 3 ovat lopulliset
|
||||
etäisyydet solmuihin 1, 5 ja 4.
|
||||
|
||||
Algoritmi käsittelee vastaavasti
|
||||
vielä kaksi viimeistä solmua,
|
||||
minkä jälkeen algoritmin päätteeksi
|
||||
etäisyydet ovat:
|
||||
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[scale=0.9]
|
||||
\node[draw, circle, fill=lightgray] (1) at (1,3) {3};
|
||||
\node[draw, circle, fill=lightgray] (2) at (4,3) {4};
|
||||
\node[draw, circle, fill=lightgray] (3) at (1,1) {2};
|
||||
\node[draw, circle, fill=lightgray] (4) at (4,1) {1};
|
||||
\node[draw, circle, fill=lightgray] (5) at (6,2) {5};
|
||||
|
||||
\node[color=red] at (1,3+0.6) {$7$};
|
||||
\node[color=red] at (4,3+0.6) {$3$};
|
||||
\node[color=red] at (1,1-0.6) {$5$};
|
||||
\node[color=red] at (4,1-0.6) {$0$};
|
||||
\node[color=red] at (6,2-0.6) {$1$};
|
||||
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:6] {} (2);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5);
|
||||
\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
|
||||
\subsubsection{Negatiiviset kaaret}
|
||||
|
||||
Dijkstran algoritmin tehokkuus perustuu siihen,
|
||||
että verkossa ei ole negatiivisia kaaria.
|
||||
Jos verkossa on negatiivinen kaari,
|
||||
algoritmi ei välttämättä toimi oikein.
|
||||
Tarkastellaan esimerkkinä seuraavaa verkkoa:
|
||||
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[scale=0.9]
|
||||
\node[draw, circle] (1) at (0,0) {$1$};
|
||||
\node[draw, circle] (2) at (2,1) {$2$};
|
||||
\node[draw, circle] (3) at (2,-1) {$3$};
|
||||
\node[draw, circle] (4) at (4,0) {$4$};
|
||||
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:2] {} (2);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:3] {} (4);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=below:6] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:$-5$] {} (4);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
\noindent
|
||||
Lyhin polku solmusta 1 solmuun 4 on
|
||||
$1 \rightarrow 3 \rightarrow 4$,
|
||||
ja sen pituus on 1.
|
||||
Dijkstran algoritmi löytää
|
||||
kuitenkin keveimpiä kaaria seuraten
|
||||
polun $1 \rightarrow 2 \rightarrow 4$.
|
||||
Algoritmi ei pysty ottamaan huomioon,
|
||||
että alemmalla polulla kaaren paino $-5$
|
||||
kumoaa aiemman suuren kaaren painon $6$.
|
||||
|
||||
\subsubsection{Toteutus}
|
||||
|
||||
Seuraava Dijkstran algoritmin toteutus laskee
|
||||
pienimmän etäisyyden solmusta $x$ kaikkiin muihin solmuihin.
|
||||
Verkko on tallennettu taulukkoon \texttt{v}
|
||||
vieruslistoina, joissa on pareina kohdesolmu
|
||||
ja kaaren pituus.
|
||||
|
||||
Dijkstran algoritmin tehokas toteutus vaatii,
|
||||
että verkosta pystyy löytämään
|
||||
nopeasti vielä käsittelemättömän solmun,
|
||||
jonka etäisyysarvio on pienin.
|
||||
Sopiva tietorakenne tähän on prioriteettijono,
|
||||
jossa solmut ovat järjestyksessä etäisyys\-arvioiden mukaan.
|
||||
Prioriteettijonon avulla
|
||||
seuraavaksi käsiteltävän solmun saa selville logaritmisessa ajassa.
|
||||
|
||||
Seuraavassa toteutuksessa prioriteettijono sisältää
|
||||
pareja, joiden ensimmäinen kenttä on etäisyysarvio
|
||||
ja toinen kenttä on solmun tunniste:
|
||||
\begin{lstlisting}
|
||||
priority_queue<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{Floyd–Warshallin algoritmi}
|
||||
|
||||
\index{Floyd–Warshallin algoritmi}
|
||||
|
||||
\key{Floyd–Warshallin algoritmi}
|
||||
on toisenlainen lähestymistapa
|
||||
lyhimpien polkujen etsintään.
|
||||
Toisin kuin muut tämän luvun algoritmit,
|
||||
se etsii yhdellä kertaa lyhimmät polut kaikkien
|
||||
verkon solmujen välillä.
|
||||
|
||||
|
||||
Algoritmi ylläpitää kaksiulotteista
|
||||
taulukkoa etäisyyksistä solmujen
|
||||
välillä.
|
||||
Ensin taulukkoon on merkitty
|
||||
etäisyydet käyttäen vain solmujen
|
||||
välisiä kaaria.
|
||||
Tämän jälkeen algoritmi
|
||||
päivittää etäisyyksiä,
|
||||
kun verkon solmut saavat yksi kerrallaan
|
||||
toimia välisolmuina poluilla.
|
||||
|
||||
\subsubsection{Esimerkki}
|
||||
|
||||
Tarkastellaan Floyd–Warshallin
|
||||
algoritmin toimintaa seuraavassa verkossa:
|
||||
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[scale=0.9]
|
||||
\node[draw, circle] (1) at (1,3) {$3$};
|
||||
\node[draw, circle] (2) at (4,3) {$4$};
|
||||
\node[draw, circle] (3) at (1,1) {$2$};
|
||||
\node[draw, circle] (4) at (4,1) {$1$};
|
||||
\node[draw, circle] (5) at (6,2) {$5$};
|
||||
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (2);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5);
|
||||
\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
|
||||
Algoritmi merkitsee aluksi taulukkoon
|
||||
etäisyyden 0 jokaisesta solmusta itseensä
|
||||
sekä etäisyyden $x$, jos solmuparin välillä
|
||||
on kaari, jonka pituus on $x$.
|
||||
Muiden solmuparien etäisyys on aluksi ääretön.
|
||||
|
||||
Tässä verkossa taulukosta tulee:
|
||||
\begin{center}
|
||||
\begin{tabular}{r|rrrrr}
|
||||
& 1 & 2 & 3 & 4 & 5 \\
|
||||
\hline
|
||||
1 & 0 & 5 & $\infty$ & 9 & 1 \\
|
||||
2 & 5 & 0 & 2 & $\infty$ & $\infty$ \\
|
||||
3 & $\infty$ & 2 & 0 & 7 & $\infty$ \\
|
||||
4 & 9 & $\infty$ & 7 & 0 & 2 \\
|
||||
5 & 1 & $\infty$ & $\infty$ & 2 & 0 \\
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\vspace{10pt}
|
||||
Algoritmin toiminta muodostuu peräkkäisistä kierroksista.
|
||||
Jokaisella kierroksella valitaan yksi uusi solmu,
|
||||
joka saa toimia välisolmuna poluilla,
|
||||
ja algoritmi parantaa taulukon
|
||||
etäisyyksiä muodostaen polkuja tämän solmun avulla.
|
||||
|
||||
Ensimmäisellä kierroksella solmu 1 on välisolmu.
|
||||
Tämän ansiosta solmujen 2 ja 4 välille muodostuu
|
||||
polku, jonka pituus on 14,
|
||||
koska solmu 1 yhdistää ne toisiinsa.
|
||||
Vastaavasti solmut 2 ja 5 yhdistyvät polulla,
|
||||
jonka pituus on 6.
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{r|rrrrr}
|
||||
& 1 & 2 & 3 & 4 & 5 \\
|
||||
\hline
|
||||
1 & 0 & 5 & $\infty$ & 9 & 1 \\
|
||||
2 & 5 & 0 & 2 & \textbf{14} & \textbf{6} \\
|
||||
3 & $\infty$ & 2 & 0 & 7 & $\infty$ \\
|
||||
4 & 9 & \textbf{14} & 7 & 0 & 2 \\
|
||||
5 & 1 & \textbf{6} & $\infty$ & 2 & 0 \\
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\vspace{10pt}
|
||||
|
||||
Toisella kierroksella solmu 2 saa toimia välisolmuna.
|
||||
Tämä mahdollistaa uudet polut solmuparien 1 ja 3
|
||||
sekä 3 ja 5 välille:
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{r|rrrrr}
|
||||
& 1 & 2 & 3 & 4 & 5 \\
|
||||
\hline
|
||||
1 & 0 & 5 & \textbf{7} & 9 & 1 \\
|
||||
2 & 5 & 0 & 2 & 14 & 6 \\
|
||||
3 & \textbf{7} & 2 & 0 & 7 & \textbf{8} \\
|
||||
4 & 9 & 14 & 7 & 0 & 2 \\
|
||||
5 & 1 & 6 & \textbf{8} & 2 & 0 \\
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\vspace{10pt}
|
||||
|
||||
Kolmannella kierroksella solmu 3 saa toimia välisolmuna,
|
||||
jolloin syntyy uusi polku solmuparin 2 ja 4 välille:
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{r|rrrrr}
|
||||
& 1 & 2 & 3 & 4 & 5 \\
|
||||
\hline
|
||||
1 & 0 & 5 & 7 & 9 & 1 \\
|
||||
2 & 5 & 0 & 2 & \textbf{9} & 6 \\
|
||||
3 & 7 & 2 & 0 & 7 & 8 \\
|
||||
4 & 9 & \textbf{9} & 7 & 0 & 2 \\
|
||||
5 & 1 & 6 & 8 & 2 & 0 \\
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\vspace{10pt}
|
||||
|
||||
|
||||
Algoritmin toiminta jatkuu samalla tavalla
|
||||
niin, että kukin solmu tulee vuorollaan
|
||||
välisolmuksi.
|
||||
Algoritmin päätteeksi taulukko sisältää
|
||||
lyhimmän etäisyyden minkä tahansa
|
||||
solmuparin välillä:
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{r|rrrrr}
|
||||
& 1 & 2 & 3 & 4 & 5 \\
|
||||
\hline
|
||||
1 & 0 & 5 & 7 & 3 & 1 \\
|
||||
2 & 5 & 0 & 2 & 9 & 6 \\
|
||||
3 & 7 & 2 & 0 & 7 & 8 \\
|
||||
4 & 3 & 9 & 7 & 0 & 2 \\
|
||||
5 & 1 & 6 & 8 & 2 & 0 \\
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
Esimerkiksi taulukosta selviää, että lyhin polku
|
||||
solmusta 2 solmuun 4 on pituudeltaan 8.
|
||||
Tämä vastaa seuraavaa polkua:
|
||||
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[scale=0.9]
|
||||
\node[draw, circle] (1) at (1,3) {$3$};
|
||||
\node[draw, circle] (2) at (4,3) {$4$};
|
||||
\node[draw, circle] (3) at (1,1) {$2$};
|
||||
\node[draw, circle] (4) at (4,1) {$1$};
|
||||
\node[draw, circle] (5) at (6,2) {$5$};
|
||||
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=above:7] {} (2);
|
||||
\path[draw,thick,-] (1) -- node[font=\small,label=left:2] {} (3);
|
||||
\path[draw,thick,-] (3) -- node[font=\small,label=below:5] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=left:9] {} (4);
|
||||
\path[draw,thick,-] (2) -- node[font=\small,label=above:2] {} (5);
|
||||
\path[draw,thick,-] (4) -- node[font=\small,label=below:1] {} (5);
|
||||
|
||||
\path[draw=red,thick,->,line width=2pt] (3) -- (4);
|
||||
\path[draw=red,thick,->,line width=2pt] (4) -- (5);
|
||||
\path[draw=red,thick,->,line width=2pt] (5) -- (2);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
|
||||
\subsubsection{Toteutus}
|
||||
|
||||
Floyd–Warshallin algoritmin etuna on,
|
||||
että se on helppoa toteuttaa.
|
||||
Seuraava toteutus muodostaa etäisyysmatriisin
|
||||
\texttt{d}, jossa $\texttt{d}[a][b]$
|
||||
on pienin etäisyys polulla solmusta $a$ solmuun $b$.
|
||||
Aluksi algoritmi alustaa matriisin \texttt{d}
|
||||
verkon vierusmatriisin \texttt{v} perusteella
|
||||
(arvo $10^9$ kuvastaa ääretöntä):
|
||||
|
||||
\begin{lstlisting}
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= n; j++) {
|
||||
if (i == j) d[i][j] = 0;
|
||||
else if (v[i][j]) d[i][j] = v[i][j];
|
||||
else d[i][j] = 1e9;
|
||||
}
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
Tämän jälkeen lyhimmät polut löytyvät seuraavasti:
|
||||
|
||||
\begin{lstlisting}
|
||||
for (int k = 1; k <= n; k++) {
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= n; j++) {
|
||||
d[i][j] = min(d[i][j], d[i][k]+d[k][j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
Algoritmin aikavaativuus on
|
||||
$O(n^3)$, koska siinä on kolme sisäkkäistä
|
||||
silmukkaa,
|
||||
jotka käyvät läpi verkon solmut.
|
||||
|
||||
Koska Floyd–Warshallin
|
||||
algoritmin toteutus on yksinkertainen,
|
||||
algoritmi voi olla hyvä valinta jopa silloin,
|
||||
kun haettavana on yksittäinen
|
||||
lyhin polku verkossa.
|
||||
Tämä on kuitenkin mahdollista vain silloin,
|
||||
kun verkko on niin pieni,
|
||||
että kuutiollinen aikavaativuus on riittävä.
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
||||
|
||||
|
||||
|
|
@ -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$.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -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.
|
||||
|
||||
|
|
@ -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}$.
|
||||
|
|
@ -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}.\]
|
||||
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
@ -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{Sprague–Grundyn lause}
|
||||
|
||||
\index{Sprague–Grundyn lause}
|
||||
|
||||
\key{Sprague–Grundyn lause} yleistää nim-pelin strategian
|
||||
kaikkiin peleihin, jotka täyttävät
|
||||
seuraavat vaatimukset:
|
||||
|
||||
\begin{itemize}[noitemsep]
|
||||
\item Pelissä on kaksi pelaajaa, jotka tekevät vuorotellen siirtoja.
|
||||
\item Peli muodostuu tiloista ja mahdolliset siirrot tilasta
|
||||
eivät riipu siitä, kumpi pelaaja on vuorossa.
|
||||
\item Peli päättyy, kun toinen pelaaja ei voi tehdä siirtoa.
|
||||
\item Peli päättyy varmasti ennemmin tai myöhemmin.
|
||||
\item Pelaajien saatavilla on kaikki tieto tiloista
|
||||
ja siirroista, eikä pelissä ole satunnaisuutta.
|
||||
\end{itemize}
|
||||
Ideana on laskea kullekin pelin tilalle Grundy-luku,
|
||||
joka vastaa tikkujen määrää nim-pelin kasassa.
|
||||
Kun kaikkien tilojen Grundy-luvut ovat tiedossa,
|
||||
peliä voi pelata aivan kuin se olisi nim-peli.
|
||||
|
||||
\subsubsection{Grundy-luku}
|
||||
|
||||
\index{Grundy-luku}
|
||||
\index{mex-funktio}
|
||||
|
||||
Pelin tilan \key{Grundy-luku} määritellään rekursiivisesti
|
||||
kaavalla
|
||||
\[\textrm{mex}(\{g_1,g_2,\ldots,g_n\}),\]
|
||||
jossa $g_1,g_2,\ldots,g_n$ ovat niiden tilojen
|
||||
Grundy-luvut, joihin tilasta pääsee yhdellä siirrolla,
|
||||
ja funktio mex antaa pienimmän ei-negatiivisen
|
||||
luvun, jota ei esiinny joukossa.
|
||||
Esimerkiksi $\textrm{mex}(\{0,1,3\})=2$.
|
||||
Jos tilasta ei voi tehdä mitään siirtoa,
|
||||
sen Grundy-luku on 0, koska $\textrm{mex}(\emptyset)=0$.
|
||||
|
||||
Esimerkiksi tilaverkossa
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[scale=0.9]
|
||||
\node[draw, circle] (1) at (0,0) {\phantom{0}};
|
||||
\node[draw, circle] (2) at (2,0) {\phantom{0}};
|
||||
\node[draw, circle] (3) at (4,0) {\phantom{0}};
|
||||
\node[draw, circle] (4) at (1,-2) {\phantom{0}};
|
||||
\node[draw, circle] (5) at (3,-2) {\phantom{0}};
|
||||
\node[draw, circle] (6) at (5,-2) {\phantom{0}};
|
||||
|
||||
\path[draw,thick,->,>=latex] (2) -- (1);
|
||||
\path[draw,thick,->,>=latex] (3) -- (2);
|
||||
\path[draw,thick,->,>=latex] (5) -- (4);
|
||||
\path[draw,thick,->,>=latex] (6) -- (5);
|
||||
\path[draw,thick,->,>=latex] (4) -- (1);
|
||||
\path[draw,thick,->,>=latex] (4) -- (2);
|
||||
\path[draw,thick,->,>=latex] (5) -- (2);
|
||||
\path[draw,thick,->,>=latex] (6) -- (2);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
Grundy-luvut ovat seuraavat:
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[scale=0.9]
|
||||
\node[draw, circle] (1) at (0,0) {0};
|
||||
\node[draw, circle] (2) at (2,0) {1};
|
||||
\node[draw, circle] (3) at (4,0) {0};
|
||||
\node[draw, circle] (4) at (1,-2) {2};
|
||||
\node[draw, circle] (5) at (3,-2) {0};
|
||||
\node[draw, circle] (6) at (5,-2) {2};
|
||||
|
||||
\path[draw,thick,->,>=latex] (2) -- (1);
|
||||
\path[draw,thick,->,>=latex] (3) -- (2);
|
||||
\path[draw,thick,->,>=latex] (5) -- (4);
|
||||
\path[draw,thick,->,>=latex] (6) -- (5);
|
||||
\path[draw,thick,->,>=latex] (4) -- (1);
|
||||
\path[draw,thick,->,>=latex] (4) -- (2);
|
||||
\path[draw,thick,->,>=latex] (5) -- (2);
|
||||
\path[draw,thick,->,>=latex] (6) -- (2);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
Jos tila on häviötila, sen Grundy-luku on 0.
|
||||
Jos taas tila on voittotila, sen Grundy-luku
|
||||
on jokin positiivinen luku.
|
||||
|
||||
Grundy-luvun hyötynä on,
|
||||
että se vastaa tikkujen määrää nim-kasassa.
|
||||
Jos Grundy-luku on 0, niin tilasta pääsee vain tiloihin,
|
||||
joiden Grundy-luku ei ole 0.
|
||||
Jos taas Grundy-luku on $x>0$, niin tilasta pääsee tiloihin,
|
||||
joiden Grundy-luvut kattavat välin $0,1,\ldots,x-1$.
|
||||
|
||||
~\\
|
||||
\noindent
|
||||
Tarkastellaan esimerkkinä peliä,
|
||||
jossa pelaajat siirtävät vuorotellen
|
||||
pelihahmoa sokkelossa.
|
||||
Jokainen sokkelon ruutu on lattiaa tai seinää.
|
||||
Kullakin siirrolla hahmon tulee liikkua jokin
|
||||
määrä askeleita vasemmalle tai jokin
|
||||
määrä askeleita ylöspäin.
|
||||
Pelin voittaja on se, joka tekee viimeisen siirron.
|
||||
|
||||
\begin{samepage}
|
||||
Esimerkiksi seuraavassa on pelin mahdollinen aloitustilanne,
|
||||
jossa @ on pelihahmo ja * merkitsee ruutua, johon hahmo voi siirtyä.
|
||||
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[scale=.65]
|
||||
\begin{scope}
|
||||
\fill [color=black] (0, 1) rectangle (1, 2);
|
||||
\fill [color=black] (0, 3) rectangle (1, 4);
|
||||
\fill [color=black] (2, 2) rectangle (3, 3);
|
||||
\fill [color=black] (2, 4) rectangle (3, 5);
|
||||
\fill [color=black] (4, 3) rectangle (5, 4);
|
||||
|
||||
\draw (0, 0) grid (5, 5);
|
||||
|
||||
\node at (4.5,0.5) {@};
|
||||
\node at (3.5,0.5) {*};
|
||||
\node at (2.5,0.5) {*};
|
||||
\node at (1.5,0.5) {*};
|
||||
\node at (0.5,0.5) {*};
|
||||
\node at (4.5,1.5) {*};
|
||||
\node at (4.5,2.5) {*};
|
||||
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
\end{samepage}
|
||||
|
||||
Sokkelopelin tiloja ovat kaikki sokkelon
|
||||
lattiaruudut. Tässä tapauksessa
|
||||
tilojen Grundy-luvut ovat seuraavat:
|
||||
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[scale=.65]
|
||||
\begin{scope}
|
||||
\fill [color=black] (0, 1) rectangle (1, 2);
|
||||
\fill [color=black] (0, 3) rectangle (1, 4);
|
||||
\fill [color=black] (2, 2) rectangle (3, 3);
|
||||
\fill [color=black] (2, 4) rectangle (3, 5);
|
||||
\fill [color=black] (4, 3) rectangle (5, 4);
|
||||
|
||||
\draw (0, 0) grid (5, 5);
|
||||
|
||||
\node at (0.5,4.5) {0};
|
||||
\node at (1.5,4.5) {1};
|
||||
\node at (2.5,4.5) {};
|
||||
\node at (3.5,4.5) {0};
|
||||
\node at (4.5,4.5) {1};
|
||||
|
||||
\node at (0.5,3.5) {};
|
||||
\node at (1.5,3.5) {0};
|
||||
\node at (2.5,3.5) {1};
|
||||
\node at (3.5,3.5) {2};
|
||||
\node at (4.5,3.5) {};
|
||||
|
||||
\node at (0.5,2.5) {0};
|
||||
\node at (1.5,2.5) {2};
|
||||
\node at (2.5,2.5) {};
|
||||
\node at (3.5,2.5) {1};
|
||||
\node at (4.5,2.5) {0};
|
||||
|
||||
\node at (0.5,1.5) {};
|
||||
\node at (1.5,1.5) {3};
|
||||
\node at (2.5,1.5) {0};
|
||||
\node at (3.5,1.5) {4};
|
||||
\node at (4.5,1.5) {1};
|
||||
|
||||
\node at (0.5,0.5) {0};
|
||||
\node at (1.5,0.5) {4};
|
||||
\node at (2.5,0.5) {1};
|
||||
\node at (3.5,0.5) {3};
|
||||
\node at (4.5,0.5) {2};
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
|
||||
Tämän seurauksena sokkelopelin
|
||||
tila käyttäytyy
|
||||
samalla tavalla kuin nim-pelin kasa.
|
||||
Esimerkiksi oikean alakulman ruudun
|
||||
Grundy-luku on 2,
|
||||
joten kyseessä on voittotila.
|
||||
Voittoon johtava siirto on joko liikkua neljä
|
||||
askelta vasemmalle tai kaksi askelta ylöspäin.
|
||||
|
||||
Huomaa, että toisin kuin alkuperäisessä nim-pelissä,
|
||||
tilasta saattaa päästä toiseen tilaan,
|
||||
jonka Grundy-luku on suurempi.
|
||||
Vastustaja voi kuitenkin aina peruuttaa
|
||||
tällaisen siirron niin,
|
||||
että Grundy-luku palautuu samaksi.
|
||||
|
||||
\subsubsection{Alipelit}
|
||||
|
||||
Oletetaan seuraavaksi, että peli muodostuu
|
||||
alipeleistä ja jokaisella vuorolla
|
||||
pelaaja valitsee jonkin alipeleistä ja
|
||||
tekee siirron siinä.
|
||||
Peli päättyy, kun missään alipelissä ei
|
||||
pysty tekemään siirtoa.
|
||||
|
||||
Nyt pelin tilan Grundy-luku on alipelien
|
||||
Grundy-lukujen nim-summa.
|
||||
Peliä pystyy pelaamaan nim-pelin
|
||||
tapaan selvittämällä kaikkien alipelien Grundy-luvut
|
||||
ja laskemalla niiden nim-summa.
|
||||
|
||||
~\\
|
||||
\noindent
|
||||
Tarkastellaan esimerkkinä kolmen sokkelon peliä.
|
||||
Tässä pelissä pelaaja valitsee joka siirrolla
|
||||
yhden sokkeloista ja siirtää siinä olevaa hahmoa.
|
||||
Pelin aloitustilanne voi olla seuraavanlainen:
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{ccc}
|
||||
\begin{tikzpicture}[scale=.55]
|
||||
\begin{scope}
|
||||
\fill [color=black] (0, 1) rectangle (1, 2);
|
||||
\fill [color=black] (0, 3) rectangle (1, 4);
|
||||
\fill [color=black] (2, 2) rectangle (3, 3);
|
||||
\fill [color=black] (2, 4) rectangle (3, 5);
|
||||
\fill [color=black] (4, 3) rectangle (5, 4);
|
||||
|
||||
\draw (0, 0) grid (5, 5);
|
||||
|
||||
\node at (4.5,0.5) {@};
|
||||
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
&
|
||||
\begin{tikzpicture}[scale=.55]
|
||||
\begin{scope}
|
||||
\fill [color=black] (1, 1) rectangle (2, 3);
|
||||
\fill [color=black] (2, 3) rectangle (3, 4);
|
||||
\fill [color=black] (4, 4) rectangle (5, 5);
|
||||
|
||||
\draw (0, 0) grid (5, 5);
|
||||
|
||||
\node at (4.5,0.5) {@};
|
||||
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
&
|
||||
\begin{tikzpicture}[scale=.55]
|
||||
\begin{scope}
|
||||
\fill [color=black] (1, 1) rectangle (4, 4);
|
||||
|
||||
\draw (0, 0) grid (5, 5);
|
||||
|
||||
\node at (4.5,0.5) {@};
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
Sokkeloiden ruutujen Grundy-luvut ovat:
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{ccc}
|
||||
\begin{tikzpicture}[scale=.55]
|
||||
\begin{scope}
|
||||
\fill [color=black] (0, 1) rectangle (1, 2);
|
||||
\fill [color=black] (0, 3) rectangle (1, 4);
|
||||
\fill [color=black] (2, 2) rectangle (3, 3);
|
||||
\fill [color=black] (2, 4) rectangle (3, 5);
|
||||
\fill [color=black] (4, 3) rectangle (5, 4);
|
||||
|
||||
\draw (0, 0) grid (5, 5);
|
||||
|
||||
\node at (0.5,4.5) {0};
|
||||
\node at (1.5,4.5) {1};
|
||||
\node at (2.5,4.5) {};
|
||||
\node at (3.5,4.5) {0};
|
||||
\node at (4.5,4.5) {1};
|
||||
|
||||
\node at (0.5,3.5) {};
|
||||
\node at (1.5,3.5) {0};
|
||||
\node at (2.5,3.5) {1};
|
||||
\node at (3.5,3.5) {2};
|
||||
\node at (4.5,3.5) {};
|
||||
|
||||
\node at (0.5,2.5) {0};
|
||||
\node at (1.5,2.5) {2};
|
||||
\node at (2.5,2.5) {};
|
||||
\node at (3.5,2.5) {1};
|
||||
\node at (4.5,2.5) {0};
|
||||
|
||||
\node at (0.5,1.5) {};
|
||||
\node at (1.5,1.5) {3};
|
||||
\node at (2.5,1.5) {0};
|
||||
\node at (3.5,1.5) {4};
|
||||
\node at (4.5,1.5) {1};
|
||||
|
||||
\node at (0.5,0.5) {0};
|
||||
\node at (1.5,0.5) {4};
|
||||
\node at (2.5,0.5) {1};
|
||||
\node at (3.5,0.5) {3};
|
||||
\node at (4.5,0.5) {2};
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
&
|
||||
\begin{tikzpicture}[scale=.55]
|
||||
\begin{scope}
|
||||
\fill [color=black] (1, 1) rectangle (2, 3);
|
||||
\fill [color=black] (2, 3) rectangle (3, 4);
|
||||
\fill [color=black] (4, 4) rectangle (5, 5);
|
||||
|
||||
\draw (0, 0) grid (5, 5);
|
||||
|
||||
\node at (0.5,4.5) {0};
|
||||
\node at (1.5,4.5) {1};
|
||||
\node at (2.5,4.5) {2};
|
||||
\node at (3.5,4.5) {3};
|
||||
\node at (4.5,4.5) {};
|
||||
|
||||
\node at (0.5,3.5) {1};
|
||||
\node at (1.5,3.5) {0};
|
||||
\node at (2.5,3.5) {};
|
||||
\node at (3.5,3.5) {0};
|
||||
\node at (4.5,3.5) {1};
|
||||
|
||||
\node at (0.5,2.5) {2};
|
||||
\node at (1.5,2.5) {};
|
||||
\node at (2.5,2.5) {0};
|
||||
\node at (3.5,2.5) {1};
|
||||
\node at (4.5,2.5) {2};
|
||||
|
||||
\node at (0.5,1.5) {3};
|
||||
\node at (1.5,1.5) {};
|
||||
\node at (2.5,1.5) {1};
|
||||
\node at (3.5,1.5) {2};
|
||||
\node at (4.5,1.5) {0};
|
||||
|
||||
\node at (0.5,0.5) {4};
|
||||
\node at (1.5,0.5) {0};
|
||||
\node at (2.5,0.5) {2};
|
||||
\node at (3.5,0.5) {5};
|
||||
\node at (4.5,0.5) {3};
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
&
|
||||
\begin{tikzpicture}[scale=.55]
|
||||
\begin{scope}
|
||||
\fill [color=black] (1, 1) rectangle (4, 4);
|
||||
|
||||
\draw (0, 0) grid (5, 5);
|
||||
|
||||
\node at (0.5,4.5) {0};
|
||||
\node at (1.5,4.5) {1};
|
||||
\node at (2.5,4.5) {2};
|
||||
\node at (3.5,4.5) {3};
|
||||
\node at (4.5,4.5) {4};
|
||||
|
||||
\node at (0.5,3.5) {1};
|
||||
\node at (1.5,3.5) {};
|
||||
\node at (2.5,3.5) {};
|
||||
\node at (3.5,3.5) {};
|
||||
\node at (4.5,3.5) {0};
|
||||
|
||||
\node at (0.5,2.5) {2};
|
||||
\node at (1.5,2.5) {};
|
||||
\node at (2.5,2.5) {};
|
||||
\node at (3.5,2.5) {};
|
||||
\node at (4.5,2.5) {1};
|
||||
|
||||
\node at (0.5,1.5) {3};
|
||||
\node at (1.5,1.5) {};
|
||||
\node at (2.5,1.5) {};
|
||||
\node at (3.5,1.5) {};
|
||||
\node at (4.5,1.5) {2};
|
||||
|
||||
\node at (0.5,0.5) {4};
|
||||
\node at (1.5,0.5) {0};
|
||||
\node at (2.5,0.5) {1};
|
||||
\node at (3.5,0.5) {2};
|
||||
\node at (4.5,0.5) {3};
|
||||
\end{scope}
|
||||
\end{tikzpicture}
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
Aloitustilanteessa Grundy-lukujen nim-summa on
|
||||
$2 \oplus 3 \oplus 3 = 2$, joten
|
||||
aloittaja pystyy voittamaan pelin.
|
||||
Voittoon johtava siirto on liikkua vasemmassa sokkelossa
|
||||
2 askelta ylöspäin, jolloin nim-summaksi
|
||||
tulee $0 \oplus 3 \oplus 3 = 0$.
|
||||
|
||||
\subsubsection{Jakautuminen}
|
||||
|
||||
Joskus siirto pelissä jakaa pelin alipeleihin,
|
||||
jotka ovat toisistaan riippumattomia.
|
||||
Tällöin pelin Grundy-luku on
|
||||
|
||||
\[\textrm{mex}(\{g_1, g_2, \ldots, g_n \}),\]
|
||||
missä $n$ on siirtojen määrä ja
|
||||
\[g_k = a_{k,1} \oplus a_{k,2} \oplus \ldots \oplus a_{k,m},\]
|
||||
missä siirron $k$ tuottamien alipelien
|
||||
Grundy-luvut ovat $a_{k,1},a_{k,2},\ldots,a_{k,m}$.
|
||||
|
||||
\index{Grundyn peli@Grundyn peli}
|
||||
|
||||
Esimerkki tällaisesta pelistä on \key{Grundyn peli}.
|
||||
Pelin alkutilanteessa on yksittäinen kasa, jossa on $n$ tikkua.
|
||||
Joka vuorolla pelaaja valitsee jonkin kasan
|
||||
ja jakaa sen kahdeksi epätyhjäksi kasaksi
|
||||
niin, että kasoissa on eri määrä tikkuja.
|
||||
Pelin voittaja on se, joka tekee viimeisen jaon.
|
||||
|
||||
Merkitään $f(n)$ Grundy-lukua kasalle,
|
||||
jossa on $n$ tikkua.
|
||||
Grundy-luku muodostuu käymällä läpi tavat
|
||||
jakaa kasa kahdeksi kasaksi.
|
||||
Esimerkiksi tapauksessa $n=8$ mahdolliset jakotavat
|
||||
ovat $1+7$, $2+6$ ja $3+5$, joten
|
||||
\[f(8)=\textrm{mex}(\{f(1) \oplus f(7), f(2) \oplus f(6), f(3) \oplus f(5)\}).\]
|
||||
|
||||
Tässä pelissä luvun $f(n)$ laskeminen vaatii lukujen
|
||||
$f(1),\ldots,f(n-1)$ laskemista.
|
||||
Pohjatapauksina $f(1)=f(2)=0$, koska 1 ja 2 tikun
|
||||
kasaa ei ole mahdollista jakaa mitenkään.
|
||||
Ensimmäiset Grundy-luvut ovat:
|
||||
\[
|
||||
\begin{array}{lcl}
|
||||
f(1) & = & 0 \\
|
||||
f(2) & = & 0 \\
|
||||
f(3) & = & 1 \\
|
||||
f(4) & = & 0 \\
|
||||
f(5) & = & 2 \\
|
||||
f(6) & = & 1 \\
|
||||
f(7) & = & 0 \\
|
||||
f(8) & = & 2 \\
|
||||
\end{array}
|
||||
\]
|
||||
Tapauksen $n=8$ Grundy-luku on 2, joten peli on mahdollista
|
||||
voittaa.
|
||||
Voittosiirto on muodostaa kasat $1+7$,
|
||||
koska $f(1) \oplus f(7) = 0$.
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -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)$.
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue