8.3 Varianten: Mehrband, Halbband, Oblivious, nichtdeterministisch

Turingmaschinen mit mehreren Bändern

Im letzten Teilkapitel ist Ihnen bestimmt aufgefallen, dass es auffallend lästig ist, selbst für einfache Sprachen wie {anbncn | n0} oder {wcw | w{a,b}} Turingmaschinen zu programmieren. Ein Grund dafür ist, dass die Maschine nur an einer Position des Bandes lesen und schreiben kann und man deswegen ständig zwischen verschiedenen Stellen hin- und herfahren muss. Es bietet sich daher an, an etwas allgemeineres Modell einer Rechenmaschine zu definieren, das dann auch leichter zu programmieren ist. Dies ist die Mehrband-Turingmaschine.

Eine Mehrband-Turingmaschine ist wie eine Turingmaschine, nur dass sie statt einem k viele Bänder und somit auch k viele Schreib-Lese-Köpfe hat. Die Zustandsübergangsfunktion δ hat somit auch die Signatur δ:Q×ΓkQ×Γk×{L,S,R}k

Beispiel 8.3.1 Entwerfen wir nun eine Turingmaschine für die Palindromsprache L:={w{a,b} | w=wR} , In Beispiel 4.2.2 haben wir dafür eine Einband-Turingmaschine geschrieben. Deren Nachteil war, dass sie ständig zwischen dem linken und rechten Rand hin-und-herlaufen musste.

Bauen wir nun eine Mehrband-Turingmaschine. Diese arbeitet in drei einfachen und kurzen Phasen:

  1. copy: kopiert das w auf das zweite Band
  2. rewind: bewegt den Kopf des ersten Bandes zurück zum Anfang
  3. compare: schaut, ob erstes und zweites Band den gleichen Inhalt haben.
Den "Quelltext" für turingmachinesimulator.com finden Sie in palindrome-multiple-tapes.txt.

Übungsaufgabe 8.3.1 Schreiben Sie eine Mehrband-Turingmaschine, die Binärzahlen addiert. Wenn also beispielsweise 1010+110 auf dem ersten Band (Eingabeband) steht, dann soll nach Abschluss der Berechnung das Ergebnis auf dem Ausgabeband stehen, also 10000.

Tip. Verwenden Sie drei Bänder. Sei der Bandinhalt x+y. In einer ersten Phase kopieren Sie x auf das zweite Band. In der nächsten Phase gehen Sie ans Ende von y. Dann addieren Sie nach den Regeln der Binäraddition. Ob "1 gemerkt" gilt oder nicht, können Sie in Ihrem internen Zustand speichern. Eine Regel wäre also zum Beispiel:

carry1, 0, 0, _ 
carry0, 0, 0, 1, <, <, < 

carry1, 0, 1, _ 
carry1, 0, 1, 0, <, <, < 

Ein lästiges Detail ist, dass y kürzer sein könnte als x und Sie daher in das + reinlaufen könnten; wenn x kürzer ist als y, dann könnten Sie auf dem zweiten Band in ein reinlaufen. Wie ist dieser Fall zu behandeln?

Berechnete Sprache, berechnete Funktion. Die Begriffe des Akzpetierens und Ablehnens definieren wir genau wie für die Einband-Turingmaschinen. Eine formale Definition der Konfiguration ersparen wir uns jedoch. Wenn unsere Mehrband-Turingmaschine nicht nur akzeptieren / ablehnen, sondern etwas berechnen soll, also eine Funktion f:Σ1Σ2 , dann bauen wir sie per Konvention so, dass sie ein designiertes Ausgabeband hat, auf dem nach Abschluss der Berechnung das Ausgabewort f(x) steht.

Einband-Maschinen können Mehrband-Maschinen simulieren

Es stellt sich heraus, dass mehrere Bänder zwar ein praktisches Feature sind, aber nicht wirklich mehr Ausdruckskraft verlangen; was eine Mehrband-Turingmaschine schafft, schafft eine Einband-Turingmaschine auch.

