Tricks im c't-Puzzle

Erst mal möchte ich allen Teilnehmern im Forum ganz herzlich danken. Die Beiträge dort haben mir immer wieder Anregungen und Ansporn gegeben.

In Anlehnung an den vielzitierten X-Trick habe ich meine Tricks mit weiteren Buchstaben belegt. Die meisten sind unter den Puzzlern wahrscheinlich schon allgemein bekannt, aber vielleicht ist doch noch die eine oder andere unbekannte Perle dabei.

Übersicht

Name

Thema

Beschreibung

S (Symmetrie)
Strategie Gesucht wird die "Anzahl gefundener Lösungen, Symmetrien nicht mitgerechnet". Jede Lösung gibt es viermal (durch 180°-Drehungen), wir sollen sie aber nur einmal zählen. Das erreiche ich dadurch, dass ich das "t"-Teil nur in 6 der eigentlich 24 möglichen Orientierungen ins Puzzle einbaue.
E (Endspiel)
Strategie
Ich löse das Puzzle nicht nicht durch Suchen bis zum letzten Platz, sondern "komme der Suche vom Ende aus entgegen". Das heißt, ich berechne vorab eine Tabelle aller möglichen Teile-Kombinationen, die allein innerhalb der letzten Bits (genau: Bits 31 bis 59) liegen, ohne die darunterliegenden Bits zu berühren.
In der normalen Suche gehe ich nur bis Bit 30, und das Komplement der so erreichten Situation muss in der Endspiel-Tabelle stehen, dann habe ich eine Lösung. Wenn nicht: Backtracking.
Innerhalb der Endspiele gibt es Symmetrien, die Tabelle enthält deshalb eine Spalte, die die Lösungsanzahl enthält.
B (Bits)
Strategie
Meine Bitzuordnung ist fest und einfach: x + 3*y + 12*z. Hiermit kann ich Verschiebungen nach rechts, links, vorne, hinten, oben oder unten durch Shift-Operationen abbilden.
X-Trick (eher XT-Trick)
Strategie
Der Hauptteil der Suche geht Bit für Bit vor und belegt ein Bit nach dem anderen, immer mit allen Teilen, die dort hinpassen. Der XT-Trick beginnt mit einem anderen Ansatz. Das "t"- und das "+"-Teil werden in einer äußeren Schleife zuerst gesetzt, bevor im Schleifenrumpf dann die normale positionsbezogene Rekursion beginnt. Seine volle Wirkung entfaltete der Trick bei mir erst, als ich auch den "o"-Trick eingebaut hatte.
H (Hotspot)
Strategie
Angeregt durch einen Forums-Beitrag von "Snej" vom 9.4. (Danke!) arbeite ich nicht mit einer festen Reihenfolge, wie ich die Positionen nacheinander belege, sondern entscheide dynamisch während der Suche, welche Position als Nächste belegt werden soll. Dabei fülle ich immer eine Zwölfer-Ebene nach der anderen und dynamisiere die Reihenfolge nur innerhalb der Ebene. Die Implementation ist eng verzahnt mit dem Plus-Index, Details siehe weiter unten.
o (isoliertes Loch)
Abbruchkriterium
Wenn irgendwo ein isoliertes Loch entstanden ist, kann das nie mehr aufgefüllt werden. Also breche ich die Suche ab, wenn für irgendeine unbelegte Stelle alle 6 Nachbarn (oben, unten, links, rechts, vorne und hinten) belegt sind.
Die Implementation betrachtet alle 60 Bits parellel, bringt die Nachbarn mit den Lücken durch Shift-Operationen (mit Maske wegen der Ränder) zur Deckung und drückt die Logik durch durch Bit-Und, -Oder und -Nicht-Operationen aus.
8 (isoliertes Doppel-Loch)
Abbruchkriterium In Analogie zu "o" teste ich hier, ob es irgendwo 2 benachbarte Löcher gibt, die vollständig eingeschlossen sind. Der Test ist aufwändiger als der "o"-Test und wird nur in der relativ zeitunkritischen Phase der Endspiel-Vorberechnung eingesetzt.
I (Indizierung) Optimierung
Ich führe alle Rotationen und Verschiebungen der Teile vorab aus und lege in der Datenstruktur "Stein" alle möglichen Varianten bereits so indiziert ab, dass die Suche später zu einer gegebenen Position direkt auf die potentiell passenden Teile-Varianten zugreifen kann. Details hierzu siehe "+"-Trick.
F (Filterung)
Optimierung
Nachdem ich die ersten beiden Teile gesetzt habe, entferne ich bei den verbliebenen Teilen alle Varianten, die mit den beiden kollidieren.
+ (Plus-Index)
Optimierung
Ich indiziere die Varianten eines Teils in der Datenstruktur "Stein" nicht nur nach der Position, an die sie passen, sondern auch nach der Belegung der 4 Nachbarn in der jeweiligen Ebene. Die 5 berücksichtigten Plätze bilden ein Plus-Zeichen, daher der Name. Um den Prozessor-Cache besser zu nutzen, habe ich einen platzsparende Implementation in Verbindung mit der Hotspot-Strategie gefunden. Details siehe unten.


