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.
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. |
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.
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.
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.
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 )