Files
bwinf40-runde2/Aufgabe2-Rechenrätsel/doc.tex
2022-03-18 20:04:24 +01:00

245 lines
12 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\documentclass[a4paper,10pt,ngerman]{scrartcl}
\usepackage{babel}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage[a4paper,margin=2.5cm,footskip=0.5cm]{geometry}
\DeclareUnicodeCharacter{25CB}{$\circ$}
% Die nächsten drei Felder bitte anpassen:
\newcommand{\Aufgabe}{Aufgabe 2: Rechenrätsel} % Aufgabennummer und Aufgabennamen angeben
\newcommand{\TeilnahmeId}{60813} % Teilnahme-ID angeben
\newcommand{\Name}{Marcel Zinkel} % Name des Bearbeiter / der Bearbeiterin dieser Aufgabe angeben
% Kopf- und Fußzeilen
\usepackage{scrlayer-scrpage, lastpage}
\setkomafont{pageheadfoot}{\large\textrm}
\lohead{\Aufgabe}
\rohead{Teilnahme-ID: \TeilnahmeId}
\cfoot*{\thepage{}/\pageref{LastPage}}
% Position des Titels
\usepackage{titling}
\setlength{\droptitle}{-1.0cm}
% Für mathematische Befehle und Symbole
\usepackage{amsmath}
\usepackage{amssymb}
% Für Bilder
\usepackage{graphicx}
% Für Algorithmen
\usepackage{algpseudocode}
% Für Quelltext
\usepackage{minted}
\usepackage{color}
\definecolor{mygreen}{rgb}{0,0.6,0}
\definecolor{mygray}{rgb}{0.5,0.5,0.5}
\definecolor{mymauve}{rgb}{0.58,0,0.82}
% Anführungszeichen
\usepackage{csquotes}
% Diese beiden Pakete müssen zuletzt geladen werden
%\usepackage{hyperref} % Anklickbare Links im Dokument
\usepackage{cleveref}
% Daten für die Titelseite
\title{\textbf{\Huge\Aufgabe}}
\author{\LARGE Teilnahme-ID: \LARGE \TeilnahmeId \\\\
\LARGE Bearbeiter/-in dieser Aufgabe: \\
\LARGE \Name\\\\}
\date{\LARGE\today}
\begin{document}
\maketitle
\tableofcontents
\vspace{0.5cm}
\section{Lösungsidee}
\subsection{Allgemeines}
Der Beweis, ob ein Rechenrätsel eindeutig lösbar ist, ist ein
Entscheidungsproblem mit NP-Schwere. Eine Lösung ist eine interessante
Ziffernfolge zufällig zu wählen und alle möglichen Kombinationen von Operatoren
auszuprobieren. Kommt ein Ergebnis nur einmal vor, wurde ein eindeutiges Rätsel
gefunden, was in der Regel der Fall sein sollte, ansonsten muss es mit anderen
Ziffern wiederholt werden. Theoretisch wäre es möglich, dass immer die Operanden
so zufällig so gewählt werden, dass es kein interessantes und eindeutiges Rätsel
gibt und das Programm somit nie zu einem Ergebnis kommt. Allerdings läuft die
Wahrscheinlichkeit dafür gegen null. Solch eine Brute-Force
Operation dauert allerdings bei zunehmender Anzahl der Operatoren exponentiell länger. Die Anzahl
der Möglichkeiten $|\Omega|$ kann in Abhängigkeit von der Operatorenanzahl $n$
berechnet werden: $|\Omega|=4^{n}$. Dabei gilt es zu beachten, dass dies der
Maximalwert ist, da die Division oft in Vorhinein ausgeschlossen werden kann,
weil diese eine nicht ganze Zahl ergibt. Das Programm sollte Rätsel mit mindestens 15
Operatoren erstellen können, da dies in der Aufgabenstellung als Richtwert
angegeben wird. Für $n=15$ gilt $|\Omega|=4^{15}=1073741824\approx10^{6}$. So
viele Möglichkeiten können noch mit einer guten Laufzeit berechnet werden.
Eindeutigkeit auch mit ganzen Zwischenergebnissen?
\subsection{Die Null als Operand?}
Nach Aufgabenstellung ist jeder Operand nur eine Ziffer. Bei den Beispielen
kommt jede Ziffer vor außer die Null, allerdings wird auch nicht ausdrücklich
gesagt, dass man sie nicht verwenden darf. Die Addition und Subtraktion einer
Null kann in einem Rätsel nicht verwendet werden, da beide Operationen das
gleiche Ergebnis haben und das Rätsel damit uneindeutig wäre. Da durch null
Dividieren nicht definiert ist, bleibt nur noch die Multiplikation übrig. Weil
mit null Multiplizieren immer null ergibt, gilt auch für die Operatoren links
des Zwischenergebnisses, dass sie eine Multiplikation sein müssen. Dazu ein
Beispiel: Da bei $1\circ4\circ0$ das letzte \enquote{$\circ$} durch eine
Multiplikation ersetzt werden muss, also $1\circ4\cdot0=1\circ0$, muss auch das
erste \enquote{$\circ$} durch eine Multiplikation ersetzt werden. Da also die
Lösung für alle Operatoren, die sich links einer Null befinden, bekannt ist,
wären solche Rätsel langweilig, weshalb ich nur die Ziffern von 1 bis 9 für die
Rätsel verwende.
\subsection{interessante Rätsel}
Die Aufgabenstellung gibt nicht genau vor wie ein Rätsel, das \enquote{interessant und
unterschiedlich} ist, zu sein hat. Allerdings wird ein Beispiel gegeben, wie ein
Rätsel aussehen kann. Das Beispielrätsel habe ich mal lösen lassen:
\begin{minted}{text}
Rätsel: 4 ○ 3 ○ 2 ○ 6 ○ 3 ○ 9 ○ 7 ○ 8 ○ 2 ○ 9 ○ 4 ○ 4 ○ 6 ○ 4 ○ 4 ○ 5 = 4792
Lösung: 4 * 3 * 2 * 6 * 3 * 9 + 7 * 8 : 2 * 9 * 4 - 4 * 6 - 4 * 4 * 5 = 4792
\end{minted}
Es fällt auf das jeder Operator mindestens einmal vorkommt, wobei Multiplikation
überwiegt. Außerdem ist das Ergebnis noch relativ klein im Vergleich zu anderen
eindeutig lösbaren Rätseln. Zudem kommen einige verschiedene Ziffern vor.
Meine Regeln für ein interessantes Rätsel sind noch etwas strenger, da hier
schon recht oft Multiplikation vertreten ist. Diese lauten wie folgt:
\begin{enumerate}
\item Bei $n$ Operatoren muss für $m_i$, die Anzahl, mit der jeder der
vier Operatoren vorkommt, gelten:
\begin{align}
\frac{n}{10} - 1 < m_i \leq \frac{n}{2} + 1
\end{align}
\item Bei $n$ Ziffern muss für $m_i$, die Anzahl, mit der jede der neun
Ziffern vorkommt, gelten:
\begin{align}
\frac{n}{16} - 1 < m_i \leq \frac{n}{4} + 1
\end{align}
\end{enumerate}
In der Regel werden viele interessante Rätsel gefunden. Da das Beispielrätsel
ein kleines Ergebnis hat und kleinere Ergebnisse auch eleganter sind, wird
von allen interessanten Rätsel, das mit dem kleinsten Ergebnis ausgewählt.
\section{Umsetzung}
\subsection{Umgebung und Bibliotheken}
Die Lösungsidee wird in Rust nightly implementiert, da Trait Aliase noch nicht
in Rust stable sind. Jeder Typ, der ein Trait implementiert, muss bestimmte
Methode haben. Traits sind daher für Generics essenziell. Trait Aliase machen es
nun möglich für mehrere Traits einen Alias zu erstellen, was Schreibarbeit
spart. Außerdem werden ein paar crates, also Abhängigkeiten in Rust, verwendet:
\begin{itemize}
\item Mit rand werden zufällige Ziffern als Operanden erstellt.
\item Mit clap werden die Kommandozeilenargumente verarbeitet.
\item Außerdem werden noch ein paar crates des rust-num Teams genutzt.
Dabei wird nicht das meta-crate num verwendet, sondern die
sub-crates werden einzeln verwendet, um die Abhängigkeiten
kleinzuhalten. num-traits ist dabei u. a. für die Konvertierung
eines bestimmten Integer-Typs zu einen generischen Integer-Typ
zuständig. num-derive stellt Macros zur Verfügung, die es
erlauben Integers zu Enums zu konvertieren. Mit num-integer
können arithmetischen Operationen generisch für verschiedene
Integer-Typen implementiert werden.
\end{itemize}
\subsection{Benutzung}
Mit der Option -c kann die Anzahl der Operatoren angegeben werden. Der
Standartwert ist 5. Allerdings können maximal Rätsel mit 32 Operatoren berechnet
werden, da die verwendeten Operatoren eines Rätsel in 64 Bit Integers
gespeichert werden. Da es vier Grundrechenarten gibt brauchen wir für jeden
Operator 2 Bits, um diesen darzustellen. Da $64/2=32$ können also maximal 32
Operatoren verwendet werden. Mit dem flag -s kann man die Ausgabe der Lösung zu dem
Rätsel einschalten. Schließlich können optional mit -d die Operanden angegeben werden, z.
B. : \mintinline{text}|-d={1,4,5,8}|. Dies ist nur zu Testzecken, wenn man die
Operanden nicht angibt, werden automatisch interessante gewählt.
\subsection{Implementierungsart}
In der main-Funktion befindet sich direkt eine while-Schleife, die solange läuft
bis ein interessantes Rätsel gefunden wurde. In der Regel wird die Schleife nur einmal
durchlaufen, weil für die erste ausgewählte Operandenkombination normalerweise
auch ein interessantes und eindeutiges Rätsel gibt. Wenn der Nutzer keine
Operanden angibt, werden diese zufällig ausgewählt. Dabei befinden sich die
Ziffern in einem Vector (Array mit dynamischer Größe) und werden daraus
zufällig gewählt. Damit die minimale und maximale Anzahl aller Ziffern
garantiert werden kann, sodass das Rätsel interessant ist, werden in Laufe der
Auswahl Ziffern aus dem Vector entfernt.
Danach wird die Funktion \mintinline{rs}|calc_results| mit den Operanden
aufgerufen. Diese kann durch Generics alle Integertypen für die
(Zwischen)ergebnisse verwenden. Das größte Ergebnis für die Operanden wird
berechnet, indem sie alle miteinander multipliziert werden. Wenn dieses zu groß
ist für 64 Bit Integers, werden 128 Bit Integers verwendet, ansonsten 64 Bit
Integers. Dafür habe ich mich entschieden, da mit 64 Bit Rechnern schneller mit
64 Integers gerechnet werden kann. Jedoch kann man trotzdem Rätsel berechnen,
die 128 Bit Integers erfordern. Man hätte auch noch kleinere Rätsel mit 32 Bit
Integers berechnen können, um das Programm für 32 Bit Rechner zu optimieren.
Allerdings werden auch für die Operatoren 64 Bit Integers verwendet, was man
dann auch noch generisch implementierten hätte müssen. Außerdem sind 32 Bit PCs
schon mindestens 15 Jahre veraltet, weshalb mittlerweile eigentlich jeder einen
64 Bit PC haben sollte.
Innerhalb der Funktion ist eine for-Schleife, die über alle Möglichkeiten
Operanden mit Punkt- bzw. Strichrechnung zu wählen iteriert. Dabei wird die
Variable \mintinline{rs}|dm_as_map| nach jedem Durchlauf um eins erhöht. Jedes
Bit der Variable steht dabei für Punkt- oder Strichrechnung, wobei das least
significant Bit für den Operatoren ganz links steht.
In der Schleife wird die Funktion \mintinline{rs}|multiplicate_divide| mit einer Sequenz von
Operanden, zwischen denen nur Punktrechnung vorkommt, aufgerufen. Diese ruft
sich selber doppelt rekursiv auf, wobei einmal multipliziert und einmal
dividiert wird. Die Division wird ausgelassen, wenn das Zwischenergebnis keine
ganze Zahl ist. Wenn das Ende der Sequenz erreicht ist, werden die verwendeten
Operatoren in einer Hashmap mit dem Zwischenergebnis als Schüssel abgespeichert.
Wenn das Zwischenergebnis bereits einmal vorkam, wird unter dem Ergebnis
\mintinline{rs}|None| abspeichert und das Zwischenergebnis somit als uneindeutig
markiert. Die Hashmaps mit den möglichen Zwischenergebnissen werden zusammen in
einem Vector \mintinline{rs}|results_multiplicate| gespeichert. Zusätzlich
werden die Operanden, die nicht Teil einer Punktrechnungssequenz sind, einfach
in den Vector übernommen, indem eine Hashmap mit nur einem möglichen
Zwischenergebnis erstellt wird.
In der rekursiven Funktion \mintinline{rs}|add_sub| wird mit einer for-Schleife
über alle möglichen Zwischenergebnissen iteriert. Innerhalb der Schleife ruft
sich die Funktion pro Durchlauf zweimal auf, wobei einmal addiert und einmal
subtrahiert wird. Dies passiert solange bis die letzte Hashmap mit möglichen
Ergebnissen erreicht wurde. Dann wird das Ergebnis mit den verwendeten
Operatoren mithilfe der Struktur \mintinline{rs}|ResultStore| gespeichert, wobei
es auch sein kann, das das Ergebnis direkt als uneindeutig markiert wird, da
schon eines der Zwischenergebnisse uneindeutig war.
Die Struktur \mintinline{rs}|ResultStore| besteht aus einer Hashmap mit den
Operatoren als Wert und den dazugehörigen Ergebnissen als Schüssel. Außerdem hat
sie noch ein weiters Attribut, das ein Hashset mit den Ergebnissen für das
Rätsel uneindeutig wäre. Die Methode \mintinline{rs}|store| hat als Parameter
das Ergebnis und eine Option von Operatoren. Wenn die Option
\mintinline{rs}|None| ist, wird das Ergebnis in das Hashset der uneindeutigen
Ergebnisse aufgenommen. Wenn das Ergebnis weder in der Hashmap noch in dem
Hashset ist, wird das Rätsel in der Hashmap gespeichert. Wenn es bereits in der
Hashmap ist, wird es als uneindeutig im Hashset gespeichert.
Schließlich wird über alle Schüssel Werte Paare der Hashmap einer Instanz der
\mintinline{rs}|ResultStore| Struktur iteriert. Es wird gezählt wie oft welche
Operatoren verwendet wurden und überprüft, ob die Anzahl innerhalb des Bereiches
liegt, indem das Rätsel als interessant befunden wird. Von allen interessanten
und uneindeutigen Rätseln wird bestimmt welches das kleinste Ergebnis hat und
diese wird ausgegeben. Wenn kein interessantes Rätsel gefunden wurde, muss die
while-Schleife der main-Funktion noch einmal durchlaufen werden.
\section{Beispiele}
\input{|./ergebnis-latex.sh}
\section{Quellcode}
Unwichtige Teile des Programms sollen hier nicht abgedruckt werden. Dieser Teil sollte nicht mehr als 23 Seiten umfassen, maximal 10.
\end{document}