This commit is contained in:
2022-02-15 21:24:05 +01:00
parent 745d04169c
commit 95480f78e4
27 changed files with 432 additions and 99 deletions

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ beispieldaten/
*.log
*.pdf
*.toc
_minted-doc/

View File

@ -1,8 +1,9 @@
\documentclass[a4paper,10pt,ngerman]{scrartcl}
\usepackage{babel}
\usepackage[T1]{fontenc}
\usepackage[utf8x]{inputenc}
\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
@ -32,28 +33,11 @@
\usepackage{algpseudocode}
% Für Quelltext
\usepackage{listings}
\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}
\lstset{
keywordstyle=\color{blue},commentstyle=\color{mygreen},
stringstyle=\color{mymauve},rulecolor=\color{black},
basicstyle=\footnotesize\ttfamily,numberstyle=\tiny\color{mygray},
captionpos=b, % sets the caption-position to bottom
keepspaces=true, % keeps spaces in text
numbers=left, numbersep=5pt, showspaces=false,showstringspaces=true,
showtabs=false, stepnumber=2, tabsize=2, title=\lstname
}
\lstdefinelanguage{JavaScript}{ % JavaScript ist als einzige Sprache noch nicht vordefiniert
keywords={break, case, catch, continue, debugger, default, delete, do, else, finally, for, function, if, in, instanceof, new, return, switch, this, throw, try, typeof, var, void, while, with},
morecomment=[l]{//},
morecomment=[s]{/*}{*/},
morestring=[b]',
morestring=[b]",
sensitive=true
}
% Anführungszeichen
\usepackage{csquotes}
@ -77,14 +61,62 @@
\vspace{0.5cm}
\section{Lösungsidee}
Die Aufgabenstellung gibt nicht vor wie ein Rätsel, das \enquote{interessant und
unterschiedlich} ist, zu sein hat. Damit das Rätsel interessant ist habe ich mir
folgende Regeln überlegt:
\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}{3} + 1
\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:
@ -92,24 +124,119 @@ folgende Regeln überlegt:
\frac{n}{16} - 1 < m_i \leq \frac{n}{4} + 1
\end{align}
\end{enumerate}
Der Beweis, ob ein Rechenrätsel eindeutig lösbar ist, ist ein
Entscheidungsproblem mit NP-Schwere. Eine Lösung ist die Ziffern 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. Solch eine Brute-Force Operation dauert allerdings bei
viele Operatoren exponentiell länger. Die Anzahl der Möglichkeiten $|\Omega|$
kann in Abhängigkeit von der Operatorenanzahl $n$ berechnet werden:
$|\Omega|=4^{n}$. 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.
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}
Hier wird kurz erläutert, wie die Lösungsidee im Programm tatsächlich umgesetzt wurde. Hier können auch Implementierungsdetails erwähnt werden.
\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}
Genügend Beispiele einbinden! Die Beispiele von der BwInf-Webseite sollten hier diskutiert werden, aber auch eigene Beispiele sind sehr gut besonders wenn sie Spezialfälle abdecken. Aber bitte nicht 30 Seiten Programmausgabe hier einfügen!
\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 23 Seiten umfassen, maximal 10.

View File

@ -0,0 +1,5 @@
#/bin/sh
for file in $(ls -v ergebnisdateien); do
operators=$(basename "$file" .txt)
printf "%s Operatoren:\\inputminted[breaklines]{text}{%s}\n" "$operators" "ergebnisdateien/$file" >> ausgabe.tmp
done

View File

@ -0,0 +1,2 @@
Rätsel: 1 ○ 3 = 3
Lösung: 1 * 3 = 3

View File

@ -0,0 +1,2 @@
Rätsel: 2 ○ 2 ○ 6 ○ 2 ○ 3 ○ 3 ○ 5 ○ 1 ○ 6 ○ 7 ○ 5 = 296
Lösung: 2 : 2 - 6 + 2 * 3 * 3 * 5 + 1 + 6 * 7 * 5 = 296

View File

