"Dancing Links" nach Donald Knuth erklärt

Es gibt viel englischsprachige Literatur zu dem Algorithmus "Dancing Links" oder DLX (Dancing Links - Algorithmus X) von Donald E. Knuth, auf deutsch etwa "Tanzende Zeiger". Hier möchte ich eine deutschsprachige Erklärung liefern.

top

Die Aufgabe

Das Problem, das man mit den "Dancing Links" lösen will und kann, ist folgendes:

Man hat eine Menge A, und verschiedene Mengen B1, B2, B3,... Jetzt will man aus den B1, B2, B3, ... mehrere Bs so auswählen, dass kein Element in den Bs doppelt vorkommt, und jedes Element aus A in den Bs vorkommt.

Man nennte es das "Problem der exakten Überdeckung", oder auf Englisch "exact cover".

Zu Abstrakt, zu schnell?

top

Ein Beispiel

Stellen wir uns vor, auf einer Insel sitzen ein paar Menschen, mit Namen Anton, Berta, Claudia, Daniel und Eva.

Diese netten Leute formen unsere Menge A.

Sie wollen von der Insel weg, und haben dafür Boote zur Verfügung. Allerdings vertragen sich manche nicht gut, und Antont und Berta sind ein Liebespaar, die nicht getrennt werden wollen.

Sie können sich nur folgende Kombinationen vorstellen, wie sie sich auf die Boote verteilen:

top
Name der GruppeMitglieder
B1Anton, Berta
B2Anton, Berta, Claudia
B3Anton, Berta, Daniel
B4Claudia, Eva
B5Claudia, Daniel, Eva

Natürlich wollen sie niemanden zurücklassen, und jede Person kann nur in einem Boot gleichzeitig sein.

In diesem Fall haben sie Glück, es gibt zwei Kombinationen, wie sie sich nach ihren Vorstellungen aufteilen können: B1 und B5 oder B3 und B5.

Wenn man ein wenig durchprobiert, wird das einem sehr schnell klar. Aber was ist, wenn man hunderte oder Tausende von Elementen hat, und noch sehr viel mehr mögliche Gruppen?

Dann muss man einem Computer beibringen, wie man so ein Problem gut löst.

top

Vorbereitung zum Lösen

Damit ein Computer so ein Problem gut lösen kann, muss man es in eine Tabelle oder Matrix schreiben. Dazu trät man nach rechts die möglichen Personen (um bei obigen Beispiel zu bleiben) auf, und nach unten die möglichen Gruppen.

Wenn eine Person in einer Gruppe ist, schreibt man eine Eins hinein, sonst eine Null.

GruppeAntonBertaClaudiaDanielEva
B111000
B211100
B311010
B400101
B500111

Jetzt kann man das Problem auch umformulieren: Wir wollen Zeilen aus dieser Tabelle so auswählen, dass in jeder Spalte genau eine Eins steht.

top

Lösungsidee

Die Lösungsidee, die Donald E. Knuth entwickelt hat, ist rekursiv, das heißt der Algorithmus verwendet sich selbst, allerdings auf ein kleineres Problem angewandt.

  1. Wenn die Matrix leer ist, wurde eine Lösung gefunden.
  2. Wenn sie nicht leer ist, wähle eine Spalte s.
  3. Wähle zufällig eine Zeile z, die in der Spalte s eine Eins stehen hat. Diese Zeile wird Teil der Zwischenlösung
  4. Für jede Spalte n, in der in der Zeile z eine 1 steht:
    • Lösche die Spalte n aus der Matrix
    • Lösche alle Zeilen, die in der gelöschten Matrix eine 1 stehen hatten.
  5. Wiederhole die Prozedur für die übriggebliebene Matrix

Dieses Verfahren wird Algorithmus X

Schritt Nummer 1 hört sich am Anfang komisch an, ein Beispiel wird es jedoch veranschaulichen.

top

Unser Beispiel mit Algorithmus X gelöst

1. Die Matrix ist nicht leer, wir haben noch keine Lösung gefunden.

