Ackermannfunktion
Die Ackermannfunktion ist eine 1926 von Wilhelm Ackermann erfundene, extrem schnell wachsende mathematische Funktion, mit deren Hilfe in der theoretischen Informatik Grenzen von Computer- und Berechnungsmodellen aufgezeigt werden können. Heute gibt es eine ganze Reihe von Funktionen, die als Ackermannfunktion bezeichnet werden. Diese weisen alle ein ähnliches Bildungsgesetz wie die ursprüngliche Ackermannfunktion auf und haben ein ähnliches Wachstumsverhalten.
Entstehungsgeschichte
1926 vermutete David Hilbert, dass jede berechenbare Funktion primitiv-rekursiv ist. Vereinfacht gesprochen bedeutet dies, dass jede Funktion, die durch einen Computer berechnet werden kann, sich aus einigen wenigen sehr einfachen Regeln zusammensetzen lässt und die Dauer der Berechnung sich im Vorfeld abschätzen lässt. Dies trifft auf nahezu alle in der Praxis vorkommenden Funktionen zu. (Die Ausnahmen sind vorwiegend im Bereich des Compilerbaus zu finden.) Im gleichen Jahr konstruierte Ackermann eine Funktion, die diese Vermutung widerlegt, und veröffentlichte sie 1928. Diese Funktion wird heute ihm zu Ehren Ackermannfunktion genannt. Die Ackermannfunktion kann also von einem Computer in endlicher Zeit ausgewertet werden, ist aber nicht primitiv-rekursiv.
1955 konstruierte Rózsa Péter eine vereinfachte Version, die den gleichen Dienst erweist. Diese Funktion, gelegentlich auch als Ackermann-Péter-Funktion bezeichnet, wird heute vorwiegend benutzt.
Idee
Man betrachtet die Folge , , , Hierbei wird bei jedem Folgeglied die Operation des vorigen Folgeglieds mal auf angewandt, also ist gerade , wobei die Variable -mal vorkommt und so weiter. Die Idee hinter der Ackermannfunktion ist es, diese Folge als Funktion aufzufassen.
- Beispiel: Setzt man in obiger Folge und , so erhält man die Folge: 6, 8, 16, 65536, (mit 65536 Zweien), ..., die man auch die Ackermannfunktion zu und nennt. Die letzte aufgeführte Zahl ist bereits wesentlich größer als die geschätzte Anzahl der Atome im gesamten Weltall.
Die Ackermannfunktion ist also eine Funktion, die die folgenden Gleichungen erfüllt:
- Fehler beim Parsen (SVG (MathML kann über ein Browser-Plugin aktiviert werden): Ungültige Antwort („Math extension cannot connect to Restbase.“) von Server „http://localhost:6011/de.wikipedia.org/v1/“:): {\displaystyle \phi(a,b,3) = a^{a^{.^{.^{.^{a}}}}} b-mal }
Ab der fünften Zeile können die Funktionswerte nicht mehr mit herkömmlichen Operatoren formuliert werden; man braucht erweiterte Notationen wie beispielsweise den Hyper-Operator.
Definition und Varianten
Die Ackermannfunktion definiert man üblicherweise rekursiv, d.h. man macht für einige Anfangswerte explizite Angaben und gibt eine Anleitung (das Rekursionsschema), wie man weitere Funktionswerte aus den bereits berechneten erhält.
Definition von Ackermann
Ackermann selbst definierte die Funktion auf recht umständliche Weise, gab aber kurz darauf die folgende äquivalente Definition an.
Dabei ist ein weitere Funktion, die Ackermann nicht weiter beschrieb. (Sie liefert die Startwerte , , , .)
- Beispiele:
- Möchte man berechnen, so kann man die erste Zeile der Definition anwenden und erhält direkt: .
- Möchte man berechnen, so kann man die zweite Zeile der Definition anwenden und erhält ebenfalls direkt: . Man beachte, dass in diesem Fall den Wert 1 hat, da auf der linken Seite der Definition steht.
- Komplizierter wird es, wenn weder das zweite, noch das dritte Argument 0 ist. Beispielsweise möchte man berechnen. Da sich die ersten beiden Zeilen der Definition nicht anwenden lassen, muss man die dritte verwenden. Damit erhält man: . wurde aber gerade im vorigen Beispiel berechnet. Setzt man dies ein, so erhält man . Jetzt kann man wieder die dritte Zeile anwenden, dann nochmal die zweite und zuletzt die erste Zeile der Definition. Alles zusammen ergibt sich in diesem Fall:
Wenn man vom Wachstum der Ackermannfunktion spricht, meint man oftmals die Funktion .
Definition von Péter
Rózsa Péter definierte 1955 eine einfachere Version der Ackermannfunktion, die nur zwei Parameter besitzt und zudem ohne die Hilfsfunktion auskommt:
Auch hier meint man im Zusammenhang mit Wachstumsuntersuchungen oftmals die Funktion , wenn man von der Ackermannfunktion spricht.
Bedeutung in der theoretischen Informatik
Wie eingangs schon erwähnt, erfand Ackermann diese Funktion als Beispiel einer Funktion, die nicht primitiv-rekursiv ist, aber berechenbar. Dies ist auch heute noch das wichtigste Anwendungsgebiet der Ackermannfunktion.
Auf der Suche nach den Grenzen von Computern stößt man sehr schnell auf den Begriff der berechenbaren Funktionen. Das sind all die Funktionen, für deren Auswertung man einen Algorithmus angeben kann, mit anderen Worten alle Funktionen, die ein Computer berechnen kann. Diese Definition stellt einen sehr schnell vor ein Problem, wenn man von einer konkreten Funktion entscheiden möchte, ob sie berechenbar ist. Findet man einen Algorithmus, der die Funktion berechnet, so ist sie offensichtlich berechenbar. Andernfalls ist ungewiss, ob die Funktion wirklich nicht berechenbar ist oder ob es zwar einen Algorithmus gibt, man ihn aber nicht gefunden hat.
Aus diesem Grund sucht man nach alternativen Definitionen, die einen solchen Nachweis einfacher führen lassen. Ein erster Ansatz hierfür waren die primitiv-rekursiven Funktionen. Dies sind Funktionen, die sich durch einige wenige Regeln aus sehr einfachen Funktionen zusammensetzen lassen.
Einige Zeit vermutete man, dass alle berechenbaren Funktionen primitiv-rekursiv sind, mit den primitiv-rekursiven Funktionen also ein Werkzeug zur Lösung des oben geschilderten Problems gefunden sei. Diese Hoffnung zerstörte die Ackermannfunktion, von der man nachweisen kann, dass sie berechenbar ist, aber nicht primitiv-rekursiv. (Siehe nachfolgenden Expertenabschnitt.)
Anmerkung: Inzwischen weiß man, dass man aus den primitiv-rekursiven Funktionen die berechenbaren erhält, wenn man noch eine weitere Konstruktionsregel, die sogenannte µ-Rekursion, zulässt.
Beweis
Der Beweis, dass die Ackermannfunktion berechenbar ist, aber nicht primitiv-rekursiv, nutzt im Wesentlichen aus, dass die Ackermannfunktion stärker wächst als jede primitiv-rekursive Funktion.
Beweisskizze zur Behauptung, dass die Ackermannfunktion nicht primitiv-rekursiv ist:
Für Details zum Beweis sehe man z.B. im Buch von Schöning nach. (Siehe Literatur) |
Anwendungen
Für die Ackermannfunktion gibt es nur sehr wenige Anwendungen. Die zwei wichtigsten sind als Benchmarktest für rekursive Aufrufe in Programmiersprachen und für die Laufzeitabschätzung im Zusammenhang mit der Union-Find-Datenstruktur.
Benchmark für rekursive Aufrufe
Bei der Einführung von neuen Programmiersprachen, Compilern und Computern möchte man deren Leistungsfähigkeit untersuchen. Eine Möglichkeit hierfür sind Benchmarktests, bei denen ein spezielles Programm benutzt wird, um bestimmte Eigenschaften zu überprüfen.
In diesem Zusammenhang wird die Ackermannfunktion gerne als Benchmark zur Überprüfung von rekursiven Prozeduraufrufen benutzt, da ein Programm zur Berechnung der Ackermannfunktion im Wesentlichen nur aus solchen Prozeduraufrufen besteht. In der Definition von Péter wird ja nur direkt berechnet. Die Schwierigkeit bei der Berechnung der Funktionswerte sind also nicht allein deren Größe, sondern die tiefe Verschachtelung der Funktionsaufrufe, die leicht zu einem Stack Overflow führt, also dazu, dass dem System der Speicher ausgeht.
Diese Idee geht zurück auf Yngre Sundblad, der 1971 die Funktion benutzte um diverse Programmiersprachen zu vergleichen. Um zu berechnen, werden Aufrufe getätigt.
Sundblad testete unter anderem, wie groß n gewählt werden kann, damit der Computer noch in der Lage ist, diese Zahl zu berechnen. Damals erreichte er . Zum Vergleich hierzu: Mit Java 1.4.2 mit den Standardspeichereinstellungen erreicht man heutzutage .
Im Laufe der Berechnung werden viele identische Aufrufe mehrfach ausgerechnet. Ein intelligenter Compiler kann dies ausnutzen und die Ergebnisse zwischenspeichern, um solche Aufrufe nur einmal durchführen zu müssen. Damit waren schon 1971 Aufrufe bis durchführbar. Einen bedeutenden Zeitvorteil erhält man auch, wenn man direkt berechnet, statt es rekursiv zu zu expandieren. Die direkte Berechnung von erfordert lineare Zeit in . Die Berechnung von erfordert quadratische Zeit, denn sie führt zu (also für eine Konstante ) verschachtelten Aufrufen von für verschiedene . Die Berechnung von erfordert schließlich eine zu proportionale Zeit (; zur Erklärung der Groß-O-Notation siehe Landau-Symbole).
Laufzeitabschätzung bei Union-Find
Da die Funktion sehr schnell wächst, wächst ihre Umkehrfunktion sehr langsam. Diese Umkehrfunktion taucht in der Laufzeitanalyse bestimmter Algorithmen auf, zum Beispiel beim Union-Find-Problem. In diesem und anderen Zusammenhängen wird die Ackermannfunktion oft leicht umdefiniert zu einer Funktion mit ähnlichem asymptotischem Verhalten. Diese modifizierten Funktionen sind nicht äquivalent zur Ackermannfunktion, aber nach den Maßstäben der Laufzeitanalyse können sie als äquivalent betrachtet werden. Die Umkehrfunktion der Funktion f ist für jede vorstellbare Eingabegröße kleiner als 4, kann also in der praktischen Analyse von Algorithmen als konstant betrachtet werden.
Implementation
In der Programmiersprache C++ mit den Datenstrukturen der Standard Template Library lässt sich die Ackermannfunktion wie folgt ohne Rekursion implementieren:
int ack(int x, int y) { std::stack<int> stack; while (x > 0 || !stack.empty()) { if (x == 0) { x = stack.top(); stack.pop(); y = y + 1; } else if (y == 0) { y = 1; x = x - 1; } else { y = y - 1; stack.push(x - 1); } } return y + 1; }
Die Rekursion wird dabei durch einen Stack simuliert.
Wertetabelle
Die folgende Tabelle zeigt einige Funktionswerte für kleine Werte von und . Die nicht vollständig ausgerechneten Werte sind zu groß, um sie dezimal darzustellen.
n\m | 0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | |
1 | 2 | 3 | 4 | 5 | 6 | |
2 | 3 | 5 | 7 | 9 | 11 | |
3 | 5 | 13 | 29 | 61 | 125 | |
4 | 13 | 65533 | ¹ | ( Terme) | ||
5 | 65533 | |||||
6 |
¹eine Zahl mit 19729 Dezimalstellen
Genauere Betrachtung
An Hand der Wertetabelle lässt sich ein Schema zur Berechnung der Funktionswerte herleiten, das leichter zu verstehen ist als die formale rekursive Definition. Intuitiv ist die erste Zeile der Wertetabelle (genauer: die Werte von ) einfach eine Liste aller natürlichen Zahlen, und die auf das Argument y angewandte mathematische Operation ist die Addition einer 1 zu . Alle folgenden Zeilen enthalten einfach Anweisungen, in dieser Zeile einen Wert zu suchen. Im Falle der Zeile vereinfacht sich diese Anweisung dahingehend, den Wert in der Zeile zu suchen, aber diese Vereinfachung ist schon etwas schwieriger herzuleiten – zum Beispiel:
a(1, 2) = a(0, a(1,1)) = a(0, a(0, a(1,0))) = a(0, a(0, 2)) = a(0, 3) = 4
Wir betrachten nun einen komplexeren Fall, nämlich , den ersten Funktionswert, der so groß ist, dass er praktisch nicht dezimal aufgeschrieben werden kann.
a(4, 3) = a(3, a(4, 2)) = a(3, a(3, a(4, 1))) = a(3, a(3, a(3, a(4, 0)))) = a(3, a(3, a(3, a(3, 1)))) = a(3, a(3, a(3, a(2, a(3, 0))))) = a(3, a(3, a(3, a(2, a(2, 1))))) = a(3, a(3, a(3, a(2, a(1, a(2, 0)))))) = a(3, a(3, a(3, a(2, a(1, a(1, 1)))))) = a(3, a(3, a(3, a(2, a(1, a(0, a(1, 0))))))) = a(3, a(3, a(3, a(2, a(1, a(0, a(0, 1))))))) = a(3, a(3, a(3, a(2, a(1, a(0, 2)))))) = a(3, a(3, a(3, a(2, a(1, 3))))) = a(3, a(3, a(3, a(2, a(0, a(1, 2)))))) = a(3, a(3, a(3, a(2, a(0, a(0, a(1, 1))))))) = a(3, a(3, a(3, a(2, a(0, a(0, a(0, a(1, 0)))))))) = a(3, a(3, a(3, a(2, a(0, a(0, a(0, a(0, 1)))))))) = a(3, a(3, a(3, a(2, a(0, a(0, a(0, 2)))))) = a(3, a(3, a(3, a(2, a(0, a(0, 3))))) = a(3, a(3, a(3, a(2, a(0, 4))))) = a(3, a(3, a(3, a(2, 5))))
Das ist für einige Zeit der einfachste Fall einer solchen Expansion, und es ist nun offensichtlich, warum Funktionswerte wie dieser in der obigen Tabelle selten direkt berechnet werden. Es ist auch interessant festzustellen, wie viele Schritte nötig sind, um schon sehr einfach aussehende Ackermann-Ausdrücke zu vereinfachen. Jede Zeile im vorigen Beispiel ist eine einzige Anwendung eines der drei Teile der Definition der Ackermannfunktion. Wenn wir an dieser Stelle mehrere logische Schritte überspringen, könnten wir zu 13 auswerten und dann versuchen, auszuwerten – das ist 65533. Der nächste Funktionsaufruf, , liefert eine Zahl, die der Zahl der Atome im Universum entspricht, einige Dutzend mal mit sich selbst multipliziert. Diese Zahl wird schließlich in die Berechnung eingesetzt, der irgendwann zu einem Ausdruck der Form ausgeschrieben würde, der aber mit unseren Mitteln nicht mehr aufgeschrieben werden kann.
Der wirklich faszinierende Aspekt der Ackermann-Funktion ist, dass die einzige Berechnung, die neben den rekursiven Aufrufen tatsächlich auftaucht, die Berechnung von ist, die einfach um 1 erhöht.
Sonstiges
Funktionswert a(4,2)
Es sei noch festgehalten, dass der Funktionswert a(4,2), der in Form einer 19727-stelligen Dezimalzahl auf verschiedenen Internetseiten auftaucht, wahrscheinlich nicht mit der rekursiven Definition der Ackermannfunktion praktisch berechnet werden kann, weil dafür bei weitem zu viel Speicherplatz für die Zwischenergebnisse benötigt würde.
Die Busy-Beaver-Funktion
1962 gab Tibor Radó mit der Busy-Beaver-Funktion (fleißiger Biber) eine noch stärker wachsende Funktion als die Ackermannfunktion an, die allerdings nicht mehr berechenbar ist.
Literatur
- Wilhelm Ackermann: Zum Hilbertschen Aufbau der reellen Zahlen, Math. Annalen 99 (1928), 118-133
- Yngre Sundblad: The Ackermann Function. A Theoretical, Computational, and Formula Manipulative Study. BIT 11 (1971), 107-119
- Uwe Schöning: Theoretische Informatik – kurzgefasst; Spektrum Akademischer Verlag, ISBN 3-8274-1099-1
- Dexter C. Kozen: The Design and Analysis of Algorithms; Springer 1992
Weblinks
- Einige Werte der Ackermannfunktion (Die Angaben für die Anzahl der Iterationen sind allerdings Unsinn)
- Beispielanwendung der Ackermannfunktion als Benchmark. Beachten Sie die große Zahl der Funktionsaufrufe schon bei der Berechnung kleiner Werte (Englisch)
- Dezimaler Wert von ack(4,2)