Discover
Theoretische Informatik

27 Episodes
Reverse
Im vorigen Jahrhundert hatte die Parallelverarbeitung bereits große
praktische Bedeutung~-- aber eben nur
im Bereich der doch eher seltenen Supercomputer, die ihrerseits in
der Regel nur für sehr spezielle numerische Probleme eingesetzt wurden. Es
ergab damals keinen Sinn, Feld-Wald-und-Wiesen-Software für
Parallelrechner fit zu machen -- es gab einfach keine auf den
Schreibtischen der Kunden. Umgekehrt versuchten
Hardware-Hersteller auch nicht, mehrere Prozessoren in die Computer
einzubauen, da diese von Standardsoftware nicht genutzt wurde. Ein
klassisches Henne-Ei-Problem lag vor und die Parallelverarbeitung
verfiel in einen etwas unruhigen Dornröschenschlaf.
Aus diesem Schlaf ist sie irgendwann kurz nach der Jahrtausendwende
wachgeküsst worden -- allerdings nicht von einem Prinzen, sondern von
Multi-Core-Prozessoren. Heute kann man beim Entwurf auch eines
einfachen Standardprogramms davon ausgehen, dass in der Regel
mehrere Rechenkern zur Verfügung stehen werden. Damit drängelt sich
sowohl in der Praxis wie auch in der Theorie ein Problem in den
Vordergrund, das bis jetzt doch eher im Abseits gestanden hat: Wie
gut lassen sich Standardprobleme eigentlich parallelisieren? Mit
anderen Worten: Wenn ich 16 Kerne zur Verfügung habe, kann ich das
Problem dann auch 16 Mal so schnell lösen?
Die Frage, welche Probleme wie gut parallelisierbar sind, wird in
diesem Kapitel nicht beantwortet werden -- schließlich gibt es eine
eigene Vorlesung »Parallelverarbeitung« in der es um nichts anderes
geht. Jedoch können wir jetzt schon festhalten, dass man parallele
Programme immer in sequentielle Programme umwandeln kann, indem
parallelen Anweisungen einfach nacheinander ausgeführt
werden. Deshalb können parallele Programme auf einem Rechner mit
$p$ Kernen höchsten um den Faktor $p$ schneller sein als
sequentielle Programme. In der Praxis ist $p$ eine kleine Zahl,
irgendetwas zwischen $2$ und vielleicht $100$ -- ein Parallelrechner
mit einer Million Kernen ist doch eher die Ausnahme. Dies bedeutet
aber, dass man mit Parallelrechnern »auch nur« Probleme in $\Class
P$ effizient wird lösen können. Der Geschwindigkeitsvorteil, den eine
Parallelisierung bringt, wird bei einem Exponentialzeit-Problem
schon durch eine kleine Erhöhung der Eingabegröße sofort zu Nichte
gemacht.
Die Frage, welche Probleme gut parallelisierbar sind, ist also die
Frage, welche Problem in~$\Class P$ gut parallelisierbar
sind. Wenn man etwas darüber nachdenkt, so stellt man schnell fest,
dass fast alle Probleme, die einem so spontan einfallen, irgendwie
ganz gut parallelisierbar scheinen. Da darf die Frage erlaubt sein,
ob nicht vielleicht einfach alle Probleme in~$\Class P$ sich
auf einem Parallelrechner viel schneller lösen lassen?
Die Beantwortung einer solchen Frage scheint wieder nicht ganz
einfach, da es ja doch recht viele und noch dazu sehr
unterschiedliche Probleme in der Klasse~$\Class P$ gibt.
An dieser Stelle kommt nun der Titel dieses Kapitels ins Spiel:
Statt uns mit unendlich vielen komischen Problemen herumzuschlagen,
könnten wir uns auf die Parallelisierbarkeit nur eines
Problems konzentrieren, wenn wir ein vollständiges Problem
für~$\Class P$ finden. Ein solches Problem gibt es: Das
Circuit-Value-Problem, welches im Folgenden eingehend
vorgestellt wird.
Was weiß man nun über die Parallelisierbarkeit des Circuit-Value-Problems?
Trotz jahrelanger Forschung ist es leider weder gelungen zu zeigen,
dass sich dieses Problem gut parallelisieren lässt (und damit auch
alle anderen Problem in $\Class P$ aufgrund der Vollständigkeit),
noch dass es sich nicht gut
parallelisieren lässt. Nach der $\Class
P$-$\Class{NP}$-Frage, der ja das gesamte Kapitel~\ref{lecture-p-np}
gewidmet ist, ist die Frage, wie gut sich das Circuit-Value-Problem
parallelisieren lässt, wohl die zweitwichtigste Frage der
Theoretischen Informatik.
Ist es moralisch verwerflich, zu betrügen? Betrug wird im Strafrecht
als ein doch eher schweres Delikt angesehen und gegebenenfalls hart
bestraft. Mensch sollten nicht betrügen. Bei Maschinen ist
die Sachlage komplexer: Es ist nicht unmoralisch, wenn Maschinen
sich ein wenig »helfen« lassen -- es führt nur zu
Berechnungsmodellen, die uns in der Praxis nicht viel nützen. Jeder
möge nun für sich selbst entscheiden, was schlimmer ist (dies ist
natürlich eine moralische Frage).
Wie können Maschinen nun schummeln? Ein besonders raffiniertes
Verfahren ist die Benutzung von »nichtdeterministischen
Hinweisen«. Die Situation ist ähnlich wie bei einem Studenten, der eine
Klausur schreibt und dem kleine Zettelchen zugeflogen kommen, auf
denen Dinge stehen wie »addiere erst die dritte und vierte Zahl«
oder »schaue auf \label{this-page}Seite~\pageref{this-page} im
Skript nach, da steht die Lösung«. Das Problem ist nur, dass diese
Hinweise nicht sonderlich verlässlich sind: Es kann sein (nichts
genaues weiß man nicht), dass der ominöse Zettelproduzent
dummerweise eine andere Klausur schreibt. Dann laufen die
Hinweise natürlich ins Leere. In diesem Fall muss die schummelnde
Maschine in der Lage sein zu erkennen, dass hier etwas nicht mit
rechten Dingen zugeht: Formal soll sie (schnell) die korrekte
Klausurantwort produzieren bei geeigneten Hinweisen; hingegen
soll sie (schnell) verwerfen, wenn die Hinweise sie nicht zur
korrekten Lösung führen werden.
Solche nichtdeterministischen Hinweis sind etwas zwielichtig, schwer
zu definieren und schwer zu motivieren. Trotzdem benötigen wir sie,
um die wohl wichtigste und bestuntersuchte Klasse der
Komplexitätstheorie zu definieren: $\Class{NP}$. Von dieser Klasse
haben selbst Nichtinformatiker oft schon im Rahmen des $\Class
P$-$\Class{NP}$-Problems gehört. Bei diesem Problem geht es
letztendlich um die Frage, ob man jede Berechnung mit Schummelei in
eine genauso effiziente Berechnung ohne Schummeln umwandeln kann.
Lug und Betrug bei Berechnungen kann man übrigens auch noch doller
treiben (was wir in dieser Vorlesung aber nicht machen werden, dies
ist dem Master-Studium vorbehalten, wo Sie auch moralisch
hoffentlich schon gefestigter sind): Statt nur rätselhafte und
in der Regel falsche Hinweise zu bekommen für eine Berechnung, kann
man auch gleich ein mächtiges Orakel fragen. Solche Orakel
kann die Maschine beliebig oft befragen, ob verschiedene Worte
Element einer bestimmten Sprache sind -- und das beste ist, dass die
Frage nichts kosten. Offenbar werden viele Problem sehr
einfach zu beantworten, wann man ein mächtiges Orakel befragen darf:
So ist das Problem $\Lang{sat}$ nun wirklich leicht zu entscheiden,
wenn man ein »$\Lang{sat}$-Orakel« befragen darf. Wenn man aber
etwas darüber nachdenkt, so stellt man schnell fest, dass bestimmte
Probleme selbst dann noch schwer sind, wenn man ein Orakel
befragen darf. So ist es ein wichtiges offenes Problem, ob
man beispielsweise einen effizienten Algorithmus für das Brettspiel
»Go« programmieren kann, wenn man ein $\Lang{sat}$-Orakel zur
Verfügung hat.
\includemovie[label=song]{0pt}{0pt}{\bodydir/media/Longest-Path-pd-Dan-Barrett-compressed.mp3}%
\begin{verse}
\centerline{\itshape Find the Longest Path}
\smallskip
\centerline{by Dan Barrett}
\smallskip
\centerline{(Original The Longest Time} by Billy Joel)
\bigskip
\bigskip
\bigskip
\small%
\begin{minipage}[t]{.5\linewidth}\parskip1em
Oh, oh, oh, oh,\\
find the longest path.\\
Oh, oh, oh,\\
find the longest path.
If you said P is NP tonight,\\
there would still be papers left to write.\\
I have a weakness:\\
I'm addicted to completeness\\
and I keep searching for the longest path.
The algorithm I would like to see\\
is of polynomial degree.\\
But it's elusive,\\
nobody has found conclusive\\
evidence that we can find the longest path.
I have been hard\\
working for so long.\\
I swear it's right,\\
and he marks it wrong.\\
Somehow I'll feel sorry when it's done:\\
gpa 2.1\\
is more than I hope for.
\medskip
\end{minipage}\hskip5mm\begin{minipage}[t]{.5\linewidth-5mm}\parskip1em
Garey, Johnson, Karp and other men\\\hbox{}\hfill (and women)\\
try to make it order $n \log n$.\\
Am I a math fool\\
if I spend my life in grad school\\
forever following the longest path?
Oh, oh, oh, oh,\\
find the longest path.\\
Oh, oh, oh,\\
find the longest path.\\
Oh, oh, oh, oh,\\
find the longest path\dots
\vskip1cm
\hfill \movieref[play]{song}{\strutAnhören}\ \ |\ \ \movieref[stop]{song}{\strutStopp}
\end{minipage}%
\end{verse}
Die Untersuchung von kleinen Platzklassen erscheint auf den ersten
Blick reichlich »akademisch«, »mainly of scholary interest«, kurz
gesagt: überflüssig. Speicherplatz ist (im Gegensatz zu Zeit) in
aller Regel wahrlich nicht unser
Problem. Wenn eine Ingenieurin ein Gigabyte an Eingabedaten hat, dann
wird sie sicherlich auch irgendwo noch Platz für ein Gigabyte
weiteren Speicher finden. Man könnte einwenden, dass kleine Systeme
wie Smartcards oder eingebettete Systeme wie
Waschmaschinensteuerungen nicht mit übermäßigem Speicherplatz
gesegnet seien und es sich sehr wohl lohne, sparsam damit
umzugehen.
Wohl wahr -- jedoch erscheint es schon von
dagobertduckhaftem Geiz zu zeugen, nur logarithmisch viel
Platz vorzusehen.
Hat man beispielsweise ein Gigabyte an Eingabedaten, so dürfte man bei
einem erlaubten Platz von $\log_2 n$ Bits gerademal 33 Bits an
Speicher verbrauchen, also großzügig gerundet zwei
32-Bit-Register. Wächst die Eingabemenge auf 1000 Terabyte, so
hätte man immerhin 53 Bits zur Verfügung, also immernoch nicht mehr
als zwei Register. Bei der größten in diesem Universum überhaupt denkbaren
Eingabe ($10^{80}$, denn so viele Atome hat das Universum,
plus/minus ein paar Zehnerpotenzen), hätte man immerhin zehn
32-Bit-Register zur Verfügung.
In diesem Kapitel soll nun logarithmischer Platz noch mit
Nichtdeterminismus gepaart werden, was uns zur Klasse $\Class{NL}$
führen wird. Diese Paarung mag Theoretiker beglücken, dem Praktiker
stehen aber zunächst die Haare zu Berge: Da wird ein unrealistisches
Modell (lächerlich wenig Platz) mit einem noch viel
unrealistischeren Modell (nichtdeterministische Turing-Maschinen)
zusammengebracht; das Resultat sind Maschinen, die man nicht bauen kann,
und wenn man sie bauen könnte, sinnlosen Einschränkungen
unterliegen.
Warum lohnt es sich trotzdem, nichtdeterministischen logarithmischen
Platz zu untersuchen? Ich möchte folgende Gründe anführen:
\begin{enumerate}
\item
Erstaunlicherweise sind nichtdeterministischer logarithmischer
Platz und die Parallelisierbarkeit von Problemen aufs Engste
verwoben; die Details hierzu übersteigen zwar den Rahmen dieser
Vorlesung, sie werden aber in den Vorlesungen
»Parallelverarbeitung« und »Komplexitätstheorie« genauer
untersucht.
\item
Das »Wesen des Nichtdeterminismus« zu verstehen hat sich zu einer
der Hauptaufgaben der Theoretischen Informatik gemausert. Wir
haben in den Kapiteln \ref{lecture-nfa} und~\ref{lecture-regex}
schon gesehen, dass Nichtdeterminismus zumindest innerhalb der
Theorie ein sehr nützliches Hilfsmittel darstellt.
Es hat sich gezeigt, dass Nichtdeterminismus bei Platzklassen
einfacher zu verstehen und zu untersuchen ist, als dies bei
Zeitklassen der Fall ist (die uns ja eigentlich mehr
interessieren).
Ein Beispiel für den Erfolg dieser theoretischen Untersuchungen
ist eine Konsequenz des Satzes von Immerman--Szelepcsényi: Die
Klasse der kontextsensitiven Sprachen (siehe
Definition~\ref{def-csl) ist abgeschlossen unter
Komplementbildung.} Diesem Resultat sieht man wahrlich nicht an,
dass hier viel Theorie über nichtdeterministischen logarithmischen
Platz drinsteckt.
\item
Die Klasse $\Class{NL}$ wird die erste natürliche Klasse sein
(zur Frage, wie »natürlich« diese Klasse wirklich ist, siehe
oben), für die wir ein (nun wirklich) natürliches
vollständiges Problem angeben können: Das
Erreichbarkeitsproblem $\Lang{reach}$ (auch $\Lang{gap}$ genannt)
ist vollständig für~$\Class{NL}$. Der Beweis ist hier relativ
einfach und soll als »Aufwärmübung« für die vertrackteren Beweise
in den folgenden Kapiteln dienen.
\end{enumerate}
\includegraphicscopyrightborderautowidth{\bodydir/media/Zutaten_caipirinha-ccas-Christian_Horvat.jpg}{Autor
Chrisitan Horvat, Creative Commons Attribution Sharealike Lizenz}%
Das letzte Kapitel hat Ihnen Reduktion hoffentlich bereits
schmackhaft gemacht.
Manche Probleme haben nun die Eigenschaft, dass ziemlich viele andere
Probleme auf sie reduziert werden können. Wenn Sie eine Caipirinha
gemixt bekommen, dann werden Sie auch keine Probleme haben mit einer
Caipiroschka, einer Caipirissima oder zur Not halt einer
Virgin-Caipi. Man kann guten Gewissens behaupten: Sie werden in der
ganzen Sippe der Caipi-Cocktails keinen wirklich »schwierigeren«
Cocktail finden. Der Theoretiker würde nun sagen, dass das Problem der
Caipirinha-Herstellung vollständig ist für die Klasse aller
Caipi-Cocktails (vorausgesetzt, der Theoretiker kann nach der
eingehenden Untersuchung der verschiedenen Cocktails noch
verständlich kommunizieren).
In diesem Kapitel geht es um die Frage, wo man in der
Theorie außer bei alkoholischen Getränken noch überall vollständige
Probleme antreffen kann. Sprich: Gibt es in bestimmten
Komplexitätsklassen vielleicht Probleme, die »unglaublich
hilfreich« sind? Gibt es Probleme, so dass man alle Probleme in der
Klasse lösen kann, wenn man nur diese Problem lösen kann? Gibt es
also Probleme, so dass sich alle Probleme einer Klasse auf dieses
eine Problem reduzieren lassen?
Die Intuition sagt einem zunächst, dass es solche Probleme nicht
geben sollte. Nehmen wir die doch recht große Klasse $\Class
P$. Diese versammelt ja alle Probleme, die sich in Zeit $O(n)$ oder
in $O(n^2)$ oder in $O(n^3)$ oder in $O(n^4)$ oder so weiter lösen
lassen. Man darf deshalb erwarten, dass es zu jedem Problem in
$\Class P$ »immer ein noch schwierigeres« Problem in $\Class P$ geben
sollte.
Die Intuition ist jedoch falsch: Es gibt ein Problem
in~$\Class P$, auf das sich alle anderen Probleme in~$\Class P$
reduzieren lassen. Kann man also dieses Problem so-und-so gut lösen,
so kann man auch alle anderen Probleme in $\Class P$ prinzipiell
auch so-und-so gut lösen. Dieses Problem wird in
Kapitel~\ref{lecture-cvp} detaillierter vorgestellt.
Die Klasse $\Class P$ ist nicht die einzige Klasse, die ein
solches »besonders hilfreiches« Problem enthält. Es hat sich
herausgestellt, dass die meisten Komplexitätsklassen solche
»vollständigen« Probleme besitzen.
In diesem Kapitel werden zunächst die Idee der Vollständigkeit
veranschaulicht, bevor die nächsten Kapitel dann für verschiedene
Klassen vollständige Probleme vorstellen werden.
Falls Sie jemals vorhaben, die Menschheit eines Tages durch
Ihren Nachwuchs zu beglücken, so möchte ich Ihnen dringend davon
abraten, Komplexitätstheoretiker nach Namensvorschlägen für Ihr Baby
zu befragen. Die Namen, die sich Komplexitätstheoretiker ausdenken,
sind -- höflich formuliert -- extrem verwirrlich. Im letzten Kapitel
wurden ja schon einige Klassen eingeführt, deren Namen typisch sind:
Warum heißt logarithmischer Platz $\Class L$, polynomieller Platz
hingegen $\Class{PSPACE}$? Wieso nennt man Probleme, die sich
praktisch gar nicht und selbst theoretisch nur mit exorbitantem
Aufwand lösen lassen, »elementar«? Und überhaupt, warum nennt man
Probleme eigentlich »Sprachen«? Gar nicht erwähnt habe ich
vorsichtshalber, dass es neben der Klasse $\Class{EXP}$ auch die
Klasse $\Class E$ gibt, die fast genauso definiert ist -- nur eben ein
ganz klein bisschen anders. Perfektioniert wird das Chaos dadurch,
dass sich die Namen mit den Jahren auch gerne verlängern oder
verkürzen, je nachdem was gerade so in Theoretikerkreisen als
modisch und besonders hipp gilt. Aus dem Mauerblümchen
$\Class{PTIME}$ wurde so das trendy $\Class P$, die fette
Klasse $\Class{EXPTIME}$ wurde zu $\Class E$ abgemagert -- logisch
wäre zwar $\Class{EXP}$ gewesen, aber das war ja schon anderweitig
vergeben.
»Was soll's?«, mag man einwenden, andere Disziplinen wie die
Biologie oder die Medizin sind auch voll von unsinnigen Namen und
Abkürzungen. Sind aber die meisten Klassennamen einfach nur
verwirrlich, so sind die drei Bezeichungen
»Reduktion«, »Schwere« und »Vollständigkeit« für drei zentrale Ideen
der Theorie katastrophale Fehlentscheidungen gewesen: Sie verwirren
Anfänger oft dermaßen, dass sie noch während des Vorspiels die Lust an
der Materie verlieren. Ein Reduktion macht ein Problem weder kleiner
noch einfacher; ein schweres Problem ist nicht etwa schwer, sondern
oft sehr leicht
zu lösen; ein vollständiges Problem ist nicht das Gegenteil eines
Teilproblems. Leider ist es zu spät, die unsinnigen Namen zu ändern,
sie haben sich festgesetzt.
Wie würden bessere Bezeichungen lauten? Statt von einer »Reduktion
zwischen Problemen« sollte man lieber von einem »Vergleich von
Problemen« sprechen, denn das macht eine Reduktion in
Wirklichkeit: Kann man $A$ auf $B$ reduzieren, dann bedeutet
dies eigentlich nur »die Komplexität von $A$ ist höchstens so groß
wie die von~$B$«. Statt »schwer für eine Klasse« sollte man eher
sprechen von »hilfreich für eine Klasse«: Wenn $A$ schwer ist für
eine Klasse, dann kann man alle Probleme in der Klasse lösen, wenn
man nur $A$ lösen kann. Schließlich sollte »vollständiges Problem
für eine Klasse« am besten in »hilfreiches Problem in einer Klasse«
umbenannt werden: Der einzige Unterschied zwischen schweren und
vollständigen Problemen ist nämlich, dass letztere eben in
der Klasse liegen müssen, erstere hingegen nicht unbedingt.
In diesem Kapitel werden wir zunächst Reduktionen genauer
untersuchen, in darauffolgenden Kapitel geht es dann mit schweren
und vollständigen Problemen weiter.
Bleibt die Frage, wen Sie um Rat fragen sollte bezüglich der
Benennung Ihrer Kinder. Fragen Sie doch einen Mathematiker!
Menschen, die »durch Distributivgesetze miteinander verschränkte
Operationen mit zugrundeliegenden Gruppenstrukturen« schlicht »Ringe«
nennen, scheinen mehr Gespür für die Schönheit von Worten zu
haben. Und wenn dem Ring noch einige weitere Axiome wie
Kommutativität der Gruppenstruktur oder Nullteilerfreiheit
hinzugefügt werden, so nennen Mathematiker dies nicht etwa »durch
Distributivgesetze miteinander verschränkte Operationen mit
zugrundeliegenden kommutativen Gruppenstrukturen und
Nullteilerfreiheit«, sondern etwas verträumt »Körper«.
Herr Winston Wolf, der Ihnen vielleicht noch aus »Logik für
Informatiker« bekannte Leiter des Amts für Problemlösungen, bekommt
an einem Montag morgens ein neues Problem auf seinen Aktenstapel:
Die Regierung bittet darum, das Problem mit den ungenauen
Klimamodellen doch bitte zu lösen -- man hätte gerne verlässlichere
Aussagen über die Entwicklung des Weltklimas in den nächsten hundert
Jahren.
Herr Wolf ruft daraufhin beim Amt für Klimaforschung an, wo
man ihm sinngemäß Folgendes mittelt: »Unsere Prognosen werden um so
besser, je höher die räumliche Auflösung der Simulationen ist. Dazu
brauchen wir mehr Rechenpower.«
In der Informatikabteilung seines Amtes arbeiten drei
Informatikerinnen -- eine ist zuständig für Technische Informatik,
eine für Praktische Informatik und eine für Theoretische
Informatik~--, die Herrn Wolf nun befragt, wie er für mehr
Rechenpower sorgen könne.
Dr.\ Naehle, die Referentin für Technische Informatik, antwortet:
»Die Sache ist ganz klar: Wir benötigen jede Menge
fpgas, die wir mit Klimasimulationsschaltungen flashen. Die
schalten wir dann zusammen und bauen damit eine
riesige dedizierte Klimasimulationsmaschine.« Dr.\ Fischerin,
zuständig für die Praktische Informatiker, meint: »Das geht viel
billiger und einfacher mit Standardkomponenten. Die könnten wir bei
Ebay besorgen (wenn die Beschaffungsstelle mitspielt). Dann brauchen
wir noch ein paar flotte Netzwerkswitches und mein geliebtes
obscure-bsd auf allen Rechnern. Das ergibt dann einen
schönen Supercomputer.« Dr.\ Meischnuck, die Theoretikerin, hat den
zweifelsohne billigsten Vorschlag: »Wir schauen erstmal, was
überhaupt das Problem ist. Bei diesen Klimasimulationen geht es doch
meist um Differentialgleichungen. Die bestehenden
Simulationsprogramm benutzen vielleicht noch stures Iterieren zur
Lösung. Da greifen wir doch mal in die algorithmische Trickkiste, da
findet sich sicherlich etwas Schnelleres.«
Wenn Herr Wolf alle drei gewähren lassen würde, wer würde am Ende
am schnellsten die gewünschten Simulationsergebnisse berechnen?
In diesem Kapitel geht es um darum, Werkzeuge zu entwickeln, um
solche Fragen sinnvoll zu beantworten. Es geht um eine »Theorie der
Geschwindigkeit von Algorithmen«. Ziele dieser Theorie sind:
\begin{enumerate}
\item Sie soll möglichst unabhängig von konkreter Hardware
sein. Dass ein Supercomputer Zahlen schneller sortiert als Ihr
Telefon, ist wenig überraschend und sagt auch wenig darüber aus, wie
schnell ein bestimmter Algorithmus ist.
\item Sie soll ein allgemeines Bild von der Geschwindigkeit eines
Algorithmus liefern. Dass ein Algorithmus auf bestimmten
Spezialfällen sehr gut funktioniert, ist zwar schön für den
Algorithmus und man kann ihn dafür auch belobigen, jedoch sagt
dies wiederum wenig über sein allgemeines Verhalten aus.
\end{enumerate}
Es hat sich herausgestellt, dass die $O$-Notation
besonders geeignet ist, die Geschwindigkeit von Algorithmus zu
beschreiben. Grob gesprochen bedeutet »der Algorithmus hat Laufzeit
$O(n^2)$«, dass er bei Eingaben der Größe $n$ grob
$n^2$ Rechenschritte benötigt unabhängig von der Hardware.
Welche der Informatikerinnen am Ende die besten Ergebnisse präsentieren kann,
ist übrigens alles andere als klar: Wenn die Theoretikerin
tatsächlich einen Algorithmus findet in einer kleineren $O$-Klasse,
so wird sie damit die Superrechner der anderen beiden locker um
Größenordnungen schlagen. Andererseits kann es aber bei Theoretikern
leicht passieren, dass sie auch nach Jahren noch in ihrem Büro über
einen neuen Algorithmus brüten, während die Computer der Techniker
und Praktiker das Problem schon längst zur allgemeinen Zufriedenheit
gelöst haben.
Beginnen wir die Komplexitätstheorie doch mit einer Fabel
für kleine Informatiker:
\includegraphicscopyrightborder[width=2.25cm]{\bodydir/media/tux-pd-larry_ewing-simon_budig-anja_gerwinski.jpg}{Authors
Larry Ewing, Simon Budig, Anja Gerwinski, public domain}%
\includegraphicscopyrightborder[width=2.25cm]{\bodydir/media/Puffy-cca-unknown.jpg}{Unknown
author, Creative Commons Attribution Lizenz}%
\includegraphicscopyrightborder[width=2.25cm]{\bodydir/media/Konqi-lgpl.jpg}{Unknown
author, Lesser gnu Public Licence}%
\includegraphicscopyrightborder[width=2.5cm]{\bodydir/media/beaver-pd-pearson-scott-foresman.jpg}{Pearson
Foresman, public domain}%
\begin{quotation}
Eines Tages, als Tux der Pinguin gerade auf dem Weg von seiner
Lieblingseisscholle nach Hause war mit einem leckeren Hering unter
der Flosse, traf er unterwegs zwei seiner Freunde:
Puffy den Kugelfisch und Konqi den Drachen. »Hallo,« sagte Tux, »wie
geht es euren Usern?« »Gut,« antworteten die beiden, »aber unsere
User haben uns jedem ein Problem gegeben, bei dem du uns bitte
helfen musst.«
Die beiden reichten Tux ein Blatt Papier, das ganz vollgeschrieben
war mit Zahlen wie $19$, $0$, $50$, $42$, $123$,
$78$ und noch viele mehr. Puffy sagte nun: »Tux, ich soll
möglichst wenige dieser Zahlen auswählen, so dass die Summe mindestens
$1000$ ergibt.« Tux dachte kurz nach und antwortete: »Na, das ist
doch ganz einfach: Du sortierst die Zahlen und nimmst dann so lange
die größten, bis du 1000 zusammenhast.«
Nun zeigte Konqi sein Blatt und sagte: »Tux, ich soll auch möglichst
wenige dieser Zahlen auswählen, aber die Summe soll genau
$1000$ ergeben.« Tux rieb sich mit seiner Flosse den Kopf, dachte
etwas nach und meinte dann: »Das scheint mir schwieriger zu sein. Da
fragen wir doch mal den Biber.«
So gingen sie zum Biber, der sich das Problem von Konqi
anschaute und dann verkündete: »Das ist ein Problem nach meinem Geschmack!
Mit etwas Fleiß kann man doch einfach alle Möglichkeiten
durchprobieren, wie die Zahlen auszusuchen sind. Und mit Fleiß kenne
ich mich ziemlich gut aus.« So machte sich der Biber daran, alle
Möglichkeiten durchzuprobieren.
Und wenn er nicht gestorben ist, so probiert er noch heute.
\end{quotation}
Die Moral hiervon ist (jede Fabel hat ja eine Moral): Niemand
weiß, wie man Konqis Problem effizient löst. Die Probleme von
Puffy und Konqi sehen fast identisch aus, haben aber --
nach allem was man weiß -- sehr unterschiedliche Komplexität.
In diesem Kapitel wird damit begonnen, eine Theorie des »Messens von
Komplexität« zu entwickeln. Das wesentliche Ziel dieser Theorie ist
zu verstehen, was Puffys Problem einfach macht, Konqis
hingegen schwierig.
Das Halteproblem ist eigentlich ganz einfach: Es ist die Frage, ob
ein in Form seines Programmtextes gegebenes Computerprogramm bei
einer bestimmten Eingabe anhält oder eben nicht. Diese Frage ist
sicherlich vom praktischen Standpunkt aus nicht ganz unwichtig -- es
wäre doch ausgesprochen schön, wenn man nur noch Programme
ausliefern würde, die auf typischen Eingaben immer anhalten.
In diesem Kapitel wird gezeigt werden, dass das Halteproblem nicht
entscheidbar ist. Es kann also gar keinen Algorithmus geben,
der beliebige andere Programme analysiert und uns dann immer korrekt
sagt, ob diese anhalten oder nicht. Man beachte allerdings, dass man
sehr wohl für einzelne konkrete Programm beweisen kann, dass sie bei
bestimmten Eingaben anhalten. Man kann dies sogar in Teilen
automatisieren -- nur eben nicht völlig allgemein.
Die Unentscheidbarkeit des Halteproblems liegt nicht etwa daran,
dass man in der Praxis nicht genügend Rechenzeit oder Speicherplatz
hat, um das Problem zu lösen. Es geht prinzipiell nicht, egal
wie viel Zeit Sie Ihrem Programm genehmigen.
Der Beweis der Unentscheidbarkeit funktioniert in etwa wie folgt:
Zunächst beweist man, dass eine bestimmte, reichlich künstlich
wirkende Sprache unentscheidbar ist. Dann wird diese Sprache
»umgeformt« zu immer neuen Sprachen, die dem Halteproblem immer
»ähnlicher« werden. Dabei zeigen wir, dass über alle Umformungen
hinweg die entstehenden Sprachen immer unentscheidbar bleiben.
Bei diesem »Umformen von Problemen« bekommt man leicht einen Knoten
im Gehirn. Die Argumente gehen nämlich so: Um zu zeigen, dass ein
Problem unentscheidbar ist, nimmt man an, es gebe doch einen
Entscheider für das Problem. Diesen benutzt man, um einen neuen
Entscheider für ein anderes Problem zu bauen, von dem man schon
weiß, dass es keinen Entscheider für das Problem gibt. Man baut also
mit viel Liebe etwas, von dem man schon weiß, dass es dieses Ding
gar nicht gibt. Bei diesen Argumenten kann es schnell passieren,
dass man gar nicht mehr weiß, was man eigentlich weiß. In diesem
Fall hilft es, sich mit Sokrates' Ausspruch »Ich weiß, dass ich
nichts weiß« zu trösten.
Dieses Kapitel beinhaltet eine gute und eine schlechte
Nachricht. Zunächst die schlechte Nachricht: Alle semantischen
Eigenschaften von Programmen sind unentscheidbar. Jetzt die gute:
Jedes Programm kann auf seinen eigenen Programmtext
zugreifen.
Im Laufe dieses Kapitels soll es hauptsächlich darum gehen, diese
beiden Nachrichten zunächst überhaupt verständlich zu machen, sie
dann mathematisch etwas exakter zu fassen, ihnen schöne Namen zu
geben und sie schließlich auch zu beweisen.
In den letzten Kapiteln wurden eine ganze Reihe doch sehr
heterogener Konzepte eingeführt: nichtdeterministische
Turingmaschinen, Register-Maschinen, minimalistische imperative
Programmiersprachen und funktionale Programmiersprachen. Ob Sie diese
Konzepte spannend finden, müssen Sie selber wissen -- gähnend
langweilig waren auf jeden Fall die Sätze, die wir über Mächtigkeit
dieser Modelle bewiesen haben: Alle Kapitel endeten mit dem Satz »Mit dem
neuen Modell können wir genau so viel berechnen wie mit
deterministischen Turing-Maschinen.« (Natürlich schreibt diesen Satz
mathematisch etwas hübscher und unverständlicher auf, die Aussage
ist aber genau diese.)
Liegt diese Ohnmacht der Modelle an der raffinierten, ja gar
perfiden Auswahl der Maschinenmodell durch den Autor? Hat er
vielleicht die logischen Programmiersprachen genau deshalb mit
keiner Silbe erwähnt, weil bei diese viel mehr zu holen ist? Maschinenmodelle aus
prähistorischer Zeit (nach Informatiker-Zeitrechnung, also älter als
fünf Jahre) werden ausführlich behandelt, moderne Quanten-Computern
hingegen wie zufällig unter den Teppich gekehrt -- was hat der Autor
zu verschweigen? Wieso werden dna-Computer mit Nichtachtung
gestraft?
Wie bei den meisten Verschwörungstheorien ist die Wahrheit eher
profan: Man könnte die Liste der Modelle und Programmiersprachen
noch über viele, viele weitere Vorlesungen verlängern -- man würde kein
neues Maschinen- oder Programmiermodell finden, das mächtiger ist
als die Turing-Maschine. Glauben Sie mir, viele Leute haben das
versucht.
Erinnern wir uns daran, wie Turing seine Turing-Maschine eingeführt
hat: Ausgehend von der Beobachtung, was ein Mensch
prinzipiell überhaupt jemals berechnen könnte, hat er ein
Modell definiert, das genau diese von Menschen durchführbaren
Berechnungen nachvollzieht. Folglich müsste ein Maschinenmodell, das
mehr kann als die Turing-Maschine, auch mehr können, als Turings
idealisierter Mathematiker.
Die Church-Turing-These besagt deshalb, dass einfach alles, was
man »intuitiv« berechnen kann, auch von einer Turingmaschine
berechnet werden kann.
Wie beweist man die Church-Turing-These? Gar nicht. Die These ist
kein mathematischer Satz, sondern eine philosophische Aussage. Das
Problem ist, dass der Begriff »intuitiv berechenbar« mathematisch
nicht genau definiert ist -- und definiert man diesen, so landet man
eben genau bei einem Modell wie der Turing-Maschine. Dann würde man
also nur beweisen, dass Turing-Maschinen genau das können, was
Turing-Maschinen können. Das erinnert stark an den Slogan »Was
Friseure können, können nur Friseure« der Friseurinnung und ist
auch in etwa von der gleichen intellektuellen Nahrhaftigkeit.
Stimmt die Church-Turing-These? Es scheint so. Andererseits könnte
man sich durchaus vorstellen, dass neuartige Computer
wirklich mehr können als Turing-Maschinen. Beispielsweise
könnte man sich zunächst berechtigte Hoffnungen machen, dass
Quanten-Computer Berechnungen anstellen können, an die Turing nicht
gedacht hat. Diese Hoffnung zerschlägt sich jedoch schnell --
Quantenrechner können auch nicht mehr als der pc von der
Stange; sie sind nur schneller.
Trotzdem bleibt die Church-Turing-These nur eine These. So könnten
in exotischen Umgebungen (wie in der Nähe eines Schwarzen Loches)
durchaus Berechnungen möglich sein, an denen eine Turing-Maschine
scheitert.
Vielleicht haben Sie sich schon gewundert, weshalb in einer
Vorlesung zur \alert{Theoretischen} Informatik ausgerechnet den
Hardware-Modellen in den letzten Kapiteln so ein breiter Raum
gegeben wurde. (Andererseits -- vielleicht wundert Sie bei dieser
Vorlesung gar nichts mehr.) Die \alert{Software}, die
schließlich bei der täglichen Arbeit der meisten Informatikerinnen und
Informatiker von zentraler Bedeutung ist, wurde bis jetzt weitgehend außen
vor gelassen mit Ausnahme einiger etwas konstruiert wirkender
Übungsaufgaben.
\includegraphicscopyrightborderautowidth{\bodydir/media/space-shuttle-ccas-.jpg}{Unknown
autor, Creative Commons Attribution ShareAlike Lizenz}%
\includegraphicscopyrightborderautowidth{\bodydir/media/piston-pd-Pearson-Scott-Foresman.png}{Autor
Pearson Scott Foresman, public domain}%
Es ist also höchste Zeit, sich der Software aus theoretischer
Sicht anzunehmen. Dazu werden wir in diesem und dem nächsten Kapitel
verschiedene Programmiersprachen betrachten, einmal aus dem Bereich
der strukturierten-imperativen Programmierung und einmal aus dem
Bereich der funktionalen Programmierung. Die logische und die
objektorientierte Programmierung (und Hunde) müssen leider draußen
bleiben -- hier sei auf die speziellen Vorlesungen wie
»Logikprogrammierung« verwiesen.
Die im Folgenden untersuchten Programmiersprachen sind natürlich
einfach gehalten. Will man erstmal prinzipiell verstehen, was
verschiedene Programmiersprachen können oder nicht können, so sollte
man nicht gleich mit Java in seiner ganzen api-Pracht
beginnen. Eine Vorlesung über Maschinenbau beginnt ja auch nicht mit
der Funktionsweise eines Space-Shuttles sonder lieber mit der eines Kolbens.
In diesem Kapitel werden zwei Sprachen eingeführt: Die Sprache
»Loop« und die Sprache »While«. Beide sind sehr einfach und es
stellt sich heraus, dass Loop zu einfach ist -- es gibt
einige Funktionen, die nicht »Loop-berechenbar« sind, obwohl man sie
eigentlich recht einfach ausrechnen kann.
Die Programmiersprache While ist fast identisch zu Loop, es gibt nur
eine minimale Erweiterung: Die While-Schleife. Diese Erweiterung hat
es aber in sich: Wir werden zeigen, dass While-Programme genauso
mächtig sind wie ram-Programme, von denen wir wiederum
schon wissen, dass sie alle Turing-Maschinen simulieren können.
Kaum etwas wird unter Informatik-Studenten und
Informatik-Professoren mit mehr Elan diskutiert als die Frage, ob
man im ersten Semester eines Informatik-Studiums lieber mit
funktionaler} oder doch lieber mit imperativer
Programmierung beginnen sollte. Sollten Sie sich an solcher einer
Debatten beteiligen wollen, hier ein paar Argumente für beide
Seiten:
\begin{block{Warum man mit funktionaler Programmierung beginnen sollte}
\begin{itemize}
\item Funktionale Sprache sind eleganter, es gibt weniger
Sonderregeln und Sonderfälle.
\item Funktionale Sprachen zeigen Grundideen wie die Rekursion
viel klarer auf, was für Einsteiger sehr nützlich ist.
\item Alle Studierende fangen auf einem ähnlichen Wissensstand an.
\item Es gibt keine Zeiger, wodurch eine große
Verständnisschwierigkeit wegfällt.
\item Funktionale Sprachen lassen sich prinzipiell viel
besser optimieren.
\end{itemize}
\end{block}
Für die imperativen Sprachen sprechen im Wesentlichen drei Gründe:
\begin{block}{Warum man mit imperativer Programmierung beginnen sollte}
\begin{itemize}
\item Im Alltag der Software-Entwicklung werden
imperative Sprachen eingesetzt.
\item Im Alltag der Software-Entwicklung werden
imperative Sprachen eingesetzt.
\item Im Alltag der Software-Entwicklung werden
imperative Sprachen eingesetzt.
\end{itemize}
\end{block}
In diesem Kapitel werden nun gleich zwei funktionale Sprachen
eingeführt mit den schönen Namen PrimitiveML und MüML (das »ML« steht
für die Programmiersprache gleichen Namens, von der die Syntax
dieser Sprachen geborgt wurde). Ähnlich wie im vorherigen Kapitel
sind dies keine »ausgebauten« funktionalen Programmiersprachen;
wieder geht es nur darum, einen möglichst minimalistischen Kern zu
definieren, auf dem man dann jede Menge Syntactic-Sugar streuen
kann, um daraus eine vernünftige Programmiersprache zu machen.
Eine der wesentlichen Eigenschaften einer funktionalen Sprache
ist, dass sie Rekursion unterstützt. Genaugenommen wird in
der funktionalen Programmierung Rekursion für einfach alles genutzt,
selbst da, wo es nun wirklich keinen Sinn ergibt. Dies mag einer
der Hauptgründe sein, weshalb manche Leute allergisch auf solche
Sprachen reagieren. Die beiden Sprachen PrimitiveML und MüML
unterscheiden sich lediglich dadurch, welche Arten von
Rekursion erlaubt sind.
Zwei imperative Sprachen, zwei funktionale Sprachen, zwei Kapitel.
In der Special Theory Edition} von \emph{Matrix Reloaded kommentiert Morpheus
dies wie folgt: »I do
not believe in chance when I see two chapters, two imperative
language, two funtional languages. I do not see coincidence, I see
providence, I see purpose. I believe it is our fate to be here. It
is our destiny. I believe this chapter holds for each and every one
of us the very meaning of our lives.« Wie wir sehen werden, hat
er mit »I see providence« recht: Die imperative Sprache
Loop ist genau gleichmächtig zur funktionalen Sprache PrimitiveML,
die Sprache While hingegen zu MüML. Ob allerdings seinem letzten
Satz zuzustimmen ist, bleibt abzuwarten.
Wie schon im Vorwort ausführlich dargelegt, versucht sich die
Theoretischen Informatik sich mit den alltäglichen Problemen der
Praktischen oder gar Technischen Informatiker möglichst nicht
herumzuschlagen. Wir betrachten Modelle, bei denen es solche
Absonderlichkeiten wie a20-Gates oder
Non-Maskable-Interrupts einfach nicht gibt. Ihre Kulmination hat
eine wahrhafte Abstraktionsorgie in der Turing-Maschine gefunden --
ausgehend von der Vorstellung eines realen Menschen (was ein,
mit Verlaub, noch viel komplexeres Wesen ist als ein Computer) ist
man bei einem Modell angekommen, das doch in seiner Abstraktheit und
Einfachheit kaum noch zu unterbieten ist. (Tatsächlich geht es auch
noch einfacher, aber dazu in einem späteren Kapitel.)
Es ist nun an der Zeit, wieder etwas zurückzurudern. Treibt man es
nämlich mit der Abstraktion zu weit, dann besteht die
Gefahr, dass die bewiesenen Sätze einem in der Praxis nur
bedingt weiterhelfen -- die realen Probleme sind
wegabstrahiert worden. So werden in der Theoretischen Informatik
beispielsweise mit viel Liebe Algorithmen sehr gründlich und
detailliert untersucht, deren Laufzeit so katastrophal hoch ist,
dass sie schon bei der Eingabelänge $1$ (in Worten: Eins) auf einem
Quanten-Computer so groß wie das Universum so lange rechnen würden,
wie das Universum alt ist.
In diesem Kapitel geht es um ein Maschinen-Modell, das
wesentlich näher am realen Computer ist als die
Turing-Maschine. \alert{Register-Maschinen} verfügen über einen
Speicher, der ähnlich einem realen Computerspeicher aus einzelnen
Zellen aufgebaut ist, die Zahlen speichern. Weiterhin verfügen
Register-Maschinen über einen Befehlssatz, der sehr dicht dran ist
an dem typischer Prozessoren.
Natürlich liegt auch Register-Maschinen noch ein gewisser Grad an
Abstraktion zu Grunde. So kann man -- völlig unrealistischer Weise
-- beliebig große Zahlen in jeder Speicherzelle speichern. Weiterhin
ist der Befehlssatz noch etwas minimalistisch verglichen mit einem
Pentium-Prozessor.
Die entscheidende Frage ist nun, wie sich das etwas realistischere
Modell von Computern zum Modell der Turing-Maschine verhält. Hier
werden wir einen sehr befriedigenden Satz beweisen: Diese beiden
Modelle sind genau gleich mächtig -- was Turing-Maschinen können,
können auch Register-Maschinen; und umgekehrt. Die vom praktischen
Standpunkt aus recht nützliche Konsequenz aus diesem Satz ist, dass
wir \alert{es uns immer wieder neu aussuchen können,} ob wir lieber
mit Turing-Maschinen oder mit Register-Maschinen arbeiten
wollen. Will man theoretische Resultate beweisen, so sind
Turing-Maschinen oft einfacher zu handhaben; will man praktische
Algorithmen untersuchen, so sind Register-Maschinen besser. Sie
haben die Wahl.
Wozu wurden in Kapitel~\ref{lecture-nfa} eigentlich
nichtdeterministische endliche Automaten eingeführt? Wenn man sich
das Kapitel nochmal durchliest, so wird dort wortreich erklärt, dass
Nichtdeterminismus ganz toll und wichtig sei -- ein wirklich
schlagendes Beispiel sucht man vergebens. Schlimmer noch: Es wird
gleich gezeigt, dass man den Nichtdeterminismus gar nicht braucht,
da er auch nicht mehr kann als die deterministischen Automaten! Er
sieht verdächtig nach »Viel Lärm um nichts« aus; einem Thema, dem
sich Shakespeare zwar ausführlich widmete, das deshalb aber
für ein nichtgeisteswissenschaftliches Studium zunächst
von geringer Relevanz erscheint.
Man muss schon weiterlesen, um die Wichtigkeit von
nichtdeterministischen endlichen Automaten zu entdecken: Erst im
Beweis von Satz~\ref{thm-reg-char}, und dort auch nur im
Kleingedruckten, werden diese Automaten gebraucht, um Ableitungen
mittels regulärer Grammatiken durch endliche Automaten
nachzuvollziehen. Mit anderen Worten: Das ganzes Konzept des
nichtdeterministischen Automaten wurde eigentlich nur eingeführt, um
ein Detailproblem in einem Beweis eines bestimmten Satzes zu
lösen. Sollten Sie zu den Lesern gehören, denen Beweise sowieso
unsympathisch sind, so werden Sie sicherlich spätestens jetzt dem
Konzept des Nichtdeterminismus die Daseinsberechtigung
absprechen.
In diesem Kapitel geht es nun um nichtdeterministische
Turing-Maschinen. Dabei liegt die Sachlage ähnlich wie bei endlichen
Automaten: So richtig brauchen tut man diese Maschinen praktisch
nicht. Wieder sind sie aber aus Sicht der Theorie sehr wichtig --
wodurch sie in einer Veranstaltung mit dem Titel »Theoretische
Informatik« wohl doch eine Daseinsberechtigung haben.
Der britische Premierminister Gordon Brown verkündete im September 2009:
\begin{quotation}
So on behalf of the British government, and all those who live
freely thanks to Alan's work I am very proud to say: we're sorry,
you deserved so much better.
\end{quotation}
Den Grund für diese Entschuldigung ergibt sich aus der Biographie,
von der die englische Wikipedia folgendes zu berichten weiß:
\begin{quotation}
\hfill Aus en.wikipedia.org/wiki/Alan\_Turing}, 2009
Alan Mathison Turing, [\dots] (\textsc{23 June 1912 –
\textsc7 June 1954), was an
English mathematician, logician, cryptanalyst, and computer
scientist. He was influential in the development of computer
science and providing a formalisation of the concept of the
algorithm and computation with the Turing machine. [\dots] His
Turing test was a significant and characteristically
provocative contribution to the debate regarding artificial
intelligence.
During the Second World War, Turing worked for the Government Code
and Cypher School at Bletchley Park, Britain's codebreaking
centre. For a time he was head of Hut~8, the section responsible
for German naval cryptanalysis. He devised a number of techniques
for breaking German ciphers, including the method of the bombe, an
electromechanical machine that could find settings for the Enigma
machine. After the war he worked at the National Physical
Laboratory, where he created one of the first designs for a
stored-program computer, the ace.
Towards the end of his life Turing became interested in
chemistry. He wrote a paper on the chemical basis of
morphogenesis, and he predicted oscillating chemical reactions
such as the Belousov–Zhabotinsky reaction, which were first
observed in the 1960s.
Turing's homosexuality resulted in a criminal prosecution in
1952 -- homosexual acts were illegal in the United Kingdom at that
time -- and he accepted treatment with female hormones, chemical
castration, as an alternative to prison. He died in 1954, several
weeks before his 42nd birthday, from an apparently
self-administered cyanide poisoning, although his mother (and some
others) considered his death to be accidental.
\end{quotation}
Die Sprachen, die von Grammatiken, deren Regeln immer genau ein
Nonterminal auf der Seite, die links von dem Pfeil, der zwischen den
Worten, die die Regelseiten gemäß der Sprachregelung, die in
dem Skript, das Sie gerade, wo auch immer der Ort, an den Sie diese
Tätigkeit gerade ausüben, sein mag, lesen, genutzt wird, genannt
werden, steht, haben, erzeugt werden, sind verschachtelt.
\includegraphicscopyrightborderautowidth{\bodydir/media/Russian-Matroshka-ccas-Fanghong.jpg}{Autor
Wikimedia-User Fanghong, Creative Commons Attribution Sharealike}%
Menschen mögen vor Nebensatzkaskaden kapitulieren -- Computer
hingegen halten sie für eine völlig normale Art der
Kommunikation. Sätze, die jeder Lektor einem Schriftsteller
gnadenlos »geradeziehen« würde, werden von Computern genüsslich nach
ihrer Struktur durchforstet und dies ist bei den wichtigsten Texten,
die Computer so lesen müssen, auch bitter nötig:
Programmtexte} sind in ascii gegossene
Nebensatzkathedralen. Damit Menschen überhaupt eine kleine Chance
haben, der Struktur eines Programmtextes zu folgen, hat sich als
sehr nützlich herausgestellt, mit Einrückungen zu arbeiten, um
anzudeuten, was wozu gehört. Bekanntermaßen sind dem Compiler Ihre
Einrückungen ziemlich schnurz, für ihn ist
\begin{lstlisting[language=Java]
\end{lstlisting}
ein völlig klares Programm. Ihnen auch?
Sprachen mit verschachtelten Worten begegnen einem in der Informatik
auf Schritt und Tritt. Wie schon erwähnt sind Programmtexte
hochgradig verschachtelt, jedoch auch html-Texte und auch
allgemein xml-Texte, \LaTeX-Dokumente und viele mehr.
In diesem und dem folgenden Kapitel soll es um Grammatiken gehen,
mit denen wir diese Art von Sprachen gut beschreiben können:
kontextfreie Grammatiken. Die erste wichtige Erkenntnis
betreffend kontextfreie Grammatiken ist, dass sie tatsächlich mehr
können als reguläre Grammatiken. Dies ist nach den vorherigen
Kapiteln ja eine eher erfrischende Feststellung, konnte man doch
vielleicht den Eindruck gewinnen, Hauptaufgabe der Theoretischen
Informatik sei es, möglichst viele unterschiedliche Methoden zu
ersinnen, reguläre Sprachen zu beschreiben.
Ähnlich wie bei der Untersuchung von regulären Sprachen werden wir
zunächst untersuchen, was kontextfreie Sprachen so alles können oder
nicht können. Wie man die deskriptiven Fähigkeiten kontextfreier
Grammatiken praktisch einsetzen soll sei hier erstmal
zweitrangig. Im nächsten Kapitel geht es dann genau um die
praktische Frage, wie man einen Parser für kontextfreie Sprachen
bauen kann.
Nachdem im letzten Kapitel die prinzipielle Fähigkeiten, aber auch
die prinzipiellen Grenzen von kontextfreien Sprachen ausgelotet
wurden, soll es nun um die praktische Frage gehen, wie das Parsing
für kontextfreie Sprache funktioniert.
Die erste beruhigende Erkenntnis ist, dass es überhaupt prinzipiell
möglich ist, jede beliebige kontextfreie Sprache zu
parsen. Tatsächlich ist die Lage noch um einiges erfreulicher:
Mit Hilfe des cyk-Algorithmus lässt sich sogar
vergleichsweise effizient überprüfen, ob und wenn ja wie sich
ein Wort aus einer gegebenen kontextfreien Grammatik erzeugen
lässt.
\includegraphicscopyrightborderautowidth{\bodydir/media/Grapevinesnail-ccasa-Juergen_Schoner.jpg}{Bild
von Jürgen Schoner, Creative Commons Attribution Sharealike Lizenz}%
Leider ist der cyk-Algorithmus nur »vergleichsweise«
effizient. Es kommt darauf an, womit man ihn vergleicht. Im
Vergleich zu Algorithmen für die Travelling Salesperson Problem oder
für $\Lang{sat}$ ist der cyk-Algorithmus tatsächlich
rasend schnell. Verglichen mit einem Such- oder Sortiertalgorithmus
hingegen gleicht der cyk-Algorithmus eher ein
Weinbergschnecke: Er hat eine Laufzeit von grob $O(n^3)$ bei
Wörtern der Länge~$n$. Ist nun das »Wort«, das vom Parser
verarbeitet werden soll, ein Programmtext, so kann $n$ schnell mal
in der Größenordnung einer Million liegen. In solch einem Fall ist
$O(n^3)$ leider keine akzeptable Laufzeit mehr; niemand möchte
ca. 30 Jahre darauf warten, dass ein Compiler die Meldung »Semicolon
expected in line 123456« produziert (eine, nebenbei bemerkt,
besonders perfide Fehlermeldung, da der Compiler ja ganz genau weiß,
was das Problem ist, und auch, wie man es lösen könnte -- er tut es
nur einfach nicht).
Eine zweite Idee ist, eine ähnliche Methodik wie bei regulären
Grammatiken anzuwenden. Dort haben wir auch ausgehend von den
Grammatiken ein Maschinenmodell entworfen (die endlichen
Automaten), mit deren Hilfe wir dann die regulären Sprachen sehr
effizient verarbeiten konnten. Zu den kontextfreien Grammatiken gibt
es auch ein passendes Maschinenmodell: Kellerautomaten (englisch
Push-Down-Automata, pdas). Genau wie normale Automaten
verarbeiten auch pdas ihre Eingaben sehr
effizient. Allerdings passt das Modell auch nicht perfekt: Nicht je
kontextfreie Sprache lässt sich von einem deterministischen
pda akzeptieren. Es ist deshalb in der Praxis recht
wichtig, sich auf solche Grammatiken zu beschränken, wo dies dann
doch möglich ist.
\includegraphicscopyrightborderautowidth{\bodydir/media/Muelltonnen_diverse-ccas-unknown.JPG}{Unbekannter
Autor, Creative Commons Attribution Sharealike Lizenz}
Wahrscheinlich tut man den Nerode-Klassen Unrecht, sie aus
didaktischen Gründen im Rahmen der Mülltrennung
einzuführen. Dabei ist es für die Lektüre vorteilhaft, aber auch
nicht unbedingt nötig, das in Deutschland zur
Obsession gesteigerte Verlangen zu verspüren, Müll nach allen nur
denkbaren Kriterien zu trennen und zu sortieren. Es wird Ihnen in
diesem Fall ganz natürlich vorkommen, auch Worte nach reichlich
merkwürdigen Kriterien zu sortieren und in Worttonnen einzukippen.
Der Begriff der »Worttonnen« hat sich in der Theorie allerdings
(noch?) nicht wirklich durchgesetzt, hätte jedoch gegenüber der
sperrigen, aber mathematisch korrekten Bezeichnung
»maximale Rechtsäquivalenzklasse« durchaus Chancen. Tatsächlich
nennt man Worttonnen vornehm »Nerode-Klassen«.
Was bringt es nun, Worte in Klassen zu trennen -- egal ob sie nun
Worttonnen oder Nerode-Klassen heißen? Eine ganze Menge, wie sich
herausstellt:
\begin{enumerate}
\item Aus den Nerode-Klassen einer regulären Sprache lässt sich
leicht ein endlicher Automat bauen.
\item Dieser Automat ist auch gleich noch der kleinste überhaupt
mögliche Automat für die Klasse.
\item Ist die Anzahl der Nerode-Klassen einer Sprache unendlich, so
kann die Sprache nicht regulär sein.
\end{enumerate}
Insbesondere der letzte Punkte dürfte alle Leser erfreuen, die mit
dem Pumping-Lemma auf Kriegsfuß stehen, da es oft einfach ist zu
zeigen, dass eine Sprache unendlich viele Nerode-Klassen hat.
Reguläre Grammatiken und endliche Automaten sind zwei Seiten
derselben Münze: Grammatiken beschreiben Sprachen, Automaten
akzeptieren sie. Heute soll der Münze noch eine dritte Seite
hinzugefügt werden beziehungsweise, da dies offenbar bei Münzen
zumindest im dreidimensionalen Raum nicht möglich ist, auf eine der
Seiten noch ein weiteres Konzept zusätzlich eingraviert werden:
reguläre Ausdrücke. Diese sind, genau wie reguläre Grammatiken, ein
Beschreibungsformalismus für reguläre Sprachen.
Reguläre Ausdrücke können nicht mehr und nicht weniger als reguläre
Grammatiken, sie sind gleichmächtig. Jedoch sind reguläre
Ausdrücke in aller Regel wesentlich kompakter als reguläre
Grammatiken, gleichzeitig sind sie für Menschen wesentlich leichter
zu erstellen -- wenn auch nicht unbedingt leichter zu lesen. Selbst
ein Profi wird den regulären Ausdruck
\verb="[.?!][]\"')]*\\($\\| $\\|\t\\| \\)[ \t\n]*"=
nicht sofort verstehen. Dies ist laut Emacs-Handbuch »a
simplified version of the regexp that Emacs uses, by default, to
recognize the end of a sentence together with any whitespace that
follows.«
Auch wenn reguläre Ausdrücke fürchterlich komplex und unlesbar
werden können, so ist die Grundidee doch recht einfach: Beginnend
mit trivialen Sprachen (bestehend nur aus einem Wort) kann man durch
bestimmte Operationen wie Vereinigung oder Verkettung neue Sprachen
bauen. Ein regulärer Ausdruck gibt nun gerade an, wie und in welcher
Reihenfolge die Operationen angewendet werden sollen.
Die Hauptschwierigkeit bei regulären Ausdrücken ist auch nicht,
dieses Konzept zu verstehen. Problematisch ist, dass sich keine
einheitliche Syntax herausgebildet hat, wie man die Ausdrücke nun
aufschreiben sollte. Hier kocht jeder sein eigenes Süppchen, welche
wir nun alle auslöffeln müssen.




