1
0
Files
tmi-practicum-2018-2019/verslag/verslag.tex
2019-05-23 18:26:35 +02:00

170 lines
10 KiB
TeX

\documentclass[a4paper, 11pt]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[dutch]{babel}
\usepackage[margin=2.56cm]{geometry}
\usepackage[linesnumbered, algoruled, lined]{algorithm2e}
\usepackage{float}
\usepackage{wrapfig}
\usepackage{graphicx}
\graphicspath{ {./} }
\title{Project: Snijdende cirkels}
\author{Arthur Bols \& Ruben Van Laer}
\date{Mei 2019}
\newcommand{\CS}{C\nolinebreak\hspace{-.05em}\raisebox{.4ex}{\tiny\bf +}\nolinebreak\hspace{-.10em}\raisebox{.4ex}{\bf +}}
\def\CS{{C\nolinebreak[4]\hspace{-.05em}\raisebox{.4ex}{\bf \#}}}
\setlength\parindent{0em}
\begin{document}
\maketitle
\section{Inleiding}
In dit practicum bespreken we algoritmes die de onderlinge snijpunten van willekeurige cirkels bepalen. We trachten algoritmes met vooropgestelde complexiteit te ontwerpen en analyseren. Dit doen we door eerst en vooral een hoogniveau beschrijving van de algoritmes op te stellen (Sectie \ref{hoogniveau}) en deze dan te implementeren. Hierna testen we de respectievelijke implementaties op hun rekencomplexiteit (Sectie \ref{exp}) door de rekentijd te meten en te plotten (Sectie \ref{comp}).
\section{Hoogniveau beschrijving van de algoritmen}
\label{hoogniveau}
\subsection{Simpel algoritme $O(N^2)$}
Een eerste algoritme dat we zullen beschouwen is de meest eenvoudige manier om de onderlinge snijpunten tussen cirkels te bepalen. Het is simpelweg het nagaan van de snijpunten van elke cirkel met elke andere cirkel. In het geval van $N$ cirkels, wordt in algoritme \ref{algo1} de buitenste for-lus $N$ maal uitgevoerd. De binnenste for-lus wordt $N-1$ maal uitgevoerd. Dit geeft een complexiteit van $N*(N-1) = N^2 - N = O(N^2)$ voor het berekenen van de snijpunten.\\
\begin{algorithm}[H]
\{$cirkels$: lijst van cirkels met een middelpunt en een straal\}\\
$S \gets \emptyset$ \{$S$: lijst met alle snijpunten\}\\
\For{iedere $c_1$ in $cirkels$}{
Verwijder($cirkels$, $c_1$)\\
\For{iedere $c_2$ in ($cirkels$)}{
$snpt \gets$ Snijpunten($c_1$,$c_2$)\\
\If{$snpt \neq \emptyset$}{
Voegtoe($S$,$snpt$)
}
}
}
\caption{Simpel Algoritme}
\label{algo1}
\end{algorithm}
\subsection{Doorlooplijnalgoritme $O(N^2)$}
Het tweede algoritme dat we beschouwen, is een algoritme gebaseerd op het concept van een verticale doorlooplijn. We slaan het begin- en eindpunt van elke cirkel op en sorteren deze punten op basis van de x-coördinaat. Hierna gaan we van klein naar groot door al deze punten en markeren we cirkels bij het startpunt als actief. Bij het eindpunt worden ze weer inactief. Elke keer we een startpunt tegenkomen, bepalen we alle snijpunten van deze nieuwe cirkel met alle andere actieve cirkels. De complexiteit in het slechtste geval, zijnde wanneer alle cirkels tegelijkertijd actief zijn, zorgt voor het zoeken van snijpunten met alle andere cirkels zoals in algoritme \ref{algo1}, wat via dezelfde redenering een rekencomplexiteit van de grootte-orde $N^2$ oplevert.
\begin{algorithm}[H]
\{$cirkels$: lijst van cirkels met een middelpunt en een straal\}\\
$E \gets \emptyset$ \{$E$: gesorteerde rij met de horizontale begin- en eindpunten van elke cirkel (ook wel de 'Events' van de doorlooplijn)\}\\
\For{iedere $c$ in $cirkels$}{
$p_1 \gets$ Beginpunt($c$)\\
$p_2 \gets$ Eindpunt($c$)\\
Voegtoe($E$, $p_1$)\\
Voegtoe($E$, $p_2$)
}
$S \gets \emptyset$ \{$S$: lijst met alle snijpunten\}\\
$A \gets \emptyset$ \{$A$: lijst met cirkels die snijden met de doorlooplijn\}\\
\While{$E \neq \emptyset$}{
$e \gets$ Kleinste($E$)\\
Verwijder($E$, $e$)\\
\If{$e$ is een beginpunt}{
$c \gets$ Cirkel($e$)\\
\For{iedere $c_i$ in $A$}{
$snpt \gets$ Snijpunten($c$,$c_i$)\\
\If{$snpt \neq \emptyset$}{
Voegtoe($S$,$snpt$)
}
}
Voegtoe($A$, $c$)\\
}
\If{$e$ is een eindpunt}{
$c \gets$ Cirkel($e$)\\
Verwijder($A$, $c$)
}
}
\caption{Doorlooplijnalgoritme $O(N^2)$}
\label{algo2}
\end{algorithm}
\subsection{Doorlooplijnalgoritme $O((N+S)log_2(N))$}
Het derde en laatst beschouwde algoritme is een variant op algoritme \ref{algo2}, maar dan met efficiëntere gegevensstructuren. Het concept hierbij is hetzelfde, maar in plaats van het nagaan van de snijpunten met alle andere actieve cirkels, worden de actieve cirkels gesorteerd in een binaire boom-structuur op basis van de volgorde waarin ze de doorlooplijn snijden. Ervan uitgaande dat maximaal twee cirkels elkaar in hetzelfde punt snijden, werkt dit omdat vlak voor de snijding van twee cirkels, deze opeenvolgende cirkels zijn. De binaire boom-structuur garandeert een complexiteit van $log_2(N)$ voor het zoeken in de boom. Aangezien er $N$ cirkels zijn en er voor deze $N$ telkens de voorganger en opvolger gezocht dient te worden, vinden we een complexiteit van $2Nlog_2(N)$. Dit is $O(Nlog_2(N))$ en dus zeker ook $O((N+S)log_2(N))$.
\begin{algorithm}[H]
\{$cirkels$: lijst van cirkels met een middelpunt en een straal\}\\
$E \gets \emptyset$ \{$E$: gesorteerde rij met de horizontale begin- en eindpunten van elke cirkel (ook wel de 'Events' van de doorlooplijn)\}\\
\For{iedere $c$ in $cirkels$}{
$p_1 \gets$ Beginpunt($c$)\\
$p_2 \gets$ Eindpunt($c$)\\
Voegtoe($E$, $p_1$)\\
Voegtoe($E$, $p_2$)
}
$S \gets \emptyset$ \{$S$: lijst met alle snijpunten\}\\
$A \gets \emptyset$ \{$A$: (gesorteerde) binaire boom met cirkels die snijden met de doorlooplijn\}\\
\While{$E \neq \emptyset$}{
$e \gets$ Kleinste($E$)\\
$c \gets$ Cirkel($e$)\\
Verwijder($E$, $e$)\\
\If{$e$ is een beginpunt}{
Voegtoe($A$, $c$)\\
$c_v \gets$ Voorganger($c$)\\
$c_o \gets$ Opvolger($c$)\\
$snpt \gets$ Snijpunten($c$,$c_v$)\\
\If{$snpt \neq \emptyset$}{
Voegtoe($S$,$snpt$)
}
$snpt \gets$ Snijpunten($c$,$c_o$)\\
\If{$snpt \neq \emptyset$}{
Voegtoe($S$,$snpt$)
}
}
\If{$e$ is een eindpunt}{
Verwijder($A$, $c$)
}
}
\caption{Doorlooplijnalgoritme $O((N+S)log_2(N))$}
\end{algorithm}
\section{Beschrijving experimenten}\label{exp}
Om de complexiteit van de algoritmen na te gaan, voeren we experimenten op een toenemend aantal cirkels uit. We starten van 10 cirkels en verdubbelen steeds het aantal. Bij elk aantal voeren we 100 testen uit, meten we de tijd die het algoritme erover doet en nemen we het gemiddelde. Zo gaan we door tot ongeveer 10.000 cirkels voor het simpele algoritme en tot ongeveer 80.000 cirkels voor de andere algoritmes. Deze gemiddelde tijd kunnen we dan plotten in functie van het aantal cirkels en deze vergelijken met de grafiek van de complexiteit die we verwachten, met eventueel verschillende factoren. Ook kunnen we het beschouwen als een 'doubling ratio'-experiment en de verhouding tussen de opeenvolgende gemiddelde tijden berekenen. Deze 'doubling ratio' geeft ook een idee van de grootte-orde van de complexiteit. Zo geeft een verhouding van ongeveer 4 weer dat bij een verdubbeling de tijd met een factor 4 toeneemt. Dit wijst op een kwadratisch verband tussen het aantal cirkels en de tijd.
De concrete resultaten zijn te vinden in Sectie \ref{comp}.
\section{Correctheid van het algoritme}
Om de correctheid van de drie algoritmes na te gaan, bewijzen we eerst de correctheid van het simpele algoritme. Hierna voeren we een groot aantal testen uit waarbij zowel het simpele algoritme als een van de andere algoritme uitgevoerd worden. Na het uitvoeren vergelijken we de uitvoer met elkaar, waarin we nagaan of de resultaten op kleine afrondingsfouten na, hetzelfde zijn. Indien dit zo is voor het grote aantal testen, gaan we ervan uit dat het andere algoritme correct is.
\section{Bespreking resultaten en rekentijden}\label{comp}
\subsection{Simpel algoritme}
Theoretisch zagen we dat het simpele algoritme een complexiteit $O(N^2)$ heeft. Dit wordt ook bevestigd door het doubling ratio experiment in tabel \ref{tab1}. In kolom 3 zien we dat de doubling ratio rond 4 fluctueert, terwijl de doubling ratio van een kwadratisch verband exact 4 is. We kunnen dus ook door de testresultaten zien dat het simpele algoritme een complexiteit van $O(N^2)$ heeft.
\begin{figure}[H]
\centering
\includegraphics[scale=.8]{simpel_doubling.png}
\caption{Tabel met de doubling ratio van het simpele algoritme.}
\label{tab1}
\end{figure}
\subsection{Doorlooplijnalgoritme $O(N^2)$}
Theoretisch zagen we dat bij het gewone doorlooplijnalgoritme de worst-case tijdscomplexiteit kwadratisch was. De gemiddelde tijdscomplexiteit is echter veel beter dan dat, wat tabel \ref{tab2} dan ook bevestigd. In de derde kolom zien we dat de doubling ratio van de testwaarden veel dichter ligt bij de doubling ratio van $N*log_2(N)$ dan bij de doubling ratio van $N^2$. Het is echter wel hoger dan $N*log_2(N)$, wat wijst op de aanwezigheid van extra factoren in de formule van de rekencomplexiteit, wat duidt op de niet-doorgevoerde optimalisaties die we in het efficientere doorlooplijnalgoritme, besproken in de volgende secties, wel hebben.
\begin{figure}[H]
\centering
\includegraphics[width=\textwidth]{doorloop_doubling.png}
\caption{Tabel met de doubling ratio van het gewone doorlooplijnalgoritme.}
\label{tab2}
\end{figure}
\subsection{Doorlooplijnalgoritme $O((N+S)log_2(N))$}
Om de rekentijd van het derde algoritme na te gaan, maken we opnieuw gebruik van een doubling ratio experiment. We gaan na wat de doubling ratio van de verwachte complexiteit is en wat de doubling ratio van de testwaarden is. In dit geval zien we in de derde kolom van tabel \ref{tab3} de doubling ratio van onze testwaarden en in de vijde kolom de doubling ratio van $N*log_2(N)$. Deze fluctueren beide rond ongeveer $2.2$, wat op een gelijkaardige complexiteit wijst. De iets grotere waarde bij de testwaarden kan wijzen op de extra term van het aantal snijdingen bij de theoretische complexiteit van $(N+S)log_2(N)$, maar aangezien $N*log_2(N)$ ook $O((N+S)log_2(N))$ is wanneer $S \neq 0$, kunnen we besluiten dat het derde algoritme in rekencomplexiteit $O((N+S)log_2(N))$ loopt.
\begin{figure}[H]
\centering
\includegraphics[width=.9\textwidth]{doorloopeff_doubling.png}
\caption{Tabel met de doubling ratio van het efficientere doorlooplijnalgoritme.}
\label{tab3}
\end{figure}
\end{document}