@ -0,0 +1,2 @@
Rätsel: 2 ○ 7 ○ 3 ○ 5 ○ 5 ○ 1 ○ 8 ○ 3 ○ 2 ○ 4 ○ 5 ○ 8 = 698
Lösung: 2 + 7 * 3 * 5 * 5 - 1 + 8 * 3 : 2 + 4 * 5 * 8 = 698

View File

@ -0,0 +1,2 @@
Rätsel: 9 ○ 3 ○ 7 ○ 3 ○ 3 ○ 6 ○ 2 ○ 9 ○ 9 ○ 9 ○ 6 ○ 6 ○ 1 = 1888
Lösung: 9 * 3 * 7 * 3 * 3 + 6 * 2 * 9 + 9 * 9 - 6 : 6 - 1 = 1888

View File

@ -0,0 +1,2 @@
Rätsel: 9 ○ 4 ○ 8 ○ 4 ○ 3 ○ 8 ○ 6 ○ 6 ○ 9 ○ 5 ○ 4 ○ 6 ○ 9 ○ 6 = 3976
Lösung: 9 - 4 + 8 * 4 * 3 : 8 * 6 * 6 * 9 + 5 + 4 * 6 + 9 * 6 = 3976

View File

@ -0,0 +1,2 @@
Rätsel: 7 ○ 8 ○ 1 ○ 8 ○ 5 ○ 6 ○ 7 ○ 8 ○ 2 ○ 4 ○ 5 ○ 1 ○ 7 ○ 4 ○ 8 = 3093
Lösung: 7 - 8 - 1 - 8 * 5 + 6 * 7 * 8 : 2 * 4 * 5 - 1 - 7 * 4 * 8 = 3093

View File

@ -0,0 +1,2 @@
Rätsel: 4 ○ 5 ○ 5 ○ 5 ○ 7 ○ 2 ○ 8 ○ 6 ○ 8 ○ 6 ○ 8 ○ 6 ○ 3 ○ 5 ○ 9 ○ 1 = 8227
Lösung: 4 * 5 * 5 * 5 * 7 : 2 - 8 + 6 - 8 + 6 + 8 * 6 * 3 * 5 * 9 + 1 = 8227

View File

@ -0,0 +1,2 @@
Rätsel: 2 ○ 9 ○ 1 ○ 3 ○ 2 ○ 9 ○ 4 ○ 8 ○ 1 ○ 6 ○ 2 ○ 4 ○ 9 ○ 8 ○ 4 ○ 5 ○ 7 = 17044
Lösung: 2 - 9 + 1 - 3 * 2 * 9 * 4 - 8 + 1 + 6 : 2 * 4 * 9 * 8 * 4 * 5 - 7 = 17044

View File

@ -0,0 +1,2 @@
Rätsel: 2 ○ 5 ○ 6 ○ 9 ○ 4 ○ 9 ○ 7 ○ 7 ○ 4 ○ 6 ○ 1 ○ 9 ○ 3 ○ 5 ○ 8 ○ 3 ○ 5 ○ 4 = 28009
Lösung: 2 - 5 + 6 * 9 * 4 : 9 * 7 * 7 * 4 * 6 + 1 - 9 - 3 * 5 * 8 : 3 * 5 - 4 = 28009

View File

@ -0,0 +1,2 @@
Rätsel: 6 ○ 9 ○ 9 ○ 6 ○ 8 ○ 9 ○ 4 ○ 5 ○ 9 ○ 5 ○ 5 ○ 1 ○ 9 ○ 3 ○ 2 ○ 8 ○ 4 ○ 3 ○ 7 = 32686
Lösung: 6 + 9 * 9 * 6 * 8 * 9 - 4 - 5 - 9 - 5 * 5 - 1 - 9 * 3 * 2 * 8 : 4 * 3 * 7 = 32686

View File

@ -0,0 +1,2 @@
Rätsel: 7 ○ 3 ○ 1 ○ 6 ○ 3 ○ 7 ○ 2 ○ 7 ○ 1 ○ 4 ○ 3 ○ 5 ○ 7 ○ 6 ○ 8 ○ 7 ○ 7 ○ 8 ○ 8 ○ 9 = 219070
Lösung: 7 + 3 - 1 - 6 * 3 * 7 : 2 * 7 - 1 + 4 * 3 * 5 * 7 : 6 * 8 * 7 * 7 * 8 - 8 - 9 = 219070

