\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 2–3 Seiten umfassen, maximal 10. \end{document}