2. Wähle eine Spalte. Wie man genau eine Spalte wählt wird später noch erklärt, hier wird erstmal die erste Spalte gewählt, also Anton.

3. Wähle eine Zeile, in der in der gewählten Spalte eine 1 steht. Das könnte B1, B2 oder B3 sein. Mein Zufallsgenerator sagt mir: Wähle Zeile B1. Also gehört B1 zu unserer vorläufigen Lösung.

4. Jetzt kommt das Löschen. Spalte 1 wird gelöscht, und alle Zeilen, die in der Spalte 1 eine Eins stehen haben. Genauso wird für Spalte 2 verfahren, weil in der Zeile B1

Übrig bleibt:

GruppeClaudiaDanielEva
B4101
B5111

Die Tabelle ist ganz schön klein geworden.

Der 5. Schritt sieht das Wiederholen vor, also wiederholen wir:

1. Die Matrix ist nicht leer, wir haben keine Lösung gefunden.

2. Wähle eine Spalte. Sagen wir, Spalte 1 der übriggeblienen Matrix, also Claudia.

3. Wähle eine Zeile, die ein Spalte 1 eine 1 stehen hat. Das trifft auf beide übriggeblienen Zeilen zu, hier wird Zeile B4 gewählt.

4. Entferne die gewählte Zeile, sowie alle Spalten, in denen dort eine 1 steht, sowie alle Zeilen, die in den gelöschten Spalten eine 1 stehen haben

Damit ist die Matrix leer, der Algorithmus ist fertig mit dieser Teillösung. Allerdings ist das Überdeckungsproblem nicht gelöst, weil bisherige Lösung, die Menge aus B1 und B4, die Spalte "Daniel" nicht abdeckt.

Also muss man einen Schritt zurückgehen, und eine andere Wahl treffen (Backtracking).

In diesem Fall war die letzte Wahl die von B4, sie muss also rückgängig gemacht werden und durch die einzig verbleibende Alternative ersetzt werden, B5.

Dann ist das Problem gelöst

top

Diskussion

Wenn man das Beispiel und den Algorithmus vergleicht, merkt man schon, dass die Lösungsidee nicht alle Fälle genau beschreibt.

Für eine genauere Diskussion des Algorithmus sei hier auf das Originalwerk von D. E. Knuth (Englisch, PDF) verwiesen.

Man kann jedoch noch einiges verbesserns, was die Geschwindigkeit angeht, indem man eine geeignetere Datenstruktur wählt.

top

Die Implementierung als zweidimensionales Array hat zwei Probleme:

  1. Das Suchen nach Einsen dauert lange
  2. Das löschen der Zeilen und Spalten daurtert lange

Beide Probleme kann man mit doppelt verlinkten Listen (double linked lists) lösen.

top

Tanzende Zeiger

Um die genialen tanzenden Zeiger zu verstehen, muß man erst etwas über doppelt verlinkte Listen wissen, und wie man daraus spärlich besetzte Matrizen aufbaut.

Doppelt verlinkte Listen

Doppelt verlinkte Listen funktioniert folgendermaßen: Für jeden Eintrag der Liste wird nicht nur der Inhalt gespeichert, sondern auch der linke und rechte Nachbar.

In Pseudocode sieht das so aus:

struct node {
	int data;
	node* left;
	node* right;
}; 

Wobei eine Liste immer aus mindestens einem Element, dem Kopf der Liste, besteht. Wenn die Liste keine Daten enthält zeigen left und right auf den Kopf selbst.

Doppelt verlinkte Liste top

Wenn man ein Zeiger auf ein Element node1 hat, und dieses löschen will, so reicht es, zwei Zeiger neu zuzuweisen ("verbiegen"):

node->left->right = node->right;
node->right->left = node->left; 

Damit zeigt node1 zwar immer noch auf seine Nachbarn, aber wenn sich man vom Kopf der Liste ausgehend an den Zeigern entlang hangelt, kommt man nicht mehr dort hin. Es ist also effektiv gelöscht.

In den Grafiken sind die Daten weggelassen, die in den einzelnen Elementen gespeichert sein können, da sie auf das verbiegen der Zeiger keinen Einfluss haben.

