Written by Lazar Todorovic.
In diesem Kapitel führen wir den Begriff der “Kongruenzen” ein und erkunden, wie man mit ihnen rechnen kann (“modulare Arithmetik”). Ausserdem stellen wir den Euklidischen Algorithmus und seine Anwendungen vor.
Wenn im Folgenden von “Zahlen” die Rede ist, sind immer nicht-negative ganze Zahlen gemeint: 0, 1, 2, …
Ein allgemein bekanntes Beispiel für Kongruenzbeziehungen sind gerade und ungerade Zahlen. Gerade Zahlen sind ohne Rest (bzw. mit Rest 0) teilbar durch 2, während bei ungeraden ein Rest von 1 zurückbleibt, wenn man sie durch 2 teilt. Wenn zwei Zahlen bei der Division durch 2 den gleichen Rest haben, nennen wir sie kongruent modulo 2. Entsprechend sind alle geraden Zahlen kongruent modulo 2 zueinander, und ebenso alle ungeraden.
Diese Betrachtung muss natürlich nicht auf Division durch 2 beschränkt bleiben. Zwei Zahlen, nennen wir sie und , sind kongruent modulo , wenn sie den gleichen Rest bei der Division durch haben. Dabei wird als Modul bezeichnet und kann eine beliebige Zahl sein, solange sie grösser als 0 ist. Die mathematische Notation dafür ist
Die Restklasse von (modulo ) besteht aus allen Zahlen, die kongruent zu (modulo ) sind. Aus diesem Grund ist “ ist kongruent zu (modulo )” gleichbedeuted mit “ gehört (modulo ) der gleichen Restklasse an wie .” Eine Restklasse wird normalerweise mit der kleinsten Zahl bezeichnet, die in ihr vorkommt. Die Bezeichnung der Restklasse von modulo ist also genau der Rest der Division von durch . Modulo 2 gibt es die Restklassen 0 und 1. Allgemein gibt es modulo genau verschiedene Restklassen, nämlich die von 0, 1, …, . Die Restklasse von ist wieder die gleiche wie die von 0. Was ist die Restklasse von ?
Allgemein gilt, dass sich die Restklasse einer Zahl modulo nicht ändert, wenn man ein Vielfaches des Moduls zu ihr dazuaddiert oder subtrahiert; die folgende Kongruenz ist also für beliebige Zahlen und erfüllt (wobei auch negativ sein darf):
Umgekehrt kann jede Zahl, die in der gleichen Restklasse wie ist, als geschrieben werden, also als Summe von und einem Vielfachen von . Dies bedeutet, dass zwei Zahlen genau dann in der gleichen Restklasse sind, wenn ihre Differenz ein Vielfaches des Moduls ist. Wenn man Restklassen auf diese Weise definiert, ist es ausserdem klar, was die Restklasse einer negativen Zahl ist.
Du kennst vielleicht den Operator % aus C++ und ähnlichen Sprachen. Der Ausdruck a%n liefert dort den Rest der Division von durch . Alternativ kann man es auch so sehen, dass dieser Operator die kleinste Zahl zurückgibt, welche in der Restklasse von modulo ist.
Rechnen mit Restklassen
Addition Das spannende an Restklassen ist, dass man mit ihnen in vielerlei Hinsicht gleich rechnen kann wie mit Zahlen. Betrachte als Beispiel die folgende Kongruenz:
Wie oben erwähnt, ändert sich die Restklasse einer Zahl nicht, wenn man ein Vielfaches des Moduls zu ihr dazuaddiert. Wir können also auf jeder Seite der Beispielkongurenz beliebig Vielfache von 4 dazuaddieren, ohne dass sie ihre Gültigkeit verliert. Wir erhalten so z.B.
Indem man statt 12 eine anderes, geeignetes Vielfaches von 4 nimmt, kann man beide Summanden durch ein beliebiges Mitglied ihrer jeweiligen Restklasse ersetzen. Normalerweise will man die Summanden durch das kleinste Mitglied ihrer Restklasse ersetzen.
Beispiel: Auch wenn dies alles bislang recht abstrakt scheint (und es durchaus auch ist), so gibt es doch auch aus dem Alltag Beispiele für Rechnung mit Kongruenzen und Restklassen. Das wohl bekannteste davon kommt aus der Zeitrechnung.
(Bild: Wikipedia) Der Stundenzeiger einer Uhr gibt die Restklasse der gegenwärtigen Stunde modulo 12 an. Ebenso gibt der Minutenzeiger die Restklasse der gegenwärtigen Minute modulo 60 an. Welche Information gibt der Sekundenzeiger? Beachte, dass in diesem Fall die Restklasse jeweils die einzige vermittelte Information ist. Bei anderen Formen der Zeitrechnung (Jahresangaben, UNIX-Zeitstempel) ist dies anders. Beim Rechnen mit Stunden ist es ganz natürlich, so vorzugehen wie im vorherigen Absatz beschrieben. Wenn wir “sieben Stunden nach 11 Uhr”, also berechnen, ersetzen wir das Ergebnis durch die kleinste Zahl seiner Restklasse: Aus wird (Uhr). Ebenso können wir auch die Summanden ersetzen, z.B. wenn wir “21 Stunden nach 19 Uhr” bestimmen wollen. Dann wird zu , abschliessend wird das Ergebnis von zu (Uhr).
Multiplikation Für Multiplikationen gelten ähnliche Eigenschaften wie bei der Addition, wie wir etwas formaler zeigen werden. Wir betrachten die Multiplikationen zweier Zahlen und modulo . Wir ersetzen durch , welches für jede beliebige Zahl in der gleichen Restklasse wie ist. Analog ersetzen wir durch , wobei wiederum eine beliebige Zahl ist. Untersuchen wir nun, ob die folgende Kongruenz erfüllt ist:
also ob die Restklasse des Produkts sich ändert, wenn wir die Faktoren durch andere Mitglieder ihrer jeweiligen Restklasse ersetzen. Dazu können wir erst einmal ausmultiplizieren:
Beachte, dass immer ein Vielfaches von ist. Wir können es also von der linken Seite abziehen, ohne die Restklasse zu ändern, sprich
Die Eigenschaft, dass es bei der Addition und Multiplikation nicht darauf ankommt, welches Mitglied einer Restklasse man nimmt, ist bei der Modulo-Rechnung am Computer (und beim Lösen von vielen SOI-Aufgaben) ziemlich praktisch. Häufig besteht eine Aufgabe daraus, die Restklasse einer unter Umständen sehr grossen Zahl zu bestimmen, die selber nicht als 64-bit Ganzzahl dargestellt werden kann.
Beispiel: Berechne die kleinste Zahl in der Restklasse von . Ein ersten Ansatz wäre, zuerst zu berechnen, z.B. mit der Funktion pow. In C++ würde dies aber fehlschlagen, weil dort alle eingebauten Funktionen nur mit den Standard-Datentypen arbeiten, die aber alle zu klein sind um exakt darzustellen. (Der Typ double, mit dem die Funktion pow arbeitet, kann zwar auch Zahlen darstellen, die viel grösser sind als , macht dann aber Rundungsfehler.) Mit einem Datentyp BigInt, der beliebig grosse Zahlen unterstützt, könnte man im Prinzip das Ergebnis wie folgt berechnen:
BigInt a = 1; for (int k = 1; k <= 42434546; k++) a = 2*a; return (a+1)%1337;
Dieser Ansatz ist aus verschiedenen Gründen recht ineffizient. Übliche Implementationen des Datentyps BigInt würden für die Darstellung der Zahl ganze Bits, d.h. mehr als 5MB benötigen. Das ist so viel, dass sogar eine einfache Operation wie die Multiplikation mit zwei ziemlich viel Zeit in Anspruch nehmen würde, von der Verkettung von solcher Operationen ganz zu schweigen. Der Einsatz von BigInt ist aber gar nicht notwendig; wir brauchen nur zu wissen, welche Restklasse modulo 1337 das Resultat hat. Aus diesem Grund können wir jedes Zwischenergebnis wie im vorherigen Absatz beschrieben durch das kleinste Mitglied seiner Restklasse ersetzen. So müssen wir nie mit Zahlen rechnen, die grösser sind als . Der obige Code kann also durch den folgenden ersetzt werden:
int a = 1; for (int k = 1; k <= 42434546; k++) a = (2*a)%1337; return (a+1)%1337;
Wir kommen später darauf zu sprechen, wieso die Laufzeit auch dieser Variante noch bei weitem nicht optimal ist.
Zusammenfassend können wir sagen, dass in C++ für (fast) beliebige ints a, b und n die folgenden Ausdrücke wahr (d.h. true) sind:
( a + b ) % n == ( (a%n) + (b%n) ) % n ( a * b ) % n == ( (a%n) * (b%n) ) % n
Dies kann nur fehlschlagen, wenn irgendwo ein Überlauf passiert, also wenn z.B. das Resultat von (a%n) * (b%n) (oder irgendeiner der anderen Additionen/Multiplikationen) zu gross ist, um als int dargestellt zu werden.
Subtraktion Die Subtraktion von Restklassen funktioniert genau gleich wie die Addition. Beim Programmieren muss man aber beachten, dass das Ergebnis von a%n nicht in allen Programmiersprachen gleich ist, wenn negativ ist. So ist es in C++ immer negativ und man muss zum Ergebnis dazuzählen, um das kleinste nicht-negative Mitglied der Restklasse zu erhalten, d.h. man benutzt in diesem Fall a%n+n.