Theorem 8.3.2 (Einband-Turingmaschine simuliert Mehrband-Turingmaschine). Sei M eine Turingmaschine mit k Bändern, wovon eines ein designiertes Ausgabeband ist. Dann gibt es eine Einband-Turingmaschine M mit folgenden Eigenschaften:
  1. M(x) akzeptiert/lehnt ab/terminiert nicht genau dann, wenn M(x) akzeptiert/ablehnt/nicht terminiert.
  2. Wenn M(x) akzeptiert und y der Bandinhalt des Ausgabebandes ist, dann akzeptiert M(x) auch, und der Bandinhalt (des einzigen Bandes, es gibt ja nur eins) ist y.

In anderen Worten: M simuliert M.

Beweis. Der erste Trick ist, dass wir die k Bänder von M "zusammenkleben" in ein neues Band, in welcher jede Zelle k Symbole enthalten kann:
Das Problem sind nun die Köpfe. Wenn wir diese Idee naiv umsetzen würden, hätte unsere Maschine M zwar ein Band, dafür drei Köpfe auf diesem:

Wir lösen dies, indem wir die Kopfpositionen in das Band selbst reinschreiben:

Das neue Bandalphabet ist also nicht Γk sondern (Γ×{head,nohead})k. Wo steht nun aber denn der Kopf von M? Jetzt kommt der schwierige Teil: um einen Schritt von M zu simulieren, muss M von ganz links nach ganz rechts laufen und alle Informationen über die k M-Köpfe sammeln. Dann von rechts nach links gehen und die ausführen.

Wir müssen also die Zustandsmenge deutlich erweitern; so muss sie speichern können, ob wir ein Symbol bereits gelesen haben; ob wir ein Symbol bereits geschrieben haben und ob wir den Kopf bereits entsprechend verschoben haben. Für eine Rechtsverschiebung müssen wir uns zusätzlich noch merken, dass wir sie gerade durchführen und für welches Band. All dies ist viel, aber immer noch endlich. Wir können alles in einer endlichen Zustandsmenge Q speichern.

Wenn der M-Zustand (der natürlich auch im M-Zustand gespeichert ist), accept erreicht hat, dann macht M noch eine Aufräumphase, in welcher sie alle Symbole, die nicht zum Ausgabeband gehören, durch ersetzt. Dann wechselt sie in ihren eigenen akzeptierenden Zustand accept'.

Einfügen versus Überschreiben

Folgende Aufgabe ist auf einer Einband-Turingmaschine sehr leicht: gegeben ein Eingabewort w{a,b}, ersetze jedes b durch ein c. In der Syntax von turingmachinesimulator.com:
name: replace_b_by_c
init: init 
accept: accept 

init, a
init, a, > 

init, b
init, c, > 

init, _ 
accept, _, > 

Ungleich schwieriger ist die Aufgabe, jedes b durch ein bc zu ersetzen, weil wir hier etwas einfügen wollen. Auf einer Einband-Turingmaschine müssen wir für jedes b alles, was rechts davon kommt, um eine Zelle nach rechts verschieben. Meinen Quelltext finden sie in replace-b-by-bc.txt. Können wir unserer Turingmaschine die Funktionalität geben, eine Zelle einzufügen und alles von Kopf bis zum linken Ende um eins nach links zu verschieben bzw. das analoge, aber nach rechts? Wir sind freie Menschen, wir können definieren, was wir wollen, müssen uns aber zwei Fragen stellen:

  1. Ist das immer noch ein plausibles Modell einer Rechenmaschine? Ist also unser neue Funktionalität physikalisch realisierbar?
  2. Verleiht es wirklich neue Funktionalität, oder ist es nur Syntaxzucker?

In diesem Fall ahnen Sie es wohl bereits: es ist nur Syntaxzucker. Die Funktionalität des Einfügens/Verschiebens können wir leicht mit zwei Bändern simulieren. Wir halten uns einfach an die Konvention, dass auf Band 1 der Kopf immer auf dem linkesten Zeichen steht und auf Band 2 der Kopf jenseits des rechtesten. δ(q,x)=(r,y,R) wird dann

q, x, _
r, _, x, >, > 
Eine Linksbewegung ist etwas schwieriger zu implementieren. Aus δ(q,x)=(r,y,L) wird
q,  x, _ 
r', x, _, -, - 

r', _, c
r,  c, _   // für jedes Zeichen c
                            

Nehmen Sie die Beispielmaschine go-left-go-right.txt, geben Sie sie auf turingmachinesimulator.com ein und starten Sie sie mit dem Eingabewort xxx.