Hotspot-Implementation

Ich suche den Hotspot nur innerhalb einer 3*4-Ebene und nehme da die Stelle, die die wenigsten freien Nachbarn hat. Welche das ist, kann ich für jede der 4096 möglichen Belegungen einer Ebene vorab ausrechnen und als Array speichern.

Wichtig in Verbindung mit der Plus-Indizierung ist die Erkenntnis, dass der Hotspot niemals mehr als 2 freie Nachbarn hat. Hätte er mehr, würde eine andere Stelle als Hotspot gewählt: es gibt immer eine Stelle (etwa am Rand) mit 2 freien Nachbarn oder weniger.

Vereinfacht gibt jetzt es ein Array int hotspot[4096], das in der Suche genutzt werden kann, um den Hotspot zu finden:
int nextPos = hotspot[belegung & 0xfff]
In dieser Form gilt das natürlich nur auf Ebene 0, für die höheren Ebenen müssen noch Shifts und Additionen dazu.

Plus-Index

Das Problem, immer viele Teile untersuchen zu müssen, obwohl nur wenige wirklich passen, haben sicher alle Teilnehmer entdeckt.

Wenn ich mich für einen Hotspot entschieden habe, könnte ich jetzt alle Teile-Varianten durchprobieren, die diese Stelle belegen. Das sind aber ziemlich viele, und der Vergleich, ob ein Teil passt oder nicht, wird damit ganz klar zum dominierenden Zeitfaktor.

Im anderen Extrem könnte ich theoretisch für jede mögliche Befüllung des Quaders eine Liste ablegen, welche Teile hinenpassen. Dann hätte ich die perfekte Vorselektion und wäre ich sicher, kein überflüssiges Teil zu probieren. Leider bräuchte ich da 260 verschiedene Listen, und so viel Speicher bekäme man wahrscheinlich auch dann nicht zusammen, wenn man sämtliche jemals produzierten RAMs der Welt kombinieren könnte. Gesucht ist also ein gesunder Kompromiss zwischen Selektionsschärfe und Speicherbedarf.

Mein Kompromiss sieht so aus, Kollisionen mit den 4 Nachbarn rechts, links, vorne und hinten durch eine entsprechende Indizierung der Varianten zu vermeiden. Ich brauche also je Position bis zu 16 verschiedene Listen (benannt A bis P) von Teile-Varianten gemäß Bild und Tabelle:





0

1 HotSpot
2

3



Freie Plätze bei den Nachbarn

Liste

0

1

2

3

Name

Bemerkung

-
-
-
-
A

-
-
-
frei B

-
-
frei -
C

-
-
frei frei D

-
frei -
-
E

-
frei -
frei F

-
frei frei -
G

-
frei frei frei H
Gibt es nicht, Mitte kann kein Hotspot sein.
frei -
-
-
I

frei -
-
frei J

frei -
frei -
K

