247 lines
12 KiB
TeX
247 lines
12 KiB
TeX
\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}
|
||
\immediate\write18{./ergebnis-latex.sh}
|
||
\input{ausgabe.tmp}
|
||
\immediate\write18{rm ausgabe.tmp}
|
||
|
||
\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}
|