Ein neues Zeichen links vom Kopf einfügen ist nun einfach: aus δ(q,x)=Zustand r, schreibe y und füge z links vom Kopf ein wird

q,   x, _ 
r'', _, z, >, > 

r'', c, _ 
r,   c, y, -, > // für jedes Zeichen c 

Sie können sich meine Implementierung in insert-z-before-y.txt ansehen. Geben Sie beispielsweise xxyxyyxx als Eingabewort ein.

Wir können von nun an also so tun, als hätten unsere Turingmaschinen die Möglichkeit, zusätzliche Zellen einzufügen. In einer konkreten Implementierung müssten wir dafür allerdings jedes Band durch zwei Bänder ersetzen. Alternativ können Sie sich eine Turingmaschine vorstellen, die statt k Bändern einfach 2k Stapel hat.

Die Dictionary-Maschine

Ein fundamentale Datenstruktur beim Programmieren sind Dictionaries, die Key-Value-Paare speichern:

python -i                        
dict = {"karl" : 42, "eva" : 35, "werner" : 20}
dict["eva"]
35

Im Zweifelsfall sind diese als Hashmaps oder Rot-Schwarz-Bäume oder B-Bäume implementiert. Hier interessiert uns nicht so sehr die Laufzeit, sondern einfach die Funktionalität. Können wir für Dictionaries eine Turingmaschine implementieren?

Übungsaufgabe 8.3.2 Schreiben Sie auf turingmachinesimulator.com eine Mehrband-Turingmaschine, die Inputs der Form k[k1:v1;k2:v2;;kn:vn] entgegennimmt, für k,k1,,kn,v1,,vn{0,1}n, also Σ={0,1,:,;,[,]} und akzeptiert, wenn es ein i gibt mit k=ki und in diesem Falle vi auf das Ausgabeband schreibt.

Tip: Kopieren Sie erst einmal den gesuchten Schlüssel k auf das zweite Band. Dann können Sie bequem den Schlüssel ki auf dem ersten Band mit dem auf dem zweiten Band vergleichen. Wenn Sie es sich einfach machen wollen, nehmen Sie einfach mal an, dass alle Schlüssel gleiche Länge haben. Das erspart Ihnen gefühlt 20 Zeilen im Programmcode der Turingmaschine.

Nichtdeterministische Turingmaschinen

Bereits im Kapitel über reguläre Sprachen haben wir gesehen, dass Nichtdeterminismus hilfreich ist, wenn wir Dinge beschreiben wollen, auch wenn es kein realistisches Modell für Rechenmaschinen darstellt. Die Sprache aller Wörter über {a,b}, die das Teilwort aababaa enthalten, kann man beispielsweise leicht mit dem folgenden Automaten beschreiben:

Es ist klar, was dieser Automat erlaubt. Einen deterministischen Automaten für die gleiche Sprache zu entwerfen (ohne systematisch über den nichtdeterministischen zu gehen) wird schnell chaotisch, und Sie werden sich in den vielen Fallunterscheidungen verlieren.

Andererseits haben wir für endliche Automaten gezeigt, dass die deterministischen und nichtdeterministischen Varianten tatsächlich gleichmächtig sind (Potenzmengenkonstruktion). Für die Kellerautomaten, die für kontextfreie Sprachen relevant sind, galt das nicht (einen Beweis haben wir allerdings in der Vorlesung nicht durchgenommen). Wie sieht es nun für Turingmaschinen aus?

Um nichtdeterministische Turingmaschinen zu definieren, müssen wir die Zustandsübergangsfunktion δ zu einer Zustandsübergangsrelation machen. Statt δ:Q×ΓQ×Γ×{L,S,R} also nun δ(Q×Γ)×(Q×Γ×{L,S,R}) . Und statt δ(q,x)=(r,y,D) schreiben wir nun (q,x)δ(r,y,D). Wir beschränken uns zunächst auf Einband-Turingmaschinen. Für Konfigurationen CΓ×Q×Γ haben wir keine erweiterte Zustandsübergangsfunktion δ(C)=C, wo C die Folgekonfiguration ist, sondern eine erweiterte Zustandsübergangsrelation: CC Wobei nun dank Nichtdeterminismus mehrere Folgekonfigurationen C geben kann (oder eben mal auch gar keine). Wir schreiben CC wenn wir von C in einer Folge von Schritten nach C kommen können, also C=C0C1C2C Wir sagen auch: Die Konfiguration C ist von C aus erreichbar.