frei -
frei frei L
Gibt es nicht, Mitte kann kein Hotspot sein.
frei frei -
-
M

frei frei -
frei N
Gibt es nicht, Mitte kann kein Hotspot sein.
frei frei frei -
O
Gibt es nicht, Mitte kann kein Hotspot sein.
frei frei frei frei P
Gibt es nicht, Mitte kann kein Hotspot sein.

Statt wirklich 11 Listen zu führen, die wieder und wieder zum großen Teil dieselben Varianten enthalten und somit nur kostbaren Platz im Prozessor-Cache verschwenden, habe ich mir einen Trick überlegt. Ich schreibe alle für die 11 Listen relevanten Varianten in geeigneter Reihenfolge in eine Gesamtliste hintereinander, und zwar so, dass jede der benötigten Listen A bis M irgendwo als Ausschnitt auftritt. Nachdem ich etliche Blätter Papier mit Tabellen und Skizzen vollgekritzelt hatte, stand die Lösung.

Basis ist, die Teile-Varianten, die eine betrachtete Stelle belegen, auch nach den Nachbarpositionen in Teile-Gruppen a bis m zu sortieren, ganz analog zur obigen Tabelle. Diese Gruppen bilden die wiederverwendbaren Teilstücke der oben geforderten Listen A bis M.

Belegung der Nachbarn durch das Teil

Teile-Gruppe

0

1

2

3

Name

Bemerkung

-
-
-
-
a Passt bei ABCDEFGIJKM
-
-
-
belegt b Passt bei BDFJ
-
-
belegt -
c Passt bei CDGK
-
-
belegt belegt d Passt bei D
-
belegt -
-
e Passt bei EFGM
-
belegt -
belegt f Passt bei F
-
belegt belegt -
g Passt bei G
-
belegt belegt belegt h Passt sicher nicht, wenn Hotspot in der Mitte.
belegt -
-
-
i Passt bei IJKM
belegt -
-
belegt j Passt bei J
belegt -
belegt -
k Passt bei K
belegt -
belegt belegt l Passt sicher nicht, wenn Hotspot in der Mitte.
belegt belegt -
-
m Passt bei M
belegt belegt -
belegt n Passt sicher nicht, wenn Hotspot in der Mitte.
belegt belegt belegt -
o Passt sicher nicht, wenn Hotspot in der Mitte.
belegt belegt belegt belegt p Passt sicher nicht, wenn Hotspot in der Mitte.

Die Frage lautet, wie ich die Gruppen a bis m so hintereinander schreiben kann, das die geforderten Listen A bis M immer einen zusammenhängenden Abschnitt ergeben. Eine Lösung hierzu ist:

d
b
a
c
g
e
a
f
b
j
i
a
c
k
a
e
i
m


A


E


I

M

B


F

K






C




J






D
















G












Ich schreibe bei jedem Teil die Liste der Varianten in der obigen Reihenfolge "dbacgeafbjiackaeim" einfach hintereinander und merke mir in einer zweiten Liste die 19 Start- und Endpunkte der Abschnitte als "Schlüsselpositionen". 

Im Betrieb liefern mir nun 2 Arrays im 4096er-Format die (abhängig von der Ebenen-Belegung und abgestimmt mit dem Hotspot-Array) passenden Start- und End-Schlüsselnummern, über die ich dann auf die Schlüsselpositionen und damit auf den richtigen Listenausschnitt zugreife.

Ergebnis

Interessant ist, was hinten rauskommt: 1364 Mio. platzierte Teile, 5999 Versuche, ein Teil zu platzieren, und 222 Mio. Zugriffe auf die Endspiel-Tabelle. Das Ganze braucht auf meinem Athlon XP2000+ etwa 182 Sekunden.

Da war doch noch was

Was ich sonst noch probiert habe, was aber überflüssig, schlecht oder katastrophal war.

Idee

Beschreibung

