DIGITAL
structs
), die wir zwar bereits in der letzten Lehreinheit kennen gelernt haben, aber noch nicht dazu gekommen sind damit zu arbeiten. struct
s hervorragend dafür, zusammengehörige Daten zu gruppieren. Das erleichtert beispielsweise das Weitergeben von Daten zwischen Funktionen. Insbesondere beim Einlesen von Daten aus einer externen Quelle bietet es sich an, diese in ein für C++ verständliches Format zu überführen. Dazu entwickeln wir eine (sehr kleine) Software zum Auswerten biologischer Daten. structs
an, die wir zum Rechnen verwenden können; wir implementieren eine Erweiterung von uint64_t
auf die dreifache Speichergröße von \(192\)-bits.
Wir implementieren eine einfache dateibasierte Datenbankabfrage.
Das Settting:
In der Biologie kann man Bakterien unter anderem daran identifizieren, welche Antibiotika gegen Sie wirksam sind und welche nicht (siehe auch https://de.wikipedia.org/wiki/Antibiogramm). Dazu sammelt man im Voraus die Reaktionen vieler bereits bekannter Bakterien auf mehrere Antibiotika und speichert diese in einer Datenbank. Will man jetzt ein unbekanntes Bakterium identifizieren, so testet man dessen Reaktion auf dieselben Antibiotika und sucht dann in seiner Datenbank nach Bakterien mit gleichen Eigenschaften.
Warum Dateibasiert?:
Die Aufgabe sieht nicht vor, die ganze Datenbankdatei — etwa als Array das alle Structs enthält — in den Speicher zu laden. Noch schlimmer: jedes mal, wenn man eine Anfrage an die Datenbank stellen muss die ganze(!) Datei neu eingelesen werden. Bei besonders großen Datenbanken wird sich dieses Verfahren als aufwändiges Unterfangen enttarnen. Es gibt jedoch Anwendungsfälle für genau solch ein Vorgehen — Tape Drives. Magnetbänder haben den Vorteil, dass ihre Speicherkapazität deutlich über der von HDDs liegt, für einen Bruchteil der Kosten. Und das Beste: Man kann sie länger lagern und man muss sie nicht ständig mit Strom vesorgen, damit die Daten darauf nicht verschwinden. Um etwa solche Datenmengen zu untersuchen, müssen die Daten komplett prozessiert werden1. Eine simple Version davon implementieren wir in dieser Aufgabe.
[1] Wenn man weiß wonach man später suchen wird, kann man natürlich die Daten strukturierter abspeichern. (Siehe Datenbanken-Vorlesung)
In dieser Aufgabe werden wir eine solche Datenbankabfrage (natürlich stark vereinfacht) nachimplementieren. Dazu wird eine (synthetische, also nicht auf echten Messungen basierende) Datenbank als Textdatei vorgelegt.
bacteria-final.zip
(13 Mb, entpackt 79 Mb)Die Datei ist folgendermaßen aufgebaut:
1000000
6069329 Gary000001 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 1 1 0 0 1 0 1 1 1 0 0 1 0 0
8664926 Larry000001 0 0 0 0 1 1 1 0 0 1 1 0 1 0 0 0 1 1 1 1 0 0 0 1 1 1 0 0 0 0
4211696 Sherry000001 1 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 1 0 0 0 0 0
3714429 Larry000002 0 1 0 0 1 1 1 0 0 1 1 0 1 0 0 0 1 1 1 1 0 0 0 1 1 1 0 0 0 1
3534463 Larry000003 0 1 1 0 1 1 1 1 1 0 0 0 0 0 0 0 1 1 0 0 1 0 1 1 1 0 0 1 1 1
3834709 Harry000001 0 1 1 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0
1895957 Sherry000002 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 1 0 0 0 0 1
2050729 Cary000001 1 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
3949739 Terry000001 0 1 0 0 0 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1
Um diese Werte sinnvoll zu repräsentieren, verwenden wir folgendes struct
.
struct Bacterium {
uint64_t id;
std::string name;
bool antibiotic_sensitivity[30];
};
Aufgaben
Denken Sie daran, dass Argumente in C++ bei der Übergabe standardmäßig kopiert werden. Verwenden Sie Referenzen, um dies zu verhindern.
print_bacterium
, die eine Instanz von Bacterium
nimmt und diese lesbar auf der Konsole ausgibt.print_database
, die die Textdatei einliest und jedes Bakterium mithilfe von print_bacterium
auf der Konsole ausgibt. Sie können hier zusätzlich eine Hilfsfunktion schreiben, die eine einzelne Zeile aus der Datei einliest und ein einzelnes Bacterium
zurückgibt.distance
, die zwei Bakterien nimmt und zurückgibt, bei wie vielen Antibiotika sich die Reaktion auf das jeweilige Antibiotikum zwischen den Bakterien unterscheidet, d.h. an wievielen Stellen die Einträge in antibiotic_sensitivity
nicht paarweise übereinstimmen.print_database
(große Teile können kopiert werden) zu einer Funktion namens find_closest
so, dass diese zusätzlich ein einzelnes Bakterium b
als Argument nimmt und das Bakterium zurückgibt, das b
gemäß des zuvor definierten Distanzmaßes am ähnlichsten ist. (Kürzere Distanzen sind besser!)antibiotic_sensitivity
vor. Finden Sie für alle drei Messungen mithilfe ihrer vorher implementierten Methoden ein bekanntes Bakterium mit der kürzesten Distanz zur jeweiligen Messung. Es reicht, wenn sie die Identifikationsnummer des gefundenen Bakteriums aufschreiben. 0 0 1 0 1 1 1 0 0 1 0 0 0 0 1 1 1 1 0 1 0 0 1 1 1 0 1 1 1 1
0 1 1 0 1 1 0 0 1 0 0 1 0 0 0 0 1 1 0 1 1 0 1 1 1 0 1 1 0 0
0 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0 1 1 1 0 1 1 1 0 1 0 0 1 1 1
#include <fstream>
#include <iostream>
int main(int argc, char * argv[])
{
std::ofstream myfile("data.txt");
int a = 42;
int b = 7;
int c = 789;
int d = 8;
myfile << a;
myfile << " "; // Leerzeichen nicht vergessen
myfile << b;
myfile << std::endl; // return wird ignoriert beim Wiedereinlesen
myfile << c << " " << d;
return 0;
}
#include <fstream>
#include <iostream>
int main(int argc, char * argv[])
{
std::ifstream myfile("data.txt");
int a,b,c,d;
myfile >> a >> b >> c;
myfile >> d;
std::cout << a << " " << b << " " << c << " " << d;
return 0;
}
int main() {
// Beispiel für fünf Fließkommawerte:
double testDouble[5];
// Mit geschweiften Klammern können Arrays initial belegt werden.
double testDouble[] = {1.2, 1.6, -2.4};
// Auf einzelne Elemente kann ebenfalls mit eckigen Klammern zugegriffen
// werden, beispielsweise würde folgender Befehl das Element an Index 1 ein
// vom Wert 1.6 auf 2.4 setzen.
testDouble[1] = 2 * testDouble[0];
}
Aus der Vorlesung und dem letzten Blatt wissen Sie, dass alle Standarddatentypen für Ganzzahlen in C++ nur begrenzt viele Werte annehmen können. Im Normalfall reichen diese auch aus. Wir werden uns jetzt aber eine Möglichkeit anschauen, wie man diese Einschränkung umgehen kann, indem wir eine einen größeren Ganzzahlentyp definieren. Konkret definieren wir eine vorzeichenlose 192-Bit-Zahl, die sich aus drei 64-Bit-Zahlen zusammensetzen wird. Die Repräsentation an sich ist kein Problem, die Schwierigkeit dieser Aufgabe besteht darin, die Rechenregeln auf diese Repräsentation anzuwenden.
Als interne Repräsentation verwenden wir ein dreielementiges Array parts
aus Werten vom Typ uint64_t
. Unsere dargestellte Zahl ergibt sich dann durch \(\texttt{parts[2]} \cdot 2^{2 \cdot 64} + \texttt{parts[1]} \cdot 2^{1 \cdot 64} + \texttt{parts[0]} \cdot 2^{0 \cdot 64}\), wir rechnen also effektiv mit einer Zahl in der Basis \(2^{64}\).
Die Aufgabe ist einen 192-bit (positiven) Ganzzahlentyp zu definieren. Um den Einstieg dazu etwas zu vereinfachen haben wir den Header für Sie bereits vollständig implementiert. Außerdem geben wir zwei Konstruktoren vor.
#include <cstdint>
struct uint192 {
uint64_t parts[3];
uint192();
uint192(uint64_t hi, uint64_t mid, uint64_t lo);
void print_binary();
void print_hex();
void print_decimal();
};
uint192 operator +(const uint192 &lhs, const uint192 &rhs);
uint192 operator *(const uint192 &lhs, const uint192 &rhs);
uint192 operator <<(const uint192 &num, uint64_t shift);
uint192 operator >>(const uint192 &num, uint64_t shift);
bool operator ==(const uint192 &lhs, const uint192 &rhs);
bool operator !=(const uint192 &lhs, const uint192 &rhs);
bool operator <(const uint192 &lhs, const uint192 &rhs);
bool operator <=(const uint192 &lhs, const uint192 &rhs);
bool operator >(const uint192 &lhs, const uint192 &rhs);
bool operator >=(const uint192 &lhs, const uint192 &rhs);
So etwa könnte Ihre cpp-Datei aussehen:
#include "uint192.h"
uint192::uint192() : uint192(0, 0, 0) {}
uint192::uint192(uint64_t hi, uint64_t mid, uint64_t lo) {
parts[0] = lo;
parts[1] = mid;
parts[2] = hi;
}
// YOUR CODE HERE
int main() {
// ...
}
Bei der Bearbeitung empfehlen wir folgendes Vorgehen:
uint64_t
das gleichwertige uint192
macht.uint16_t a = 43690; // C++14 erlaubt auch a = 0b1010101010101010;
uint16_t b = 32768; // C++14 erlaubt auch b = 0b1000000000000000;
uint16_t c = a + b; // c == 10922 oder c == 0b0010101010101010;
uint16_t carry = ((a >> 15) + (b >> 15)) >> 1;
a
und b
an, für die der Code nicht den korrekten Übertrag liefert.uint64_t
inklusive Übertrag liefert. (Erklären Sie kurz, etwa als gut gekennzeichneter Kommentar im Code, wie Ihre Methode funktioniert.)operator+
.uint192 a ( 3, ~7ull, ~42ull );
uint192 b ( 2, 21, 46 );
uint192 c ( 6, 14, 3 );
// es sollte gelten: a + b == c
operator<<
und operator>>
wie im Header vorgegeben. Denken Sie daran, dass jetzt Verschiebungen um mehr als 64 Bit möglich sind! uint192 a ( 3, ~7ull, ~42ull );
uint192 b ( 15, (~7ull << 2) + 3, ~42ull << 2 );
uint192 c ( 0, ~1ull, 4611686018427387893ull );
// es sollte gelten: a << 2 == b
// es sollte gelten: a >> 2 == c
operator*
.Rechne 214 * 123 = 26322
* | 1 2 3
--+-----------------
2 | 2 4 6
1 | 1 2 3
4 | 4 9 2
--+----------------- +
2 6 3 2 2
operator==
, operator!=
, operator>
, operator>=
, operator<
und operator<=
. print_binary
, um die Zahl in Binärdarstellung auf der Konsole auszugeben sowieprint_hex
, um die Zahl in Hexadezimaldarstellung auf der Konsole auszugeben.uint64_t
speichern, wohl aber in einem uint192
. Schreiben Sie eine Funktion uint192 factorial(uint64_t n)
, die die Fakultät von n in einem uint192
berechnet.main
-Methode die Fakultät von 45 aus.print_decimal
, um die Zahl in Dezimaldarstellung auf der Konsole auszugeben. Wir verlangen an dieser Stelle natürlich nicht einen effizienten Algorithmus, der die Aufgabe löst, eigenständig zu entwerfen und dann zu implementieren. Alternativ schlagen wir zwei sehr einfach zu verstehende und zu implementierende Möglichkeiten vor:*
und einen Repräsentanten für die Zahl \(10\). struct
names uint192divmod10
. Dieses implementiert die Division eines uint192
durch 10 mit Rest. Möchte man eine Zahl n durch 10 dividieren, kann man mittels uint192divmod10 result(n);
ein uint192divmod10
-Objekt anlegen und dann über result.quotient
und result.remainder
auf den Quotienten und den Divisionsrest zugreifen. Dadurch können Sie die Dezimalstellen einzeln extrahieren.#include <cstdint>
struct uint192divmod10 {
uint192 quotient;
uint64_t remainder;
explicit uint192divmod10(const uint192 ÷nd);
};
#include "uint192.h"
#include <iostream>
// für die Bonusaufgabe
constexpr uint64_t HALF = static_cast<uint64_t>(0xffffffffull);
constexpr uint64_t SHIFT = static_cast<uint64_t>(32u);
uint192divmod10::uint192divmod10(const uint192 ÷nd) {
remainder = 0;
for (uint64_t i = 0; i < 3; ++i) {
uint64_t part = 2 - i;
uint64_t upper_dividend = remainder << SHIFT | dividend.parts[part] >> SHIFT;
uint64_t upper_quotient = upper_dividend / 10;
remainder = upper_dividend % 10;
uint64_t lower_dividend = remainder << SHIFT | (dividend.parts[part] & HALF);
uint64_t lower_quotient = lower_dividend / 10;
remainder = lower_dividend % 10;
quotient.parts[part] = upper_quotient << SHIFT | lower_quotient;
}
}
// Bonusaufgabe Ende