Für ein Eingabewort x sie Cx:=startx die Startkonfiguration. Eine nichtdeterministische Turingmaschine akzeptiert x, wenn es eine akzeptierende Endkonfiguration Caccept gibt mit CxCaccept wenn es also (mindestens) eine akzeptierende Konfiguration gibt, die von Cx aus erreichbar ist. Dabei kann es mehrere erreichbare akzeptierende Konfigurationen geben, Es kann sogar eine ablehnende Konfiguration CxCreject geben. Spielt keine Rolle: solange es einen Weg CxCaccept gibt, sagen wir, dass M das Eingabewort akzeptiert.

Definition 8.3.3 (Akzeptieren und Entscheiden bei nichtdeterministischen Turingmaschinen). Eine nichtdeterministische Turingmaschine M akzeptiert die Sprache L wenn xLM akzeptiert x Für jedes xL gibt es also keine akzeptierende Konfiguration C mit CxC.

Die Turingmaschine M entscheidet die Sprache L, wenn sie sie akzeptiert und es keine unendlich langen Ketten CxC1C2 gibt.

Oft wird händeringend versucht, zu erklären, was denn eine nichtdeterministische Turingmaschine tut. Da lesen Sie dann beispielsweise, dass die alle Möglichkeiten gleichzeitig ausprobiert oder den richtigen Pfad von einem Engel gesagt bekommt oder errät. Ich stelle mir lieber vor, dass eine nichtdeterministische Turingmaschine gar nichts "tut" sondern Spielregeln definiert, wie man "ziehen" kann. Man gewinnt, wenn man in einer akzeptierenden Konfiguration landet.

Beispiel 8.3.4 Beim Teilsummenproblem haben wir eine Liste von Waren (alles Unikate) mit Preisen p1,p2,,pn und ein Guthaben g gegeben und wollen wissen, ob wir unser Guthaben exakt ausgeben können. Ob es also eine Teilmenge I[n] von Waren gibt, die genau g kostet: gibt es ein I[n] mit iIpi=g? Um das als formale Sprachen bzw. Entscheidungsproblem zu formalisieren, müssen wir uns eine Codierung überlegen. Preise sind ganze Zahlen (in Cent, wenn Sie so wollen), in Dezimalschreibweise dargstellt. Waren sind mit einem # separiert. Nach den Waren kommt ein : und dann das Guthaben. Wenn also beispielsweise die Waren die Preise 65, 8, 22, 19, 7, 58, 30, 1, 13, 38 haben und unser Guthaben 194 ist, dann würden wir das als Wort #65#8#22#19#7#58#30#1#13#38:194 über dem Alphabet Σ={0,1,2,3,4,5,6,7,8,9,#,:} codieren. Das Wort ist in unserer Sprache L enthalten, wenn es nun eben eine Teilmenge gibt, die sich genau zu 194 aufsummiert.