Strategie nach Teilen Alle Teile Teil für Teil einbauen und nicht nach Positionen.
Katastrophal.
Sandwich-Strategie
Alle 2-Ebenen-Teile-Kombinationen ausrechnen (hatte ich wegen der Endspiele sowieso) und dann "nur noch" die mittlere Ebene dazwischen mit allen Varianten füllen und jeweils oben und unten sehen, ob es in die Endspieltabelle passt.
Katastrophal.
3 Teile vorab Nicht nur 2, sondern 3 Teile vorab setzen.
Schlecht.
Abbruchkriterium "Schwarz/Weiß" Wenn man den Quader als "3-dimensionales Schachbrett" immer abwechselnd schwarz und weiß färbt, muss jede Lösung genausoviele schwarze wie weiße Elemente haben. Die meisten Teile produzieren je nach Lage entweder einen Schwarz- oder einen Weiß-Überschuss von +1. Ausnahmen sind das 6er-Teil mit +/-2, das Kreuz mit +/-3 und das 4er-Teil mit +/-0. Ich kann eine Situation abbrechen, wenn ihr Schwarz/Weiß-Ungleichgewicht mit den verbliebenen Teilen nicht mehr kompensiert werden kann.
Ist zwar nicht aufwändig zu berechnen, wirkt sich aber erst am Ende richtig aus, wo ich sowieso eine Tabelle habe.
Überflüssig.
Strategie "Erst abstrakt, dann konkret lösen"
Im ersten Schritt alle abstrakten Lösungen finden, wobei nur berücksichtigt wird, ob alle Ebenen zum Schluss 12-mal belegt sind und ob das Schwarz/Weiß-Gleichgewicht gegeben ist (hier ist ein Denkfehler, Schwarz/Weiß ist da noch irrelevant). Im zweiten Schritt zu den abstrakten Lösungen alle konkreten Varianten ermitteln, die dazu passen, ohne zu kollidieren. Leider gab es um einige Größenordnungen mehr abstrakte Lösungen als konkrete, war damit nichts.
Katastrophal.
Abbruchkriterium "Belegbarkeit" Ich kann eine Situation abbrechen, wenn ich irgendwo eine Lücke habe, in die kein Teil mehr hineinpasst, ohne zu kollidieren. Das reduziert den Suchbaum zwar sehr schön, ist aber aufwändig zu rechnen (wenn nicht jemand einen tollen Trick hat). Insgesamt machte es mein Programm nur langsamer.
Schlecht.
Abbruchkriterium "Reihe"
Viele Teile haben irgendwo einen 3 oder sogar 4 Bit langen geraden Abschnitt. Wenn so ein Teil noch gesetzt werden muss, aber keine gerade Lücke mehr da ist, kann ich abbrechen. Der Test ist nicht aufwändig, da er auf 60-Bit-Parallelverarbeitung mit ein paar Shifts zurückgeführt werden kann, aber er trifft so selten, dass er nichts bringt.
Überflüssig.
Hotspot-Strategie über 2 Ebenen Statt nur eine Ebene zu betrachten, wenn ich mich für einen Hotspot entscheide, kann ich die Belegung der darüberliegenden Ebene in die Entscheidung einbeziehen. Damit komme ich dem Ideal "Stelle, wo am wenigsten Varianten passen" definitiv näher, brauche dafür aber entweder Rechenzeit während der Suche oder eine Tabelle, die nicht 4096, sondern 16 Mio. Einträge hat. Unterm Strich war es ein Verlust.
Schlecht.
Segment-Strategie Statt immer nur ein Teil an einen Platz zu setzen, kann man mehrere zusammenfassen (z.B. als 3er-X-Reihe oder 4er-Y-Reihe oder als ganze Ebene) und gemeinsam als "Makro-Teile" setzen. Der Suchbaum wir dadurch zwar um den Faktor 5 bis 20 flacher, aber so unendlich breit, dass der Ansatz sinnlos war.
Katastrophal. 

Ich hoffe, meine Erläuterung haben zur Erhellung, Erbauung oder auch Erheiterung beigetragen und bin gespannt auf die Ideen der anderen.

Ralf Kleberhoff ( ralf03@kleberhoff.de )