Hallo in die Runde,
ich habe nach einiger Zeit mal wieder ein Geduldsspiel in die Hände
bekommen, welches ich vor Jahren gekauft, aber nie gelöst habe: 25
Holzwürfel (je 5 Stück von 5 verschiedenen Farben) sind zu sieben 3er
und zwei 2er Blöcken zusammengeklebt. Diese Blöcke müssen nun so in ein
5x5 Quadrat gelegt werden, dass jede Farbe pro Reihe und Spalte genau
einmal vorkommt.
Ich habe zwar schon ein wenig über Algorithmen für "15er-Puzzle"
gelesen, die u.U. durchaus artverwandt sind, kann das aber nicht auf
mein Puzzle anwenden, da ja nicht aus einer Ausgangslage durch eine
bestimmte Zugfolge eine Lösung erreicht werden muss.
Wie geht man das Ganze am Besten an?
Meine bisherigen Ideen:
- Ich platziere Stein 1 an der ersten leeren Position im Feld. Ich
platziere Stein 2 an der nächsten freien Stelle im Feld und prüfe dann,
ob die Bedingungen noch erfüllt sind. Falls ja: weiter. Falls nein,
Stein wieder entfernen und auf die nächste freie Stelle legen. usw. usf.
Ich denke allerdings, dass das nicht unbedingt die optimale Lösung ist,
insbesondere auch, weil u.U. schon die ersten beide Steine so gelegt
werden können, dass die anderen unabhängig von der Farbe allein schon
gar nicht mehr im Quadrat platziert werden können.
- Ich erzeuge mir nacheinander mögliche Anordnungen von 1x1-Würfeln im
Quadrat und prüfe dann, ob es möglich ist, diese Anordnung mit den
vorgegebenen Blöcken abzudecken. Ob das allerdings unbedingt schneller
geht, weiß ich auch nicht
- Generell könnte man ja auch ALLE möglichen Platzierungsreihenfolgen
berechnen und dann Stein 1 an die erste mögliche freie Stelle, Stein 2
erst an die nächste, dann die übernächste. dann die überübernächste
setzen, usw., aber dass dürfte wohl die Variante größten Aufwands
werden... Dazu 9 Schleifen ineinander zu schachteln mag hier zwar
funktionieren, ist aber auch nicht soooo schön, zumal es ja auch die
entsprechenden Abbruchbedingungen geben müsste..
Was meint ihr dazu?
Viele Grüße,
FargoTof
Geht es um dieses Puzzle hier?
https://app.box.com/s/e46s46chzjgtyko1lrtu
Da ist allerdings Aufgabenstellung etwas anders beschrieben als bei dir.
Edit: Ah, doch, die erste Variante, wo die Teile alle rechteckig sind
und zu einem Quadrat zusammengefügt werden müssen, entspricht genau
deiner Beschreibung.
Starte in einem "Eck" da dort bereits gewisse vereinfachende
Einschränkgungen gelten.
Dann verwende eine Breitensuche anstelle eine Tiefensuche.
Da davon auszugehen ist das das "Spiel" von "Menschen" lösbar ist ist
das ein intuitiv gewinnversprechender Ansatz. Außerdem ist zu erwarten
dass es mehrere mögliche Lösungen gibt, die Breitensuche also bereits
recht früh zu einer Lösung führt.
Bei der Breitensuche kann man dann wieder optimieren in dem du die Liste
der möglichen Platzierungen sortierst, z.b. das "Bauteil" das am
wenigsten mögliche Platzierungen zulässt als nächstes verwendest.
FargoTof schrieb:> Wie geht man das Ganze am Besten an?>> Meine bisherigen Ideen:>> - Ich platziere Stein 1 an der ersten leeren Position im Feld. Ich> platziere Stein 2 an der nächsten freien Stelle im Feld und prüfe dann,> ob die Bedingungen noch erfüllt sind. Falls ja: weiter. Falls nein,> Stein wieder entfernen und auf die nächste freie Stelle legen. usw. usf.
Der Begriff für diese Art der universellen Problemlösung ist
'Backtracking'. Das Backtracking Verfahren ist immerhin so mächtig, dass
sich eine ganze Programmiersprache drum herum entwickelt hat - Prolog.
> Ich denke allerdings, dass das nicht unbedingt die optimale Lösung ist
Was ist schon 'optimal'.
Erste Voraussetzung dafür ist es erst mal, dass du überhaupt zu einer
Lösung kommst. Bist du soweit? Hast du ein Programm, welches die
Aufgabenstellung löst?
> insbesondere auch, weil u.U. schon die ersten beide Steine so gelegt> werden können, dass die anderen unabhängig von der Farbe allein schon> gar nicht mehr im Quadrat platziert werden können.
Ja und?
Wenn ein Stein nicht mehr platziert werden kann, dann ist das bisherige
offenbar keine sinnvolle Anordnung, da sich dadurch keine Lösung
erzielen lässt -> 1 Backtrack Schritt, der zuletzt gelegte Stein wird
anders gelegt und nachgesehen, ob es damit möglich ist zu einer Lösung
zu gelangen.
> Was meint ihr dazu?
Probier deine unterschiedlichen Strategien aus, in dem du jeweils ein
Programm dafür schreibst. Dann machst du Untersuchungen darüber, welches
Verfahren wie lange dauert und wie sich das Zeitverhalten entwickelt.
Dazu ist es oftmals auch eine gute Idee, sich im Detail anzusehen, wie
eigentlich das Programm zur Lösung kommt (das Programm gibt zb nach
jedem Schritt eine Zwischenlösung aus und protokolliert mit was es warum
getan hat). Daraus zieht man dann seine Schlüsse und erkennt, wann
welches Verfahren sich wie gut eigenet. So etwas nennt man dann
'Lernen'.
Und ja. Man kann viel davon lernen, indem man dasselbe Problem mit
unterschiedlichen Lösungsstrategien einfach durchprobiert. Niemand hat
seine Karriere damit angefangen, dass er von Haus aus wusste, welches
Verfahren sich wann besonders gut eignet. Erfahrung gewinnt man nur
dadurch, indem man die Dinge macht.
Guten Morgen!
Karl Heinz schrieb:> Erfahrung gewinnt man nur> dadurch, indem man die Dinge macht.
Ja ja, hast ja recht ;)
Tatsächlich habe ich das Spiel gestern relativ schnell gelöst bekommen,
und zwar mit Variante 1. In neun (neun Teile!) ineinander
verschachtelten Schleifen werden nacheinander immer mehr Steine versucht
im Spielfeld zu platzieren und falls nötig wieder rückwärts so weit
entfernt, bis man weiter voran kommt (bzw. letztlich bis zum Ziel).
Interessanterweise habe ich insgesamt acht Lösungen erhalten (da jeweils
vier identisch sind und nur um 0°, 90°, 180°, 270° verdreht also zwei
Lösungen) und das auch wirklich schnell (<10s). Großen
Optimierungsbedarf sehe ich insofern erstmal nicht ;)
Viele Grüße,
FargoTof
FargoTof schrieb:> Interessanterweise habe ich insgesamt acht Lösungen erhalten (da jeweils> vier identisch sind und nur um 0°, 90°, 180°, 270° verdreht also zwei> Lösungen)
... von denen wahrscheinlich die eine das Spiegelbild der anderen ist :)
Hmm. Darauf bin ich noch nicht gekommen. :D Muss aber tatsächlich so
sein. Umso komplizierter dann eigentlich, sich das Rätsel auch genau so
zu überlegen, dass es nur "eine" Lösung gibt...
Es wird sicherlich Konstellationen von Blöcken geben, die mehrere
Lösungen ermöglichen. Sollte ich vllt. mal versuchen herauszufinden ;)
FargoTof schrieb:> Tatsächlich habe ich das Spiel gestern relativ schnell gelöst bekommen,> und zwar mit Variante 1. In neun (neun Teile!) ineinander> verschachtelten Schleifen
Gut.
Jetzt nimmst du dir das ganze nochmal vor und löst die ineinander
verschachtelten Schleifen durch eine rekursive Funktion.
Voila. Du hast die Lösung soweit verändert, dass sie nicht mehr mit
einer ganz bestimmten Anzahl an Klötzchen funktioniert, sondern das du
die Anzahl der Klötzchen auch von einem Benutzer eingeben lassen kannst.
Wenn der die Anzahl wie in der ursprünglichen Aufgabenstellung eingibt,
dann kommt wieder dieselbe Lösung raus. Aber man kann auch andere
Vorgaben machen und zb sich mal ansehen, wieviele Lösungen es für jede
Anzahl an Klötzchen gibt.
Als Anregung könntest du dir zb. mal eine Lösung zum '8-Damen-Problem'
ansehen. Das ist eines der klassischen Beispiele für Backtracking.
(Beim 8-(Schach)Damen Problem geht es darum, 8 Damen auf einem
Schachbrett so anzuordnen, dass keine Dame eine andere schlagen kann)
Naja, die anzuordnenden Blöcke sind ja farblich alle unterschiedlich
zusammengesetzt. Insofern müsste man den Benutzer auch das erstmal
definieren lassen. Das werde ich daher erstmal sein lassen..
Aber dem 8-Damen-Problem werde ich mich durchaus mal annehmen. Gern auch
rekursiv, denn mit rekursion habe ich soweit kaum Berührungspunkte, da
schadet es nicht, sich mal wieder damit auseinanderzusetzen... :)
Ich habe das Puzzle auch mal programmiert und dabei die Klötzchenfarben
wie in diesem Link gewählt:
https://app.box.com/s/e46s46chzjgtyko1lrtu
Das Programm findet in 7,5 ms 320 Lösungen. Wenn von denjenigen, die
durch Rotation, Spiegelung oder Vertauschen der beiden Zweierklötzchen
(die die gleichen Farben haben) ineinander übergehen, jeweils alle bis
auf eine weglässt, bleiben immer noch 20 Lösungen übrig. Ich habe zwar
nicht alle auf Richtigkeit überprüft, es sind aber definitiv mehr als 2
unterschiedliche Lösungen.
@FargoTof:
Sind bei dir die Klötzchen vielleicht anders eingefärbt, dass es bei dir
nur so wenig Lösungen gibt? Wenn ja, würde mich interessieren, wie.
zeigt mal euren Code.
Was mich bei solchen Aufgaben immer nervt, ist, dass man teilweise mehr
Zeit damit verbringt, die Spielmechanik zu implementieren, als den
eigentlichen Lösungsalgorithmus.
Yalu X. schrieb:> Sind bei dir die Klötzchen vielleicht anders eingefärbt, dass es bei dir> nur so wenig Lösungen gibt? Wenn ja, würde mich interessieren, wie.
Ich kann leider schon wieder hier von Arbeit den verlinkten Inhalt nicht
öffnen.. Und ich finde auch grad kein Foto von meinem Puzzle. Ich
liefere aber nachher mal nach, wie die Blöcke bei mir zusammengesetzt
sind.
geht IMHO auch ohne Rekursion oder verschachtelte Schleifen, oder ?
(was nicht heißen soll, dass es einfacher/besser wäre: ich finde die
Rekursion auch einfacher..)
aber nur von der IDEE her:
ist das nicht eine 9 Stellige "Zahl" in einem 100er Zahlensystem?
die man auch als 18-Stellige Zahl im Dezimalsystem Darstellen könnte
also 10^18 (100^9) Möglichkeiten
also einfach eine Schleife von 0 bis 999999999999999999 (was praktisch
ist, weil sich das mit 64bit locker ausgeht..)
viele Möglichkeiten scheiden dann recht schnell aus, weil sich Steine
überlappen oder aus dem Spielfeld ragen.., aber das ist mit anderen
Methoden ja ähnlich)
ich Frage mich aber jetzt wie man so viele Möglichkeiten (oder hab ich
mich verrechnet) in 7,5 ms schafft??
Edit: Nachtrag: wobei sich die Rekursive Methode natürlich VIEL
einfacher optimieren lässt (damit auch die 7,5 ms wohl möglich sind..)
Robert L. schrieb:> ist das nicht eine 9 Stellige "Zahl" in einem 100er Zahlensystem?> die man auch als 18-Stellige Zahl im Dezimalsystem Darstellen könnte
Ich nehme an, die 9 Stellen stehen für die 9 Teile, die platziert werden
sollen und die 100 verschiedenen Ziffern für die 25 möglichen Positionen
multipliziert mit den 4 möglichen Orientiereungen für jedes Teil.
Ja, so kann man das rekursionsfrei und ohne verschachtelte Schleifen
machen. Aber, wie du schon richtig erkannt hast, lässt sich auf diese
Weise der Suchbaum nicht (oder nur sehr umständlich) beschneiden, und
1e18 ist halt schon eine ziemlich große Zahl. Selbst wenn man jede Zahl
(d.h. Anordnung der Teile) in nur 1 ns überprüfen könnte, würde die
komplette Suche über 31 Jahre dauern.
Um den Suchbaum klein zu halten, bin ich bei meinem Algorithmus
folgendermaßen vorgegangen:
Die Teile werden nacheinander jeweils so platziert, dass ihre linke
obere Ecke möglichst weit oben und (bei mehreren Möglichkeiten)
möglichst weit links im Spielfeld zu liegen kommt.
Beispiele:
1
Erstes Teil:
2
————— ————— ————— —————
3
|ooo | |o | |oo | |o |
4
| | |o | | | |o |
5
| | oder |o | oder | | oder | |
6
| | | | | | | |
7
| | | | | | | |
8
————— ————— ————— —————
9
10
Wenn bereits einige Teile platziert sind (Felder mit #):
11
————— ————— ————— —————
12
|#####| |#####| |#####| |#####|
13
|##ooo| |##o | |##oo | |##o |
14
|# | oder |# o | oder |# | oder |# o |
15
| | | o | | | | |
16
| | | | | | | |
17
————— ————— ————— —————
Diese Vorgehensweise hat folgende Vorteile:
- Für das i-te hinzugefügte Teil (i=1..9) müssen maximal 4·(10-i)
Möglichkeiten durchprobiert werden (10-i Teile in 4 Orientierungen),
da die Position bereits festgelegt ist. Eine erschöpfende Suche müsste
also 4⁹·9!≈ 1e11 Kombinationen untersuchen (ein Zehnmillionstel der
obigen 1e18), was auch ohne weitere Optimierung durchaus schon im
Bereich des Machbaren läge.
- Die Teile werden von Anfang an recht dicht gepackt, so dass sich
schnell Situationen ergeben, wo auf Grund unpassender Farben keine
weiteren Teile mehr angelegt werden können, der Suchbaum an dieser
Stelle also abgeschnitten wird.
- Löcher von der Größe eines einzelnen Felds, bei deren Auftreten eine
Fortsetzung Suche Zeitverschwendung wäre, können bei dieser Methode
erst entstehen, wenn das Spielfeld schon fast komplett gefüllt ist.
Man muss sie deswegen nicht explizit vermeiden.
Für jedes hinzugefügte Teil wird sofort überprüft, ob es geometrisch und
farblich passt. Wenn nicht, wird die Suche mit der nächsten Alternative
fortgesetzt. Dadurch reduziert sich die Anzahl der Knoten des Suchbaums
auf 48193.
Um schnell entscheiden zu können,
- welche Teile jeweils noch verfügbar sind,
- auf welchem Feld diese platziert werden sollen und
- welche Farben auf jedem Feld noch erlaubt sind,
habe ich noch etwas Bit-Magic einfließen lassen, so das am Ende wirklich
nicht mehr allzu viel zu rechnen ist.
Der eigentliche Lösungsalgorithmus steckt in der Funktion solve, die
sich in bis zu 9 Ebenen rekursiv aufruft und jeweils beim Erreichen der
letzten Ebene eine Lösung ausgibt.
Die unteren 4 Lösungen sind Spiegelungen der oberen 4 Lösungen an der
Diagonale. Dadurch ist die Lösung bis auf die Spiegelungen tatsächlich
eindeutig.
Karl Heinz schrieb:> Aber man kann auch andere Vorgaben machen und zb sich mal ansehen,> wieviele Lösungen es für jede Anzahl an Klötzchen gibt.>> Als Anregung könntest du dir zb. mal eine Lösung zum '8-Damen-Problem'> ansehen. Das ist eines der klassischen Beispiele für Backtracking.
Wobei man in der Regel nur EINE Lösung sucht und nicht alle, das kann
nämlich die Laufzeit empfindlich erhöhen.
Manchmal interessiert natürlich auch, wieviele Lösungen es gibt.
Gerade beim 8-Damen-Problem ist der Suchraum nicht besonders groß, so
dass man recht fix alle Lösungen suchen kann. Das Progrämmchen im Anhang
braucht dafür gerade einmal 16 µs (ohne Ausgabe). Es liefert 92 Löungen
bzw. 12, die nicht durch Spiegelung oder Rotation aus anderen Lösungen
hervorgehen.
Frage zu den 2 C Progrämmchen hier..
die wurden (wohl nicht nur aus Sicht eines Pascal programmierers ;-) )
wohl eher auf Geschwindigkeit Optimiert, denn auf
Lesbarkeit/Verständlichkeit..
wie "beweist" man hier jetzt, dass die Lösung richtig ist, und es nicht
doch noch Andere Möglichkeiten gibt
(das ist jetzt eher eine allgemein Fragen, nicht speziell auf diese 2
Beispiele gerichtet)
sollte/müsste man nicht erst ein einfaches/verständliches und vor allem
allgemeineres u.U. natürlich langsameres Programm schreiben,
quasi als "Beweis" ?
(vorallem uint64_t del[] ist sehr "woher zum Teufel kommt das")
konkret lässt sich das letzte Programm hier auch schlecht auf andere
Spielfeldgrößen erweitern?
http://de.wikipedia.org/w/index.php?title=Damenproblem&redirect=no
Robert L. schrieb:> Frage zu den 2 C Progrämmchen hier..> die wurden (wohl nicht nur aus Sicht eines Pascal programmierers ;-) )> wohl eher auf Geschwindigkeit Optimiert, denn auf> Lesbarkeit/Verständlichkeit..
Genau so ist es ;-)
Robert L. schrieb:> wie "beweist" man hier jetzt, dass die Lösung richtig ist, und es nicht> doch noch Andere Möglichkeiten gibt> (das ist jetzt eher eine allgemein Fragen, nicht speziell auf diese 2> Beispiele gerichtet)
Ja, wie beweist man, dass ein Programm korrekt ist? Das ist oft sehr
schwierig und deswegen Gegenstand aktueller Forschung.
Dazu braucht man erst einmal eine formale und allgemein akzeptierte
Definition des Problems. Dann kann man versuchen, mit viel Logik vom
Programmcode auf diese Definition zu schließen oder umgekehrt.
Als formale Definition könnte man den C-Code selbst heranziehen, dann
wäre der Beweis der Korrektheit trivial. Nur würde vermutlich kaum
jemand diese Definition des Problems akzeptieren ;-)
Programme in funktionalen Programmiersprachen lassen sich oft (vor
allem, wenn Effizienz unwichtig ist) so schreiben, dass sie weniger wie
ein Programm, sondern eher wie eine mathematische Beschreibung des zu
lösenden Problems aussehen. Ich habe mal das 8-Damen-Problem auf diese
Weise in Haskell formuliert. Zusammen mit den Kommentaren (jeweils
eingeleitet durch "--") sollte der Code auch für Nichthaskellianer
halbwegs verständlich sein. Hat man ihn verstanden, ist schnell
einzusehen, dass dieser Code das Problem korrekt beschreibt.
Anmerkung: Damit der Code ohne große Haskell-Kenntnisse verständlich
ist, ist er sehr geradlinig gehalten, enthält deswegen absichtlich viel
Copy/Paste und ist auf eine feste Zahl von Feldern (8×8) beschränkt. Die
Zusammenfassung der Copy/Paste-Teile würde den Code kompakter und
universeller machen, gleichzeitig aber Wissen über ein paar spezielle
Haskell-Funktionen voraussetzen.
> sollte/müsste man nicht erst ein einfaches/verständliches und vor allem> allgemeineres u.U. natürlich langsameres Programm schreiben,>> quasi als "Beweis" ?
Ja, wenn jeder akzeptiert, dass dieses verständlichere Programm korrekt
ist und es die gleichen Ergebnisse liefert, kann es als Beweis gelten.
Das Haskell-Programm im Anhang kann natürlich auch ausgeführt werden und
liefert dann tatsächlich die gleichen Lösungen wie das obige C-Programm.
Nur braucht es dafür mehr als die 20000-fache Zeit (die mit 360 ms aber
noch lange nicht astronomisch ist).
Das Vorgehen, am Anfang erst einmal einen einfachen und leicht
verifizierbaren Algorithmus zu programmieren und diesen anschließend
schrittweise zu optimieren, propagiert auch Richard Bird in seinem
genialen Buch "Pearls of Functional Algorithm Design" und erläutert es
anhand vieler Beispiele.
Die Verifikation der Optimierungsschritte erfolgt dort aber nicht über
den Vergleich der Programmausgaben, da damit zum einen i.Allg. nicht
alle Fälle abgedeckt werden können und zu anderen der initiale
Algorithmus oft so ineffizient ist, dass er nicht in akzeptabler Zeit
ausgeführt werden kann.
Vielmehr wird jeder Optimierungsschritt als eine Umformung von
Programmcode betrachtet. Jede dieser Umformungen ist so beschaffen, dass
deren Korrektheit beweisbar und in den meisten Fällen sogar
offensichtlich ist. Man kann sie sich ähnlich vorstellen wie die
Umformungen bei der Herleitung einer komplizierten mathematischen
Formel. Viele korrekte Umformungen nacheinander auf ein etwas Korrektes
angewandt ergeben wieder etwas Korrektes.
Am Schluss steht ein effizienter und nachweislich korrekter Algorithmus
in Form eines ausführbaren Programms. Dieser Algorithmus ist naturgemäß
nicht leicht zu verstehen, kann aber konsistent und vollständig durch
das ursprüngliche Programm und die Beschreibung der Optimierungsschritte
dokumentiert werden. Nachträgliche Änderungen oder Erweiterungen sind
leicht möglich, indem man sie erst am übersichtlichen Ursprungscode
vornimmt und danach sämtliche Optimierungsschritte in angepasster Form
wiederholt.
> (vorallem uint64_t del[] ist sehr "woher zum Teufel kommt das")
Das sind Bitmaps der von einer auf einem der 8 Felder der Grundlinie
stehenden bedrohten Felder:
1
10000001 00000010 00000100 00001000
2
01000001 10000010 00000100 00001000
3
00100001 01000010 10000100 00001000
4
00010001 00100010 01000100 10001000
5
00001001 00010010 00100100 01001001
6
00000101 00001010 00010101 00101010
7
00000011 00000111 00001110 00011100
8
D D D D
9
10
00010000 00100000 01000000 10000001
11
00010000 00100000 01000001 10000010
12
00010000 00100001 01000010 10000100
13
00010001 00100010 01000100 10001000
14
10010010 00100100 01001000 10010000
15
01010100 10101000 01010000 10100000
16
00111000 01110000 11100000 11000000
17
D D D D D = Position der Dame
Jedes der acht 7×8-Muster bildet eine von acht 56-Bit-Binärzahlen, die
im Quellcode als Hexdazimalliterale dargestellt sind. Man hätte die
Zahlen vielleicht mit etwas Makro-Magic so hinschreiben können, dass die
obigen Muster deutlicher in Erscheinung treten.
Mit diesen Bitmaps können mit einer einzelnen Operation sämtliche von
einer Dame bedrohten Felder in einem durch eine 64-Bit-Zahl
dargestellten Spielbrett als belegt markiert werden (besser wäre es
eigentlich gewesen, die Bitmaps vorher zu invertieren, dann hätte man
sich bei der Verwendung den ~-Operator sparen können).
Robert L. schrieb:> konkret lässt sich das letzte Programm hier auch schlecht auf andere> Spielfeldgrößen erweitern?
Auf kleinere ja, auf größere nicht ohne weiteres. Das Programm nutzt die
Tatsache, dass man ein komplettes 8×8-Spielbrett in einer
64-Bit-Variablen und damit in einem Register eines 64-Bit-Prozessors
ablegen kann, weswegen sämtliche Operationen darauf sehr effizient
umgesetzt werden können.
Mir ging es primär nicht darum, das 8-Damen-Problem allgemein zu lösen
(dafür gibt es im Netz genügend Beispiele), sondern die Ausführungszeit
auf einen unsinnig kleinen Wert zu optimieren ;-)