Übrigens kann ein gelöschtes Element sehr einfach wieder hergestellt werden, weil es immer noch Zeiger nach rechts und links besitzt. Dazu reicht

node1->left->right = node1;
node1->right->left = node1;

Hier gibt es eine genauere Beschreibung der doppelt verketteten Liste.

top

Spärlich besetzte Matrizen

Wenn eine Matrix nur wenig Element hat, wie das bei den hier diskutierten Problemen häufig der Fall ist, lohnt es sich, die Matrix aus doppelt verlinkten Listen aufzubauen.

Und zwar verlinkt man die Elemente doppelt nach horizontal und Vertikal, wie in folgender Grafik zu sehen ist:

(Klicken Sie auf das Bild, um eine größere Version zu sehen).

Dabei ist nicht jeder Link einzeln dargestellt, da überall, wo es einen Zeiger in eine Richtung gibt, auch ein Zeiger in die Rückrichtung gibt.

Diese Datenstruktur erlaubt es, schnell die nicht-leeren Einträge zu finden, da nur nicht-leere Einträge (in unserem Beispiel Einsen) darin vorkommen.

Der zweite große Vorteil für unseren Algorithmus ist, dass man Zeilen und Spalten sehr schnell und einfach löschen kann.

Hier der Beispielcode zum Löschen einer Zeile

// Die Struktur sieht so aus:
struct node {
	node* left,
	node* right,
	node* top,
	node* bottom
}
// Lösche Zeile eine Zeile, wenn z der Zeiger zum Kopf der 
// Zeile ist:
for (node i = z->right; i != z; i = i->right){
	i->top->bottom = i->bottom;
	i->bottom->top = i->top;
}
z->top->bottom = z->bottom;
z->bottom->top = z->top;

Wenn man diese Datenstruktur für den oben beschriebenen Algorithmus X verwendet, besteht ein sehr großer Teil des Programms daraus, sich an Zeigern entlangzuhangeln und sie "umzubiegen". Daher der Name "Dancing Links" oder auf Deutsch "Tanzende Zeiger".

top

Sudokus mit "Dancing Links" lösen

Man kann Sudoku als "exact cover"-Problem auffassen, und damit mit "Dancing Links" lösen.

Zunächst muß man sich überlegen, was die Bs sind:

In jede Zelle kann entweder genau eine Zahl 1, 2, 3, ... 9 hinein.

Deswegen ist z.B. B1: "In Zelle (1, 1) kommt Zahl 1 hinein", B2 wäre "In Zelle (1, 1) kommt Zahl 2 hinein", usw., bis hin zu B729: In Zelle (9, 9) kommt Zahl 9 hinein".

Wir haben also 9*9*9 = 729 Mengen, aus denen wir wählen können.

Jetzt muss man nur noch die Sudokuregeln so umformulieren, dass sie ein Überdeckungsproblem darstellen:

  1. In jede Zeile muss jede Zahl einmal hinein (9*9 Bedingungen)
  2. In jede Spalte muss jede Zahl einmal hinein (9*9 Bedingungen)
  3. In jeden Block muss jede Zahl einmal hinein (9*9 Bedingungen)
  4. In jeder Zelle darf jede Zahl nur einmal stehen (9*9 Bedingungen)

Das sind also 9*9*4 = 324 Bedingungen.

Die Matrix des Überdeckungsproblems hat also die Abmessungen 324x729, hat also 236.196 Einträge, von denen die meisten Null sind.

Man kann sich leicht überlegen, dass in jeder Zeile vier Einsen stehen müßen, da jede Zahl in je einer Spalte, Zeile und einem Block vorkommt sowie eine Zelle füllt, die Matrix hat also 729*4 = 2916 nicht verschwindende Einträge, das ist jeder einundachzigste.

In verschiedenen Programmiererforen wird berichtet, dass die Methode der tanzenden Zeiger eine sehr schnelle Methode zum lösen von Sudokus ist, schneller als logikbasierte Solver, die im Zweifelsfall Backtracking verwenden.

top

Referenzen