Entwerfen wir nun eine nichtdeterministische Turingmaschine M für diese Sprache. M geht von links nach rechts alle Waren durch. Jedes Mal, wenn ein Preis beginnt, haben wir die Möglichkeit, diesen Preis auf das zweite Band zu kopieren (die Ware zu kaufen) oder eben nicht: also (choose,#,)(buy,#,+,R,R)(choose,#,)(skip,#,R,S)(für jedes c{0,,9})(buy,c,)(buy,c,c,R,R)(buy,#,)(choose,#,,S,S)(für jedes c{0,,9})(skip,c,)(skip,c,,R,S)(skip,#,)(choose,#,,S,S)(buy,:,)(add,:,,S,S)(skip,:,)(add,:,,S,S) Dies erlaubt uns zum Beispiel, bei Eingabe #65#8#22#19#7#58#30#1#13#38:194 eine Konfiguration zu erreichen, wo auf dem ersten Band der Kopf auf dem : steht und auf dem zweiten Band +6+19+58+1+13 aber eben auch jede beliebige andere Summe. Im Zustand add aufgerufen, muss nun die Turingmaschine alle diese Zahlen auf dem zweiten Band addieren (lästig, geht aber irgendwie) und dann in einer dritten Phase mit der Zahl rechts vom : vergleichen. Stimmen sie überein, akzeptiert die Turingmaschine, stimmt sie nicht über ein, lehnt sie ab. Da 6+19+58+1+13=97, haben wir eine ablehnende Konfiguration erreicht. Es sind aber viele Endkonfigurationen erreichbar. Wir können zum Beispiel in Phase 1 auch +65+22+19+7+30+13+38 auf das untere Band kopieren. Da dies tatsächlich 194 ergibt, akzeptiert die Turingmaschine. Wir sehen also: M(#65#8#22#19#7#58#30#1#13#38:194)=accept , weil es eben eine vom Start aus erreichbare akzeptierende Konfiguration gibt.

Deterministische Turingmaschinen simulieren nichtdeterministische

Sind nun nichtdeterministische Turingmaschinen inhärent mächtiger? Können Sie das Teilsummenproblem auch mit einer deterministischen lösen? Klar! Hier ist mein Code in Elm, einer funktionalen Programmiersprache: er probiert alle Möglichkeiten durch.

module SubsetSum exposing (..)


subsetSum : List Int -> Int -> Bool
subsetSum prices amount =
    case ( prices, amount ) of
        ( [], 0 ) ->
            True

        ( x :: rest, _ ) ->
            subsetSum rest amount || subsetSum rest (amount - x)

        ( [], _ ) ->
            False

Auf einer deterministischen Turingmaschine wäre das deutlich anstrengender, aber irgendwie auch möglich. Können wir jede nichtdeterministische Turingmaschine deterministisch simulieren, indem wir "alles ausprobieren"? Ja, in der Tat!

Theorem 8.3.5 (Nichtdeterministische Turingmaschinen deterministisch simulieren) Sei M eine nichtdeterministische Turingmaschine. Dann gibt es eine deterministische Maschine M mit L(M)=L(M), d.h. M akzeptiert x genau dann, wenn M es akzeptiert.

Zusätzlich gilt: wenn M die Sprache nicht nur akzpetiert, sondern entscheidet, dann entscheidet auch M die Sprache (terminiert also auf jedem Eingabewort).

Beweis. Als erstes führen wir eine kosmetische Änderung unserer nichtdeterministischen Maschine durch: wir wollen, dass es für jedes (q,c) genau zwei Möglichkeiten gibt, also (q,c)(q1,c1,D1)(q,c)(q2,c2,D2) , außer wenn q{reject,accept}; dann gibt es gar keine Möglichkeit. Dies ist einfach: sollte es mehr als zwei Möglichkeiten geben, so führen wir Zwischenzustände ein:
Auf der Menge der Konfigurationen schaut das dann noch intuitiver aus:

Sollte ein Paar (q,c) weniger als zwei Folgemöglichkeiten geben, so erfinden wir einfach neue, die jedoch direkt nach reject führen. Es sollte klar sein, dass diese Änderungen rein kosmetisch sind und nichts an der Funktionsweise von M ändern.

Nun bauen wir M um und geben ihr ein zweites Band. Auf diesem Band soll ein Wort in {0,1} stehen. Wir machen M deterministisch mit der folgenden Regel: wenn Du im Zustand q bist und auf dem ersten Band ein c hast und auf dem zweiten Band eine 0 liest, nimm den oberen Pfeil, der von q,c ausgeht; wenn Du eine 1 liest, nimm den unteren Pfeil.

Falls wir auf dem zweiten Band einem anderen Zeichen begegnen ( oder sonst etwas, das weder 0 noch 1 ist), dann lehnen wir sofort ab. Wir haben nun eine deterministische Turingmaschine M, die jedoch nicht das gleiche tut wie M. Aber: wenn xL(M), dann gibt es ein Wort z{0,1}, das wir auf das zweite Band schreiben könnten, so dass M das Eingabewort x akzeptiert. Im Gegenzug: wenn xL(M), dann können wir auf das zweite Band schreiben, was wir wollen, M wird immer ablehnen.

Nun bauen wir schlussendlich eine Maschine M, die in einer Endlosschleife alle möglichen z{0,1} aufzählt, auf das zweite Band schreibt, und M neustartet. Geht das? Wir können zum Beispiel i=1,2,3,4, hochzählen, binär schreiben und die führende 1 löschen. Überzeugen Sie sich, dass in dieser Reihe wirklich alle z{0,1} vorkommen.