View File

@ -0,0 +1,2 @@
Rätsel: 1 ○ 7 ○ 8 = 2
Lösung: 1 - 7 + 8 = 2

View File

@ -0,0 +1,2 @@
Rätsel: 4 ○ 9 ○ 3 ○ 1 ○ 2 ○ 8 ○ 9 ○ 7 ○ 8 ○ 6 ○ 9 ○ 5 ○ 2 ○ 3 ○ 9 ○ 8 ○ 7 ○ 6 ○ 5 ○ 2 ○ 1 = 52647
Lösung: 4 - 9 * 3 - 1 - 2 - 8 * 9 * 7 * 8 + 6 + 9 * 5 * 2 * 3 * 9 * 8 * 7 : 6 * 5 : 2 - 1 = 52647

View File

@ -0,0 +1,2 @@
Rätsel: 3 ○ 7 ○ 8 ○ 6 ○ 8 ○ 2 ○ 5 ○ 4 ○ 1 ○ 6 ○ 6 ○ 2 ○ 2 ○ 7 ○ 3 ○ 2 ○ 9 ○ 8 ○ 5 ○ 7 ○ 6 ○ 1 = 49785
Lösung: 3 + 7 * 8 * 6 * 8 : 2 + 5 + 4 - 1 + 6 * 6 * 2 * 2 * 7 : 3 * 2 * 9 * 8 + 5 + 7 * 6 - 1 = 49785

View File

@ -0,0 +1,2 @@
Rätsel: 3 ○ 8 ○ 2 ○ 8 ○ 2 ○ 5 ○ 4 ○ 8 ○ 8 ○ 5 ○ 7 ○ 4 ○ 5 ○ 5 ○ 4 ○ 2 ○ 5 ○ 3 ○ 3 ○ 4 ○ 6 ○ 1 ○ 9 = 471837
Lösung: 3 - 8 - 2 - 8 : 2 * 5 * 4 * 8 - 8 + 5 * 7 * 4 * 5 * 5 * 4 : 2 * 5 * 3 * 3 : 4 * 6 + 1 - 9 = 471837

View File

@ -0,0 +1,2 @@
Rätsel: 4 ○ 4 ○ 5 ○ 1 = 2
Lösung: 4 + 4 - 5 - 1 = 2

View File

@ -0,0 +1,2 @@
Rätsel: 7 ○ 2 ○ 5 ○ 9 ○ 2 = 1
Lösung: 7 * 2 + 5 - 9 * 2 = 1

View File

@ -0,0 +1,2 @@
Rätsel: 7 ○ 4 ○ 2 ○ 8 ○ 8 ○ 3 = 36
Lösung: 7 * 4 - 2 * 8 + 8 * 3 = 36

View File

@ -0,0 +1,2 @@
Rätsel: 5 ○ 6 ○ 3 ○ 7 ○ 3 ○ 1 ○ 1 = 41
Lösung: 5 * 6 + 3 + 7 + 3 - 1 - 1 = 41

View File

@ -0,0 +1,2 @@
Rätsel: 1 ○ 9 ○ 3 ○ 9 ○ 5 ○ 8 ○ 3 ○ 3 = 92
Lösung: 1 * 9 - 3 + 9 + 5 + 8 * 3 * 3 = 92

View File

@ -0,0 +1,2 @@
Rätsel: 2 ○ 4 ○ 9 ○ 7 ○ 3 ○ 9 ○ 8 ○ 1 ○ 8 = 125
Lösung: 2 * 4 * 9 - 7 - 3 + 9 * 8 - 1 - 8 = 125

View File

@ -0,0 +1,2 @@
Rätsel: 8 ○ 5 ○ 4 ○ 7 ○ 8 ○ 3 ○ 6 ○ 3 ○ 8 ○ 3 = 285
Lösung: 8 * 5 * 4 + 7 * 8 + 3 - 6 + 3 * 8 * 3 = 285

