1
0
Files
tmi-practicum-2018-2019/verslag/verslag.tex
2019-05-24 10:28:02 +02:00

188 lines
13 KiB
TeX

\documentclass[a4paper, 11pt]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[dutch]{babel}
\usepackage[margin=2.4cm]{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}). Ook bewijzen we dat de algoritmes correct zijn aan de hand van een correctheidsbewijs van één van de algoritmes (Sectie \ref{cor}).
\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 gemiddeld $N/2$ maal uitgevoerd. Dit geeft een complexiteit van $N*(N/2) = N^2/2 = 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. Dit werkt 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 met $2$ cirkels en verdubbelen steeds het aantal. Bij elk aantal voeren we voor elk algoritme $100$ testen uit, meten de tijd die het algoritme erover doet en nemen het gemiddelde. Dit herhalen we tot $32.768$ cirkels voor het simpele algoritme en tot $262.144$ cirkels voor de andere algoritmes, aangezien we dan voldoende informatie hebben om de complexiteit aan te tonen, zonder onnodig langdurige experimenten uit te moeten voeren. 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.
\subsection{Alternatieve benadering}
Het enige probleem met het vergelijken van de voorgenoemde plots is dat we ze moeilijk op eenzelfde grafiek kunnen plaatsen omdat de grootte-orde van tijden ten opzichte van aantallen sterk verschillen. Om geen schaalfactor in te moeten voeren, kiezen we daarom voor een andere manier om het verloop van de tijd te bepalen, namelijk het doubling ratio experiment. Hierbij verdubbelen we steeds de invoerwaarde - in dit geval het aantal cirkels - en berekenen de verhouding waarmee de gemeten tijd elke keer toeneemt. Deze verhouding is de doubling ratio. We doen daarna hetzelfde voor de functie die de tijdscomplexiteit voorstelt, bijvoorbeeld $Nlog_2(N)$. Wanneer we van al deze verhoudingen het gemiddelde nemen, hebben we dan twee getallen om met elkaar te vergelijken, in plaats van grafieken.\\
Wanneer een getal kleiner is, duidt dit op een kleinere stijging bij een verdubbeling van de grootte van de invoer en dus ook een kleinere stijging van de grafiek ten opzichte van de grafiek met het grotere getal. Wanneer we dus een gelijkaardige waarde uitkomen voor de testwaarden en de complexiteitsfunctie, kunnen we besluiten dat de complexiteitsfunctie de complexiteit van de testwaarden voldoende benaderd. De concrete resultaten van onze experimenten zijn te vinden in Sectie \ref{comp}.
\section{Correctheid van het algoritme}\label{cor}
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 algoritmes 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.
\subsection{Correctheid simpel algoritme}
We zoeken alle onderlinge snijpunten tussen cirkels. Om na te gaan of algoritme \ref{algo1} dit op een correcte manier doet, beginnen we bij de eenvoudigste gevallen.
\begin{itemize}
\item In het geval dat er geen cirkels zijn, zal de lijst $S$ met snijpunten leeg zijn, aangezien de for-lus geen enkele keer uitgevoerd zal worden. Dit is het correcte resultaat.
\item In het geval van één cirkel wordt de buitenste for-lus slechts eenmaal uitgevoerd. De binnenste zal niet uitgevoerd worden aangezien er geen andere cirkels zijn. Aan $S$ zullen dus ook geen snijpunten toegevoegd worden, waardoor $S$ leeg zal zijn. Dit is ook het correcte resultaat.
\item In het geval van twee cirkels wordt de buitenste for-lus eerst uitgevoerd voor de eerste cirkel, de binnenste for-lus dan enkel voor de tweede cirkel. Indien er snijpunten zijn, worden deze aan $S$ toegevoegd. De buitenste for-lus wordt nog een keer voor de tweede cirkel uitgevoerd, maar er zijn geen andere cirkels meer. Enkel de snijpunten tussen de eerste en de tweede cirkel worden dus toegevoegd, indien die er zijn. Dit is ook het correcte resultaat.
\item Deze redenering kan doorgetrokken worden voor $N$ cirkels, waarna de correcte snijpunten bekomen worden.
\end{itemize}
\textbf{Bemerking:} Een laatste bemerking die wel nog gemaakt moet worden is dat wanneer drie cirkels in eenzelfde punt snijden, dit punt meermaals aan $S$ toegevoegd zal worden. Dit kan opgelost worden door van $S$ een set te maken (die geen dubbele waarden bijhoudt), of extra te controleren of een waarde zich al in $S$ bevindt.\\
Indien dit zo geïmplementeerd wordt, is de uitvoer van het simpele algoritme correct.
\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 en als gemiddelde $5$ heeft, terwijl de doubling ratio van een kwadratisch verband exact $4$ is. De waarden voor de testen met 256 en 512 cirkels halen het gemiddelde wel sterk naar boven. Het verschil zou kunnen komen door minder goede nauwkeurigheid en hogere waarden bij de experimenten met relatief minder cirkels. De complexiteit is dan ook gebaseerd op het gedrag op lange termijn van een functie. We kunnen dus ook door de testresultaten zien dat het simpele algoritme een complexiteit van $O(N^2)$ heeft aan de hand van de zes laatste rijen.
\begin{figure}[H]
\centering
\includegraphics[scale=.7]{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. De gemiddelde ratio van onze testwaarden is $1.64$ en die van $Nlog_2(N)$ is $2.4$. De gemiddelde doubling ratio van $N^2$ is dan weer $4$. De ratio is zelfs lager dan een $Nlog_2(N)$-functie. Hieruit kunnen we besluiten dat de doubling ratio van de testwaarden veel meer aansluit bij die van $Nlog_2(N)$ dan bij een kwadratisch verband.
\begin{figure}[H]
\centering
\includegraphics[width=.6\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 $Nlog_2(N)$. Het gemiddelde van de ratio's van onze testwaarden is $2.33$ en dat van $Nlog_2(N)$ is $3.06$, wat op een tragere stijging van onze testwaarden dan de $Nlog_2(N)$-functie wijst.
\begin{figure}[H]
\centering
\includegraphics[scale=.6]{doorloopeff_doubling.png}
\caption{Tabel met de doubling ratio van het efficiëntere doorlooplijnalgoritme.}
\label{tab3}
\end{figure}
\end{document}