5. Arithmetische Algorithmen
5.3 Multiplikation
Schriftliche Multiplikation - die Schulmethode
Wie multipliziert man zwei natürliche Zahlen? Die "schriftliche Multiplikation" haben wir alle in der Grundschule gelernt. Daher ist Ihnen vielleicht gar nicht klar: die schriftliche Multiplikation ist ein Algorithmus! Und wahrscheinlich der erste "richtige", der Ihnen im Leben überhaupt begegnet ist. Man kann sogar noch weiter gehen und unsere Dezimaldarstellung als Datenstruktur zur Darstellung von natürlichen Zahlen bezeichnen, die sich gegen die römischen Zahlen) durchgesetzt hat, unter Anderem weil sie effiziente und vergleichsweise einfache Multiplikationsalgorithmen unterstützt. Rufen wir uns diesen "Uralgorithmus" ins Gedächtnis.
Der Schulalgorithmus zur schriftlichen Multiplikation benötigt eine Subroutine (bzw. eine
Helfer-Funktion), die eine beliebig lange Zahl mit einer einstelligen Zahl
multipliziert. Nennen wir diese Funktion integerTimesDigit
Sobald wir dies verstanden haben, können wir beliebig lange Zahlen multiplizieren:
Strenggenommen müssen wir noch zwei Subroutinen definieren: eine zum Addieren beliebig langer Zahlen (aber das überlasse ich Ihnen), und eine zur Multiplikation zweier einstelliger Zahlen. Für letzteres gibt es keinen Trick. Das müssen Sie auswendig lernen (unter dem Namen kleines Einmaleins). Machen Sie sich klar, dass der gerade beschriebene Algorithmus unabhängig von der Basis unseres Zahlensystems ist - nur das kleine Einmaleins ist für jede Basis anders. Für Basis 2 ist der obige Algorithmus besonders einfach.
Laufzeitanalyse
Lemma 5.3.1
Der Algorithmus integerTimesDigit
, der eine
Wie sieht es jetzt nun mit der Laufzeit der Schulmethode aus? Um eine
integerTimesDigit
insgesamt
die jeweils wieviele Stellen haben? Jede höchstens
Die längste Zahl,
Die Asymmetrie zwischen
Übungsaufgabe 5.3.1
Zeigen Sie, wie man die
Tip: Der Trick besteht darin, auszunutzen, dass von den maximal
Theorem 5.3.2 Die Schulmethode zur Multiplikation natürlicher
Zahlen benötigt
Divide-and-Conquer
Erinnern Sie sich, was Mergesort so einfach und erfolgreich gemacht hat: Wir
haben das unsortierte Array in zwei (fast) gleichgroße Teilarrays zerlegt;
jedes dieser zwei rekursiv sortiert; dann die beiden sortieren
Teilarrays mittels der recht einfachen Funktion merge
zu einem
ganzen sortieren Array zusammengefügt. Dieses Prinzip
Zerlegen - rekursiv lösen - zusammenfügen nennt man
in der Algorithmik Divide and Conquer. Können wir für
Multiplikation einen Divide-and-Conquer-Algorithmus entwickeln?
Wie zerlegt man "eine natürliche Zahl in zwei gleichgroße Hälften"?
Damit meine ich nicht
Beobachtung 5.3.3 Sei
Wie verwenden wir dies nun, um zwei natürliche Zahlen zu multiplizieren? Um unser Gehirn zu
schonen und damit
wir uns auf die wirklich wichtigen Dinge konzentrieren können, gehe ich im folgenden davon
aus, dass beide Zahlen
und schauen dann, was bei der Multiplikation geschieht:
An einem konkreten Beispiel:
Hier ist nun unser Pseudo-Code für den naiven Divide-and-Conquer-Algorithmus:
def naiveMult(x, y):
# x, y are two n-bit integers, and n is even
a = first(x)
b = secondHalf(x)
c = firstHalf(y)
d = secondHalf(y)
ac = naiveMult (a, c)
ad = naiveMult (a, d)
bc = naiveMult (b, c)
bd = naiveMult (b, d)
result = ac * 10**n + (ad + bc) * 10**(n/2) + cd
return result
Da fehlt natürlich noch die Rekursionsbasis, beispielsweise wenn *
und 10**n
in Zeile 13 einschüchtern: hier wird
nicht wirklich etwas multipliziert; mit
Beobachtung 5.3.4 Es gibt eine Zahl naiveMult
insgesamt höchstens
Die Zahl naiveMult
zu analysieren, müssen wir dieses
Sei naiveMult
im Worst-Case
macht, wenn es zwei
Warum? Der Term
Es sollte klar sein, dass naiveMult
zu verstehen), müssen wir den Rekursionsbaum
zeichnen. Ich nehme, wie oben bereits erwähnt, an, dass
Ein Knoten auf Ebene naiveMult
mit
zum Gesamtwert
Beobachtung 5.3.5 Wenn
Theorem 5.3.6 Die Anzahl der
Schritte, die naiveMult
braucht, um zwei
Beweis.
Sei naiveMult
höchstens
Schritte. Wir erinnern uns, dass
Wir haben also einen ganz gehörigen Aufwand betrieben und am Ende doch wieder nur
einen
Die Karatsuba-Multiplikation
Umsonst war der Aufwand doch nicht, weil er die Bühne bereitet für einen tatsächlich viel schnelleren Algorithmus: die Karatsuba-Multiplikation. Der fast schon magische Trick ist, die vier Multiplikationen in Zeilen 8, 9, 10, 11 durch drei Multiplikationen (und mehrere Additionen) zu ersetzen. Die zentrale Beobachtung ist
Wir berechnen nun also rekursiv die Produkte
def karatsuba (x, y):
# x, y are two n-bit integers, and n is even
a = first(x)
b = secondHalf(x)
c = firstHalf(y)
d = secondHalf(y)
ac = karatsuba (a, c)
bd = karatsuba (b, d)
a_plus_b_times_c_plus_d = karatsuba (a+b, c+d)
ad_plus_bc = a_plus_b_times_c_plus_d - ac - bd
result = ac * 10**n + ad_plus_bc * 10**(n/2) + cd ac * 10**n + (ad + bc) * 10**(n/2) + cd
return result
Die Implementierung ist nun etwas schwieriger, weil wir zusätzlich Subtraktion als
Subroutine benötigen. Aber auch dies geht in naiveMult
an); der Basisfall
verursacht höchstens
Das geht einfach zu analysieren, wenn
Auf Ebene
Für
Wir brauchen nun die Formel für die geometrische Summe:
Somit gilt
Bevor wir nun eine schöne Form für
Theorem 5.3.7 Die Laufzeitkomplexität der
Karatsuba-Multiplikation ist
Beweis.
Sei
Schritte. Wir schätzen nun
Die Anzahl der Schritte ist also höchstens