View File

@ -0,0 +1 @@
nightly

View File

@ -1,18 +1,21 @@
#![feature(trait_alias)]
#![feature(trait_alias)] // aktiviert Trait Aliase
// Importe
use clap::Parser;
use std::collections::HashSet;
use std::collections::HashMap;
use std::collections::{HashSet, HashMap};
use rand::distributions::{Distribution, Uniform};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use num_traits::NumCast;
use num_integer::Integer;
use std::hash::Hash;
use std::fmt::Display;
use num_traits::bounds::Bounded;
// definiert einen Trait Alias, da diese Traits öfter gemeinsam benötigt werden
trait BasicInteger = Integer + Hash + Copy;
#[derive(FromPrimitive)]
// Enum für die vier Grundoperatoren
#[derive(FromPrimitive)] // Durch FromPrimitive können ganze Zahlen zu Enums konvertiert werden.
enum Operator {
Add,
Subtract,
@ -20,19 +23,17 @@ enum Operator {
Divide,
}
// Enum für Punkt- oder Strichrechnung
#[derive(FromPrimitive, PartialEq, Clone, Copy)]
enum AddSubOrMultiplicateDivide {
AddSub,
MultiplicateDivide,
}
// Struktur für die Kommandozeilenargumente
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
#[clap(about, long_about = None)]
struct Args {
/// maximale Anzahl der auszugebenden Lösungen
#[clap(short, long, default_value_t = 1)]
maximum: usize,
/// Anzahl der Operatoren
#[clap(short, long, default_value_t = 5)]
count: u8,
@ -40,107 +41,203 @@ struct Args {
/// zeigt die Lösung an
#[clap(short, long, takes_value = false)]
solution_print: bool,
/// nutzt vorgegebene Ziffern zur Erstellung des Rätsels (für reproduzierbare Testfälle)
#[clap(short, long)]
digits: Vec<u8>,
}
struct PartOperation<T> {
// mögliche Zwischenergebnisse eines Teils der Rechnung, der nur Punktrechnung enthält.
results: HashMap<T, Option<u64>>,
last_operator: Option<usize>,
last_operator: Option<usize>, // der Operator, der vor dieser Rechnungsteil steht
}
// Struktur, die eindeutige Ergebnisse speichert
struct ResultStore<T> {
riddles: HashMap<T, u64>,
riddles: HashMap<T, u64>, // speichert eindeutige Rätsel
// speichert Ergebnisse, für die es bereits mehrere Operatorenkombinationen gibt
results_already_taken: HashSet<T>,
}
impl<T: Eq + Hash> ResultStore<T> {
// erstellt einen neuen, leere ResultStore
fn new() -> Self {
Self{
Self {
riddles: HashMap::new(),
results_already_taken: HashSet::new(),
}
}
/* Methode, zum Speichern eines neuen Ergebnisses.
* Ist der Parameter operators == None, so ist das Ergebnis uneindeutig. */
fn store(&mut self, result: T, operators: Option<u64>) {
// Wenn das Ergebnis bereits uneindeutig ist, wird nichts gemacht.
if self.results_already_taken.contains(&result) {return;}
// Wenn es das Ergebnis bereits gibt, ...
if match operators {
None => true,
Some(_x) => {self.riddles.contains_key(&result)}
Some(_x) => self.riddles.contains_key(&result),
} {
// ... dann wird es von den eindeutigen Rätseln entfernt ...
self.riddles.remove(&result);
self.results_already_taken.insert(result);
self.results_already_taken.insert(result); // ... und zu den uneindeutigen hinzugefügt.
}
// Ist es eindeutig, wird zu den eindeutigen hinzugefügt.
else {self.riddles.insert(result, operators.unwrap());}
}
}
fn main() {
// Methode des crates clap, die die Kommandozeilenargumente einliest.
let args = Args::parse();
let mut digits: Vec<u8> = vec!(0; (args.count + 1) as usize);
{
let mut selectable_digits: Vec<u8> = Vec::from_iter(0..9);
let mut digits_count: [u8; 9] = [0; 9];
let min: u8 = digits.len() as u8 / 16;
let max: u8 = digits.len() as u8 / 4 + 1;
let mut left_for_non_min: u8 = digits.len() as u8 - 9 * min;
let mut digits_min_statisfied: HashSet<u8> = HashSet::with_capacity(9);
for digit in digits.iter_mut() {
if left_for_non_min == 0 {
selectable_digits.retain(|x| !digits_min_statisfied.contains(x));
}
let rand_i = Uniform::new(0, selectable_digits.len()).sample(&mut rand::thread_rng());
let rand_number = selectable_digits[rand_i];
digits_count[rand_number as usize] += 1;
let digit_used = digits_count[rand_number as usize];
if digit_used == min {
if left_for_non_min == 0 {
selectable_digits.remove(rand_i);
} else {digits_min_statisfied.insert(rand_number);}
}
if digit_used > min {left_for_non_min -= 1;}
if digit_used == max {selectable_digits.remove(rand_i);}
*digit = rand_number + 1;
}
/* Rätsel für größere Operatorenanzahlen können nicht berechnet werden, da die verwendeten
* Operatoren als u64 gespeichert werden müssen und für jeden Operator zwei Bits benötigt
* werden. Natürlich ist die praktische Grenze laufzeitbedingt schon vorher erreicht. */
if args.count > 32 {
println!("Es können maximal Rätsel mit 32 Operatoren erstellt werden.");
return;
}
let mut continue_searching = true;
let mut digits = args.digits;
// Wenn keine Ziffern bzw. Operanden vorgegeben wurden, werden diese zufällig ausgewählt.
let random_digits = digits.len() == 0;
while continue_searching {
if random_digits {
digits = vec!(0; (args.count + 1) as usize); // erstellt einen neuen Vektor für die Ziffern
/* die Ziffern, die noch zur Auswahl stehen. Dabei steht jedes Element des Vektors für
* die Ziffer, die um eins größer ist. */
let mut selectable_digits: Vec<u8> = Vec::from_iter(0..9);
let mut digits_count: [u8; 9] = [0; 9]; // Counter wie oft die jeweiligen Ziffern vorkommen
let min: u8 = digits.len() as u8 / 16; // minmale Anzahl der jeweiligen Ziffern
let max: u8 = digits.len() as u8 / 4 + 1; // maximale Anzahl
/* Anzahl der Operanden, die verwendet werden können ohne, dass dadurch der Erfüllung
* der minimalen Anzahl der Ziffern nähergekommen wird. */
let mut left_for_non_min: u8 = digits.len() as u8 - 9 * min;
/* die Ziffern, die bereits in der minimalen Anzahl vorhanden sind.
* Es sei denn die minimale Anzahl ist Null, dann ist das Hashset leer. */
let mut digits_min_statisfied: HashSet<u8> = HashSet::with_capacity(9);
for digit in digits.iter_mut() {
// Eine zufällige Ziffer wird aus den übrig bleibenden ausgewählt.
let rand_i = Uniform::new(0, selectable_digits.len()).sample(&mut rand::thread_rng());
let rand_number = selectable_digits[rand_i];
digits_count[rand_number as usize] += 1; // erhöht den Zifferncounter
let digit_used = digits_count[rand_number as usize];
if digit_used == min {
if left_for_non_min == 0 { // Wenn keine Operanden frei sind, ...
selectable_digits.remove(rand_i); // ... wird die Ziffer aus den ausgewählbaren entfernt.
} else {digits_min_statisfied.insert(rand_number);}
}
if digit_used > min { // wird die Ziffern öfter als notwendig verwendet
left_for_non_min -= 1;
/* Wenn die restlichen Operanden gebraucht werden, um die minimale Anzahl der
* übrigen Ziffern zu erfüllen, werden die Ziffern die bereits genug vorkommen,
* aus den auswählbaren entfernt. */
if left_for_non_min == 0 {
selectable_digits.retain(|x| !digits_min_statisfied.contains(x));
}
}
// Kommt eine Ziffer oft genug vor, wird diese aus den auswählbaren entfernt.
if digit_used == max {selectable_digits.remove(rand_i);}
/* weist die zufällig ausgewählte Ziffer zu. Diese wird zuvor noch um eins erhöht,
* da die Zufallszahl im Bereich von null bis acht liegt. */
*digit = rand_number + 1;
}
}
// berechnet das maximale Ergebnis durch die Multiplikation aller Operanden
let mut max_result: u128 = 1;
for digit in &digits {
max_result *= *digit as u128;
}
/* nutzt 64 Bit Integers wenn möglich und 128 Bit Integers das maximale Ergebnis zu groß für
* 64 Bit Integers ist. */
continue_searching = if max_result <= i64::MAX as u128 {
calc_results::<i64>(args.solution_print, &digits)
} else {calc_results::<i128>(args.solution_print, &digits)};
// Wenn die Ziffern vom Nutzer festgelegt sind, wird es nicht nochmal mit anderen versucht.
if !random_digits {break;}
}
calc_results::<i64>(args, digits);
}
fn calc_results<T: BasicInteger + 'static + FromPrimitive + NumCast>(args: Args, digits: Vec<u8>) {
let mut results = ResultStore::new();
for dm_as_map in 0..2u32.pow(args.count as u32) {
/* Funktion, die eindeutige Rätsel mit den vorgegebenen Operanden berechnet und eins davon ausgibt.
* Dabei kann der Integer-Typ der für die (Zwischen)ergebnisse verwendet mit Generics angegeben werden. */
fn calc_results<T: BasicInteger + 'static + FromPrimitive + Display + Bounded>
(solution_print: bool, digits: &[u8]) -> bool {
let number_operators = digits.len() - 1;
let mut results = ResultStore::new(); // Hier werden die Rätsel gespeichert.
// Bei dm_as_map steht jedes Bit für Punkt- oder Strichrechnung.
for dm_as_map in 0..2u32.pow(number_operators as u32) {
// enthält die Zwischenergebnisse mit bereits durchgeführter Punktrechnung
let mut results_multiplicate: Vec<PartOperation<T>> = Vec::new();
{
let mut last_i: usize = 0;
let mut i: usize;
let mut state: AddSubOrMultiplicateDivide = FromPrimitive::from_u32(dm_as_map & 1).unwrap();
while last_i < args.count as usize {
let mut state = AddSubOrMultiplicateDivide::from_u32(dm_as_map & 1).unwrap();
while last_i < number_operators {
i = last_i;
while i < args.count as usize && dm_as_map >> i & 1 == state as u32 {
// bestimmt das Ende einer Sequenz, die nur aus Punkt- oder Strichrechnung besteht
while i < number_operators && dm_as_map >> i & 1 == state as u32 {
/* In einer Strichrechnungssequenz können die Operanden direkt als
* Zwischenergebnisse verwendet werden. */
if state == AddSubOrMultiplicateDivide::AddSub {
results_multiplicate.push(insert_digit(&digits, i));
results_multiplicate.push(insert_digit(digits, i));
}
i += 1;
}
if state == AddSubOrMultiplicateDivide::MultiplicateDivide {
// speichert die Ergebnisse einer Punktrechnungssequenz
let mut part_results: HashMap<T, Option<u64>> = HashMap::new();
i += 1;
let mut digits_calc = digits[last_i .. i].iter();
let first_digit = FromPrimitive::from_u8(*digits_calc.next().unwrap()).unwrap();
let mut digits_calc = digits[last_i .. i].iter(); // Slice mit den Operanden der Sequenz
// der erste Operand der Sequenz
let first_digit = T::from_u8(*digits_calc.next().unwrap()).unwrap();
// berechnet die Zwischenergebnisse
multiplicate_divide(digits_calc, &mut part_results, last_i as u8 * 2, first_digit, 0);
// speichert die Zwischenergebnisse
results_multiplicate.push(PartOperation{
results: part_results,
last_operator: last_operator_helper(last_i),
});
}
/* Nachdem eine Punkt- oder Strichrechnungssequenz fertig, ist kommt eine Sequenz
* der anderen Rechenart. */
state = match state {
AddSubOrMultiplicateDivide::MultiplicateDivide => AddSubOrMultiplicateDivide::AddSub,
AddSubOrMultiplicateDivide::AddSub => AddSubOrMultiplicateDivide::MultiplicateDivide,
};
last_i = i;
}
if dm_as_map >> args.count - 1 & 1 == AddSubOrMultiplicateDivide::AddSub as u32 {
/* Wenn der letzte Operator eine Strichrechnung ist, muss der letzte Operand noch als
* Zwischenergebnis hinzugefügt werden. */
if dm_as_map >> number_operators - 1 & 1 == AddSubOrMultiplicateDivide::AddSub as u32 {
results_multiplicate.push(insert_digit(&digits, digits.len() - 1));
}
{
// führt die Strichrechnung mit den Zwischenergebnissen durch
let mut iter = results_multiplicate.iter();
for (part_result, operators) in &iter.next().unwrap().results {
add_sub(iter.clone(), &mut results, *part_result, *operators);
@ -148,62 +245,115 @@ fn calc_results<T: BasicInteger + 'static + FromPrimitive + NumCast>(args: Args,
}
}
}
let mut not_first = false;
let mut best_riddle_operators: Option<u64> = None; // die Operatoren des endgültig ausgewählten Rätsel
/* das Ergebnis des ausgewählten Rätsels. Zu Beginn wird der maximale Wert verwendet, damit die
* Bedingung, dass das Ergebnis des Rätsels kleiner ist, in beim ersten Rätsel erfüllt ist. */
let mut smallest_result = T::max_value();
for (result, operators) in results.riddles {
if not_first {println!();}
not_first = true;
let result_u128 = num_traits::cast(result).unwrap();
print_riddle(&digits, false, result_u128, operators);
if args.solution_print {
print_riddle(&digits, true, result_u128, operators);
let mut operator_count: [u8; 4] = [0; 4]; // Anzahlen der Operatoren
// zählt die Operatoren
for i in 0..number_operators {
operator_count[(operators as usize) >> 2 * i & 3] += 1;
}
/* überprüft, ob alle Operatorenanzahlen innerhalb eines Bereiches liegen und das Rätsel
* somit interessant ist. */
let mut interessing_riddle = true;
for count in operator_count {
interessing_riddle &= number_operators as u8 / 10 <= count && count <= number_operators as u8 / 2 + 1;
}
/* Ist das Rätsel interessant und das Ergebnis kleiner als das aktuelle Rätsel, dann wird
* dieses als vorläufig bestes Rätsel gespeichert. */
if interessing_riddle && result < smallest_result {
smallest_result = result;
best_riddle_operators = Some(operators);
}
}
match best_riddle_operators {
/* wurde kein Rätsel gefunden wird true zurückgeben und die Suche wird wiederholt, da der
* Rückwert als Bedingung in der while-Schleife genutzt wird. */
None => true,
Some(operators) => {
// Wenn ein Rätsel gefunden wurde, wird es ausgegeben.
print_riddle(digits, false, smallest_result, operators);
if solution_print { // Die Ausgabe der Lösung ist optional.
print_riddle(digits, true, smallest_result, operators);
}
false
}
}
}
/* rekursive Funktion, die Punktrechnung auf einer Sequenz der Operanden durchführt und die
* Ergebnisse in einer Hashmap speichert. */
fn multiplicate_divide<'a, T: BasicInteger + FromPrimitive>
(mut iter: impl Clone + Iterator<Item = &'a u8>, results: &mut HashMap<T, Option<u64>>,
operator_index: u8, part_result: T, operators: u64) {
match iter.next() {
/* Wenn keine Ziffern mehr übrig sind, werden die Operatoren in einer Hashmap abspeichert.
* Wenn das gleiche Zwischenergebnis bereits durch andere Operatoren erreicht wurde ist
* dieses uneindeutig und None wird in der Hashmap gespeichert. */
None => {
results.insert(part_result, if results.contains_key(&part_result)
{None} else {Some(operators)});
}
Some(next) => {
let next_digit = FromPrimitive::from_u8(*next).unwrap();
let next_digit = T::from_u8(*next).unwrap();
// führt Multiplikation und Division durch
for operator in [Operator::Multiplicate, Operator::Divide] {
multiplicate_divide(iter.clone(), results, operator_index + 2,
match operator {
Operator::Multiplicate => part_result * next_digit,
Operator::Divide => {
/* Ist das Ergebnis der Division keine ganze Zahl, wird diese
* abgebrochen. */
if part_result % next_digit != T::zero() {continue;}
part_result / next_digit
}
_ => T::zero(),
/* wird nie ausgeführt. Nur vorhanden, da bei einer match-Anweisung
* alle möglichen Werte abgedeckt werden müssen. */
_ => T::zero(), // Methode returned eine Null des Typs T
},
// verwendeter Operator wird in einer Bitmap gespeichert.
operators | (operator as u64) << operator_index);
}
}
}
}
/* rekursive Funktion, die Strichrechnung auf einer Sequenz der Zwischenergebnisse durchführt und
* die Ergebnisse mithilfe der Struktur ResultStore speichert. */
fn add_sub<'a, T: BasicInteger + 'static>
(mut iter: impl Clone + Iterator<Item = &'a PartOperation<T>>,
results: &mut ResultStore<T>, part_result: T, operators: Option<u64>) {
match iter.next() {
None => {
// speichert das Rätsel, wenn das Ergebnis eine natürliche Zahl ist
if part_result > T::zero() {results.store(part_result, operators);}
}
Some(next) => {
// für jedes mögliche Zwischenergebnis ...
for (next_result, part_operators) in &next.results {
// ... wird Addition und Subtraktion durchführt.
for operator in [Operator::Add, Operator::Subtract] {
add_sub(iter.clone(), results,
match operator {
Operator::Add => part_result + *next_result,
Operator::Subtract => part_result - *next_result,
/* wird nie ausgeführt. Nur vorhanden, da bei einer match-Anweisung
* alle möglichen Werte abgedeckt werden müssen. */
_ => T::zero(),
},
/* Wenn mindestens einer der beiden verrechneten Zwischenergebnisse
* uneindeutig ist, ist das resultierende Ergebnis auch uneindeutig. */
if operators == None || *part_operators == None {None}
else {
// Ansonsten werden die Teiloperatoren zusammengefasst gespeichert.
Some(operators.unwrap() | part_operators.unwrap() |
(operator as u64) << 2 * next.last_operator.unwrap())
}
@ -214,12 +364,16 @@ results: &mut ResultStore<T>, part_result: T, operators: Option<u64>) {
}
}
// gibt den Operatorenindex vor dem entsprechenden Operandenindex zurück
fn last_operator_helper(index: usize) -> Option<usize> {
if index == 0 {None} else {Some(index as usize - 1)}
}
fn insert_digit<T: Eq + Hash + FromPrimitive>(digits: &Vec<u8>, i: usize) -> PartOperation<T> {
// Hilfsfunktion, um eine einzelne Ziffer als Zwischenergebnis hinzufügen
fn insert_digit<T: Eq + Hash + FromPrimitive>(digits: &[u8], i: usize) -> PartOperation<T> {
let mut result_map: HashMap<T, Option<u64>> = HashMap::new();
// Some(0) heißt, dass keine Operatoren verwendet wurden
result_map.insert(T::from_u8(digits[i]).unwrap(), Some(0));
return PartOperation{
results: result_map,
@ -227,7 +381,8 @@ fn insert_digit<T: Eq + Hash + FromPrimitive>(digits: &Vec<u8>, i: usize) -> Par
};
}
fn print_riddle(digits: &Vec<u8>, show_solution: bool, result: u128, operators: u64) {
// gibt das Rätsel bzw. die Lösung des Rätsels aus
fn print_riddle<T: Display>(digits: &[u8], show_solution: bool, result: T, operators: u64) {
print!("{}: ", if show_solution {"Lösung"} else {"Rätsel"});
let mut i = 0;
for digit in digits[.. digits.len() - 1].iter() {