Entwickler-Ecke

Algorithmen, Optimierung und Assembler - Nonogramm/Griddler - Solver


LLCoolDave - Do 18.05.06 08:15
Titel: Nonogramm/Griddler - Solver
Wer dieses Japanische Puzzle nicht kennt, findet eine kurze Erklärung auf Wikipedia: http://de.wikipedia.org/wiki/Nonogramm

Diese Rätsel haben mich seit langem in ihren Bann gezogen, und ich würde gerne einfach einmal einen Solver dafür schreiben.

Meine bisherige Idee ist:

-Erstelle eine Liste aller möglichen Anordnungen für eine Zeile/Spalte
-Schmeisse die Einträge aus der Liste, die mit der bisherigen Spielsituation nicht verträglich sind (also ein schon als schwarz eingetragenes Feld wäre weiß etc.)
-Bilde die Schnittmenge aller übrigen Einträge, und trage diese ein (ist also ein Feld in allen möglichen Anordnungen Schwarz, so wird es als schwarz eingetragen)
-Gehe zur nächsten Zeile/Spalte
-Laufe abwechselnd Zeilen und Spalten durch
-Nach jedem Vollständigen Durchgang wird ein Abbild des Spielfeldes gemacht und mit dem vorherigen verglichen. Sind sie identisch ist das Puzzle entweder gelöst oder nicht eindeutig lösbar

Dass dieser Algorithmus zum Ziel führen würde, bin ich mir recht sicher, jedoch würde er auch Ewigkeiten brauchen, weil z.B. bei einem 15*15 Feld die Zeile 2 3 2 sehr viele Möglichkeiten hat, und diese erst alle aufzustellen, um dann eh den Großteil rauszuwerfen, wirkt für mich unpraktisch.

Ach, da fällt mir gerade eine Lösungsmöglichkeit ein: Am Anfang werden einmal alle Möglichen Anordnungen aufgestellt für jede Zeile und Spalte und gespeichert. Diese Menge wird dann bei jedem Zug als Ausgangsmenge genommen. Wenn etwas im vorherigen Zug schon nicht ging, wird es in Zukunft auch nicht mehr gehen. Das sollte das Ganze deutlich beschleunigen.

Fällt vielleicht sonst noch jemandem eine Optimierung oder ein besserer Ansatz ein?


digi_c - Do 18.05.06 08:35

Aha, scheinen ja interessant zu sein.

Ich bin nur ein schlechter Algorithmierer aber ich würde vorher die Elemente ausschließen, die 0 haben und somit unbelegt sind. Ich glaube gut ist es, wenn man bei den Zeilen/Spalten anfängt, die die größten Nummern haben da da ja am wenigsten Permutationen(sind das überhaupt welche?) haben. Dann würde ich deren Nachbarfelder angehen, da diese dann ja auch eindeutiger werden...


alzaimar - Do 18.05.06 09:06

Ganz billig, 'ziemlich' langsam, aber sicher zum Ziel führend ist hier mal wieder das gute, alte Backtracking. Man erzeugt einfach alle Kombinationen und gelangt somit automatisch zum Ziel.

Das ist die gute Seite.

Es kann einige Sekunden dauern. Auch bis zu 2000000000000. Oder noch etwas länger.

Das ist die schlechte Seite.

Jetzt kommt der gesunde Menschenverstand in Form von Heuristiken daher. Gibt es nicht einfache Regeln, die bestimmte Kombinationen von vorneherein ausschließen, oder einzelne Reihen/Spalten schon eindeutig vorbelegen können? Da es sich um ein Logikrätsel handelt, das ein Mensch in endlicher Zeit schaffen kann, und es nur eine Lösung gibt, sollte man so vorgehen:

Ein Feld kann drei Zustände annehmen: Schwarz, Weiss, oder Unbekannt. Alle Felder haben zunächst die Belegung: 'Unbekannt'.

Dann:
Algorithmus "Ermittle Lösung" - Liefert 'Fertig', oder 'es gibt keine Lösung':
0. Wenn alle Felder ausgefüllt sind, ist das die Lösung (Das Resultat ist 'Fertig')
1. Ermittle die Reihe/Spalte, mit der geringsten Anzahl möglicher Kombinationen (=N).
2. Wenn N=0, gibt es keine Lösung (Das Resultat des Algorithmus ist "Keine Lösung").
3. Wenn N=1, färbe die Felder entsprechend und gehe zu 1.
4. Wenn N>1 mache weiter mit Schritt 5.
5. Färbe die Felder nacheinander entsprechend den Kombinationsmöglichkeiten
6. Für jede Kombinationsmöglichkeit, führe "Ermittle Lösung" aus, das Resultat sei R
7. Wenn R='Fertig', ist das Resultat auch 'Fertig' (beende den Algorithmus)
8. Mache den 'Zug' (5) wieder rückgängig und mache weiter der nächsten Kombination.


LLCoolDave - Do 18.05.06 11:56

Ich will ja nichts dagegen sagen, aber ich habe persönlich meine Zweifel ob diese Backtracking variante z.B. auf einem 50x50 Feld auch nur in annähernd brauchbarer Zeit abläuft. Ich könnte sie ja trotzdem mal Probieren, halte aber instinktiv meinen Ansatz für besser, da jedes Feld, das eingetragen wird, auch mit Sicherheit richtig ist, sofern eine Lösung besteht. Bei dem Backtracking Verfahren stellt sich mitunter erst nach einer hohen Tiefe heraus, dass der Ansatz bei der aller Ersten Reihe bereits falsch war, was zu einer Wahnwitzigen Rekursion führen würde. Andererseits ist mein Ansatz wohl bei 50x50 auch nicht alzu schnell. Ich werde mal versuchen, beides nacher umzusetzen, mal sehn was die besseren Resultate liefert.


Horst_H - Do 18.05.06 12:28

Hallo,

wie bei Wiki gesagt gibt es auch nicht eindeutige.
Man denke nur an eine Diagaonale in einem NxN-Feld hat N! Moeglichkeiten.

Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
X1111
1...o
1..o.
1.o..
1o...
Oder
X1111
1o...
1..o.
1.o..
1...0
etc...

Trotzdem witzige Seite
http://puzzle.geoweb.ge/contact.htm

Etwas muss man auch beachten wenn dort eine 10 steht, bedeutet das 10 Felder sind am Stueck belegt, was die Sache extrem vereinfacht, da man nur Startkoordinate Richtung(v,h) Anzahl braucht.
Zwei Zahlen in einer Spalte uebereinader bedeutet, diese sind durch mindestens ein Feld getrennt
3 4 bedeutet das die 4 immer mindestens 4 hinter dem Startpunkt von 3 ist.

Das muss ich mir mal zu Gemuete fuehren

Gruss Horst


LLCoolDave - Do 18.05.06 12:48

Prinzipiell sind nichteindeutige Nonogramme eher sinnlos, weil sie meistens Bilder darstellen, was ja bei nicht eindeutiger Anordnung nicht möglich ist.

Habe gerade mal versucht meinen Ansatz umzusetzen, der ist jedoch an etwas gescheitert, das ich bisher nicht bedacht hatte: Wie erstelle ich am einfachsten alle zulässigen permutationen für jede Zeile/Spalte? Hat mir da jemand nen guten Tipp, mir fällt gerade keine sinnvolle Vorgehensweise ein.


Horst_H - Do 18.05.06 13:18

Hallo,

es gibt keine Permutation einzelner Punkte, was ich im editierten Bereich sagen wollte.
Es gibt nur eine Anordung der Startpunkte
eine zeile einer beliebigen Spalte sei
3 4->das heisst ein 3-er Block wird von einem 4-er Block mit mindestens einem Abstand gefolgt
Gerechnet
X1*_+N1*o+X2*_+N2*0
Wobei N1=3 N2=4 und
X1 0..Summe[i=1..k] Ni - k = Start leere Stellen _
X2 = So, das mindestens eine Stelle hinter N1 und maximal N2 in der letzten Spalte endet.
Fuer 34 ergeben sich nur ein paar Moeglichkeiten = (Anzahl der leeren Stellen)!, hier also 3!=6

Quelltext
1:
2:
3:
4:
5:
6:
7:
  :X??????????
34:000___0000
34:000__0000_
34:000_0000__
34:_000__0000
34:_000_0000_
34:__000_0000


Gruss Horst


digi_c - Do 18.05.06 13:20

Also die "leere" Zeile/Spalte ist ein 1 dimensionales Array. Und dann vielleicht


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
//soviele Durchläufe wie es Zeichenpositionen je Zeile gibt
for i:= 0 to 10 //Anzahl der Positionen
begin
 //
 for j:= 0 to AnzZuBelegendeZeichen
 begin
  if j<10 then 
  begin
    zeile[i]:='#';
  end
  else
  begin //bei Überlauf von vorne beginnen
    zeile[j-i]:='#';  
  end;
 end;
end;


Aber ob das so alles stimmt weiß ich nicht, bin da nicht so begabt ;)
Ich fände es schön, wenn mal gemand einen Solver baut, wo man step by step die einzelnen Schritte sehen kann(welches Feld wird analysiert,...).


alzaimar - Do 18.05.06 13:29

Mit ein bisserl Backtracking, womit sonst?
Sei S die Liste der Blocklängen (z.B. 1,2,4) dann ist es doch so, das erst ein Block der Länge 1 irgendwo ist, dahinter ein Block der Länge 2 und dahinter ein Block der Länge 4, oder?

Ich benötige zur Lösung also nur ein Array of 'Vektor P', wobei P[i] der Startpunkt des Blockes der Länge S[i] ist.


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
Procedure CreateCombinations (S, I,K,Row, P)
Begin
  For J := K to Board.Width - 1 do
    If IsBlock(Board[Row, J], S[i]) Then // An Pos (Row,J) kann ein Block der Länge S[i] sein
      If I = Length(S) Then // Der letzte Block wurde gefunden
        AddToCombinations (P + [J])// Der Lösungsvektor P + [J] ist eine Möglichkeit
     else
       CreateCombinations (S,I+1, J+S[j]+1, P+[J])
End;

Ist natürlich pseudocode: P+[J] soll bedeuten, das an den Vektor P der Wert 'J' angehängt wird.

'AddToCombinations (P : Vektor)' fügt den Vektor P an die Liste der Kombinationen an.
Der Aufruf geht so:
CreateCombinations (S,0,0,Row,[]);

'[]' soll eine leere Liste darstellen.


Horst_H - Do 18.05.06 13:56

Hallo,

ich habe mal (Java aktiviert) http://puzzle.geoweb.ge/online/online_nonogram-001.htm zu loesen probiert.

Die erste Zeile kann man sofort einfuellen , da genau 2 Freistellen vorgegeben sind.
Damit hat man sofort die Startpunkte der ersten Senkrechten Bloecken.
Hinter UND vor jedem Block das Feld in Fahrtrichtung ( zeichenrichtung ;-) ) kann man als nicht belegbar markieren.

Es sieht fuer mich als Nichtspieler so aus, das eine Kombination aus abwechselndem zeilen,spaltenweise eintragen schneller zum Ziel fuehren koennte, was meint der Autor des Threads.
Welche Loesungsstrategien gibt es?

Gruss Horst
Edit

Man kann bei langen Bloecken auch vorher einen sicher Bereich festlegen.
Wenn 10 STellen frei verfuegbar sind und ein 9er Block dahinein soll, kann man die erste und letzte Stelle als ungewiss markieren und die acht mittendrin als sicher.


Horst_H - Do 18.05.06 16:30

Hallo,

man kann sogar eine Studienarbeit daraus machen:
http://page.mi.fu-berlin.de/~schindle/projekte.html

Gruss Horst


LLCoolDave - Do 18.05.06 16:35

Also, nochmal zur Erklärung, das wäre mein Lösungsansatz:

Zunächst wird für jede Zeile und Spalte eine Liste aller Möglichen Einträge erstellt.
Dann werden abwechselnd alle Zeilen, dann alle Spalten, durchgelaufen. Tritt nach einem vollständigen Durchlauf keine Änderung mehr ein, ist der Solver am Ende seiner Kräfte angekommen: Entweder, weil das Puzzle fertig ist, weil es nicht eindeutig lösbar ist, oder weil mein Verfahren vielleicht doch nicht 100% funktioniert.
Dabei geschieht bei jeder Zeile/Spalte folgendes: Die einzelnen Elemente der Zeile werden durchlaufen. Ist bereits etwas eingetragen, werden alle Möglichkeiten aus der Liste herausgeschmissen, die in diesem Feld etwas anderes stehen haben, da diese für eine Lösung nicht in Frage kommen. (Alle im Spielfeld markierten Felder haben sich zwingend logisch ergeben). Ist die Zeile so komplett durchlaufen, befinden sich in der Liste nur noch Möglichkeiten, die auf die aktuelle Spelsituation passen. Anschließend wird geprüft, ob für eines der freien Felder in allen Möglichkeiten der selbe Wert steht. Wenn ja, ergibt sich dieser als logisch zwingend für das Feld und wird im Spielfeld eingetragen.

So weit, so gut. Das einzige was mir bei diesem Ansatz Probleme bereitet, ist das Erstellen der Liste mit allen Möglichkeiten. Ich hab mir dafür zwar im Chemieunterricht etwas überlegt, das wirkt aber so ineffektiv und langsam, dass ich es vorerst für mich behalten will. Der Ansatz mit den Vektoren scheint interessant zu sein, ich kann ihn nur leider nicht ganz nachvollziehen, eine Erklärung wäre nett, weil das ganze recht einfach und effektiv zu sein scheint.

PS: Was auf Wikipedia nicht erwähnt wurde, es gibt auch mehrfarbige Nonogramme, mit etwas anderen Regeln. Ich habe jedoch berechtigte Zweifel, das mein Lösungsansatz auch in diesem Fall funktioniert, da die Mehrfarbigkeit auch andere logische Ansätze erlaubt, die mein Lösungsalgorithmus schlicht übersieht. (Z.B.: Wenn es nur ein einziges rotes Feld gibt, ist dies durch den Schnittpunkt der Zeile und Spalte, die den Roten Block enthalten, eindeutig bestimmt. Mein Algorithmus übersieht meines erachtens diese Begebenheit schlicht und einfach. Mitunter ergibt sich aber nur durch eintragen dieses Feldes ein logisch zwingender Lösungsweg für das Nonogramm.)

Edit: Mmh, hab die Studienarbeit nur überflogen, aber meinen Lösungsansatz hab ich darin nicht entdeckt. Daher interessiert mich nun erst recht, ob mein Algorithmus alle eindeutigen Nonogramme löst.


Horst_H - Do 18.05.06 17:13

Hallo,

http://www.comp.lancs.ac.uk/computing/users/ss/nonogram/fmt2.html findet sich vielleicht ein Dateiformat, mit dem man etwas fuer Testprogramme anfangen kann, sodass man hier etwas einheitliches verwendet.
Damit hat man dann einheitliches Testnonogramme bis 100x100 :
http://www.comp.lancs.ac.uk/computing/users/ss/nonogram/puzzles.html

Somit kann man sich jetzt ein internes Datenformat ueberlegen.
maxrule gibt ja maximale Anzahl von Abschnitte in Zeile oder Spalte an.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
tRule = record 
          rules    :array[1..maxrule] of integer;
          startpt  :array[1..maxrule] of integer;
          rulesCnt : integer;  //Wieviele sind es wirklich
          fixedlength: integer;//fest eingefuegt 
          fixedrule [1..maxrule] of boolean;//fest eingefuegt 
       end;

tSpalte = array[1..weidth] of tRule;
tZeile  = array[1..height] of tRule;

Das Spielfeld hat mehrere Moeglichkeiten:
unbelegt,garantiert belegt,garantiert frei

Hat jemand dazu ergaenzende Vorschlaege?

Gruss Horst


alzaimar - Do 18.05.06 17:35

user profile iconLLCoolDave hat folgendes geschrieben:
ADer Ansatz mit den Vektoren scheint interessant zu sein, ich kann ihn nur leider nicht ganz nachvollziehen, eine Erklärung wäre nett, weil das ganze recht einfach und effektiv zu sein scheint.

Kein Problem. Ich mach das mal für Reihen. Für Spalten ist es ja das Selbe in Grün.
Meine Routine findet nun alle Kombinationen, in einer Reihe die Blöcke mit einer bestimmten Folge von Blocklängen zu setzen. Die Blocklängen sind in B, wobei B ein Vektor (Array) ist. Sagen wir, B= (1,2,4). Wir suchen also alle Möglichkeiten, in einer Reihe (Breite = W) diese Blöcke zu plazieren.
Eine Möglichkeit ist P, wobei P wieder ein Vektor ist. z.B. P= (0,3,9). Dann fängt der 1er Block bei Spalte 0 an, der 2er Block bei 3 und der 4er Block bei 9. Daneben gibt es ja noch weitere Möglichkeiten, die stehen dann in dem Lösungsvektor L hintereinander. Z.B.:
L = ( (0,3,9), (1,4,9) )
Es gibt dann zwei Möglichkeiten in dieser einen Reihe die Blöcke mit der Länge 1,2 und 4 zu plazieren, nämlich an den Positionen 0,3 und 9 sowie an 1,4 und 9.

Ich probiere einfach alle Möglichkeiten, den 1.Block zu plazieren. Für die rekursive Lösung ersetze ich 'alle' durch 'nächste'. Also nochmal...

Ich suche die 'nächste' Möglichkeit, den 1.Block zu plazieren. Ich fange also bei 0 an. Nehmen wir an, an Position J habe ich einen freien Platz. Also kommt J in meinen Lösungsvektor P an die 1.Stelle, denn der 1.Block (Länge = 1) könnte an Position J kommen. Alle folgenden Blöcke müssen mindestens ab Position J+<Länge 1.Block>+1 anfangen. Wenn ich keinen folgenden Block mehr habe, habe ich ja eine Möglichkeit gefunden.

Wie man sieht, sagt ein Code mehr als 1000 Worte:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
Procedure FindRowCombinations (B : TPosArray; aRow, aBlockIndex, aStartPos : Integer; P : TPosArray);
Var
  J, iThisBlockWidth : Integer;

Begin
  iThisBlockWidth := B[aBlockIndex]; // so einen Block wollen wir plazieren
  For j:=aStartPos To Board.Width - iThisBlockWidth Do
     If Board.CanPutABlockAtPos (J,iThisblockWidth) Then Begin
       P[aBlockIndex] := J;
       If aBlockIndex = Length (B) Then Begin // Der letzte Block wurde plaziert
         Solutions.Add (P);  // Also ist P eine Möglichkeit und kommt zum Lösungsvektor
       End
       Else // Ansonsten versuchen wir, den nächsten Block zu plazieren
         FindRowCombinations (B, aRow, aBlockIndex + 1, J + iThisblockWidth + 1, P);
     End
End;


Ehrlich gesagt glaube ich, das mein Ansatz besser ist. Aber nur deshalb, weil Ich bei deinem Ansatz nicht durchblicke ;-). Vermutlich gibt es immer eine Reihe/Spalte, die eindeutig zu belegen ist. Nachdem dies geschehen ist, fängt mein Algo ja von vorne an und sucht wieder eine R/S, die eindeutig zu belegen ist. Zum Schluss bleiben (bei einem gemeinen Nonogramm) vielleicht ein paar R/S übrig, die alle mehre Möglichkeiten haben, die Gesamtanzahl der zu prüfenden Möglichkeiten dürfte aber minimal sein. Und erst dann setzt das Backtracking an. Die gleiche Strategie führte bei Sudoku zum Ziel. Das Backtracking wurde da nur bei mehrdeutigen Sudoku's (die per definitionem Keine sind) angeworfen.

Lass uns doch einfach die Lösungen vergleichen.


F34r0fTh3D4rk - Do 18.05.06 17:46

das wollte ich als nächstes projekt, verdammt, da war einer schneller :)


LLCoolDave - Do 18.05.06 17:52

Schneller? Ich hab keine einzige Zeile Code :P

Naja, erst dachte ich ich habs verstanden, und jetzt bin ich wieder verwirrt. Ist das da jetzt eine Lösung zum finden aller möglichen Anordnungen in einer Spalte/Zeile oder gleich eine Idee für das Lösen eines ganzen Nonogramms?

Naja, meine Idee war folgende:

Es wird festgestellt, wie viele leerstellen in einer zeile/spalte zu vergeben sind. Einfach Zeilenlänge - Summe der Blocklängen - Anzahl der Blöcke +1. Dann wird diese Restleerzeichenzahl einfach in allen möglichen Summenzerlegungen auf die Anzahl der Blöcke + 1 möglichen Plätze verteilt, in denen sie stehen können. Daraus kann ich mir dann auch die Liste aller Möglichkeiten generieren.

Nur die Startstellen der Blöcke anzugeben ist für meinen Algorithmus unbrauchbar, aber beim Eintragen in die Lösungsmenge kann das ganze ja bereits umgeschrieben werden, finde deinen Ansatz jetzt auch besser, auch wenn ich ih noch nicht ganz verstanden hab (hab heute Denkblockade XD). Damit mach ich mich vielleicht nacher nochmal ran an das Problem.


Jetstream - Do 18.05.06 19:41

Moin moin,
ich versuch mich auch mal an deinem Algo, ist natürlich nur ein Ansatz :)

Gehen wir mal davon aus, wir haben ein zweidimensionales Array aus Integern von der Größe
des Feldes, das die möglichen Färbungen darstellen. Eine Null steht dabei für "nicht eingefärbt",
eine Eins für "eingefärbt" und eine Zwei für "steht noch nicht fest".

Dann brauchen wir noch zwei Arrays, die die Abschnitte angeben, dort sollte sowas wie "4 2 1" oder "3" stehen können.
Dafür könnte man zwei Arrays aus LinkedLists, oder einfach wieder zweidimensionale Arrays aus Integern nehmen.

Pseudo-Code:

Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
Schleife: Wiederhole das so lange, bis sich nichts mehr verändert hat:

  Schleife:
  Gehe alle Zeilen/Spalten durch, wenn die Anzahl der eingefärbten Felder 
  der Summe der Elemente des darauf bezogenen Arrays entspricht, 
  setze alle ungewissen Felder auf Null.

  Schleife:
  Gehe alle Zeilen/Spalten durch, wenn die Anzahl der ungewissen Felder 
  der Summe der Elemente des darauf bezogenen Arrays minus der bereits eingefärbten entspricht, 
  setze alle ungewissen Felder auf Eins.

  Schleife:
  Suche nach fertigen Abschnitten im Feld, und setze das Feld hinter dem Abschnitt auf Null.


Ich weiss nicht, ob mein Ansatz bereits jedes Feld eindeutig löst, aber wenn der Code erst mal implementiert ist
und du ihn an ein paar Beispielen getestet hast, wirst du schnell eventuell vorhandene Grenzen bemerken.

mfg Jetstream


Horst_H - Fr 19.05.06 09:53

Hallo,

nochmals zur Vereinheitlichung, damit man kooperieren kann, sollte eine sinnhafte Datenstruktur erzeugt werden.
Statt rule koennte man auch Block sagen:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
maxBlkcnt :integer = const
tBlock = record
            BlkLength   : integer;
            StartPos,       //als Laufvariable von min bis max
            MinStartPos,
            maxStartPos :integer;
            Fixed       : Boolean; //oder MinStartpos=MaxStartPos machen
          end;
tRowCol = record    
             Blocks    : array[1..maxBlkcnt] of tBlock;   
             BlockCnt  : integer;
             Blockfixed: array[1..Blkcnt] of boolean;
          end;   

tSpalte = array[1..weidth] of tRowCol;   
tZeile  = array[1..height] of tRowCol;


Man kann ja auch statt array[1..maxBlkcnt] of ?? mit dynamischen Feldern arbeiten.

In der Studienarbeit wird auch dass Programm des italienischen Mathematik Professors Maurizio Paolini erwaehnt:
Zitat:

Der Algorithmus sucht sich die Zeilen oder Spalten mit den geringsten Möglichkeiten der Verteilungen der Blöcke(-> moeglichst wenig >freie< Zwischenraeume(Weite- (Gesamtlaenge der n Bloecke + (n-1){Zwischenraeume})und am besten mit geringer Blockanzahl) und sucht dann Überschneidungen, die keinen Widerspruch zu schon gesetzten Einträgen ergeben.


Dies scheint mir eine gute Ausgansgsbasis, um anschliessend Brute-Force darauf loszulassen.

Gruss Horst


alzaimar - Fr 19.05.06 11:41

Moin

Mein Ansatz, die möglichen Kombinationen zu finden, ist Müll. Vergesst ihn. Ich poste abends vielleicht eine erste Lösung...


LLCoolDave - Fr 19.05.06 13:46

Horst_H: Für meinen Ansatz ist die Datenstruktur der Blöcke absolut und völlig ungeeignet. Ich brauch für eine auch nur annähernd effektive Arbeitsweise einen Array für die einzelnen Blöcke in der Spalte/Zeile.

Und da ich das Gefühl habe, das wir sowieso aneinander vorbeiarbeiten, ich will KEIN Bruteforce verwenden. Mir geht es nicht darum, am Ende die Lösung zu haben, sondern ich will heraus finden, ob mein Ansatz a) Die meisten Nonogramme löst bzw b) Alle logisch zwingenden Nonogramme löst. Bei nicht eindeutigen Nonogrammen besteht bei mir absolut kein Interesse, diese sind allerhöchstens aus theoretischer Sicht von Interesse, praktisch finden sie keine Anwendung als Rätsel.

Daher sehe ich in der von Horst vorgeschlagenen Datenstruktur für mich keinerlei Nutzen. Ich habe nichts dagegen, wenn ihr auch an Lösungsansätzen arbeitet, so können wir am Ende unsere Resultate vergleichen, aber mir geht es hauptsächlich darum, zu testen, ob dieser Algorithmus, den ich seit einem guten Jahr im Kopf habe, auch die richtigen Resultate liefert.

Daher vereinfache ich meine Frage bezüglich der Permutationen auf folgende Aufgabenstellung:

Gegeben seien zwei Zahlen m und n. m soll nun auf alle möglichen Weisen in n Summanden (jeder dabei >= 0) zerlegt werden, und alle Permutationen der möglichen Zerlegungen zurückgegeben werden.

Beispiel: m=4 n=3

L=((4,0,0),(0,4,0),(0,0,4),(3,1,0),(3,0,1),(1,3,0),(1,0,3),(0,3,1),(0,1,3),(2,2,0),(2,0,2),(0,2,2),(2,1,1),(1,2,1),(1,1,2))

Hoffe ich habe keine vergessen, aber das Prinzip sollte klar sein.

Edit: Dank eines Freundes habe ich nun einen Algorithmus, der mir alle Möglichkeiten ausgibt, und soweit ich mich an den Algorithmus weiter oben erinnern kann, funktioniert er ähnlich. Angesichts der Tausenden von Möglichkeiten bei größeren Rätseln, ist evt. nur das Speichern der Anfangsstellen eines Blocks effektiver, auch beim berechenen, sollte aber trotzdem am Ende in einen Array oder String umgewandelt werden zwecks Auswertung. Das sind aber Kleinigkeiten, an denen ich auch später noch feilen kann. Laufzeit scheint auch gut zu sein, braucht ca. 0.004 s zum berechnen aller 330 Möglichkeiten für (2,2,1,5) bei einer Zeilenbreite von 20. Jetzt mach ich mich mal an den Rest des Algorithmus.

Edit2: So, das ganze funktioniert jetzt, ist nur alles andere als Benutzerfreundlich. Dafür ist es einigermaßen schnell, ein 20x20 Nonogramm (das schwerste auf Griddlers.net) wird in etwa einer halben Sekunde gelöst, das Problem entsteht bei größeren: Bei dem Berechnen von allen Möglichkeiten für (1,2,1,1,1,1) bei Spaltengröße 30 steigt mein programm mit einer Out of Memory Meldung aus, also muss ich schauen ob ich da vielleicht was verbessern kann, ich kann euch nacher auch mal den Quellcode geben, falls ihr mir dabei helfen wollt. Ich schau nacher mal nach 30x30 Nonogrammen, die keine Zeilen mit 120.000 Möglichkeiten haben ;) Mal schaun wie mein Programm da abschneidet.


Horst_H - Sa 20.05.06 09:24

Hallo,

wo ist Dein Ansatz nicht genau Brute force?
Du versuchst alle Kombinationen (mindestens einer Zeile,Spalte) vorab zu bestimmen und zu verifizieren.
Beim Backtracking wird eine Kombination nach der anderen erzeugt, was einfach speichersparend ist und frueher bei falschen Loesungen abbricht.
Ich hab doch oben mal geschrieben das die Anzahl der Anordnungen pro Zeile,Spalte = (Anzahl der beweglichen Leerstellen)! ist.
Aber poste doch mal Deine Loesung, und es waere schoen, wenn man sich zumindest auf das Fileformat oben einigen koennte :-)

Gruss Horst


delfiphan - Sa 20.05.06 13:47

Beim Spiel kann man doch ganz sicher ohne grosses Probieren viele Fälle direkt aus der Logik ableiten. Es gibt bestimmt viele Fälle die man einfach so mal algorithmisch relativ einfach lösen kann. Einfaches Beispiel: Wenn die Zeilenlänge 20 ist und man hat eine einzelne "12" auf der Seite, so kann man doch daraus direkt schliessen, dass die mittleren 4 schwarz eingefärbt sein müssen. Oder wenn die Zahlen links plus die Anzahl der Zahlen minus eins genau die Zeilenlänge gibt kann man die Zeile grad einfärben und die entsprechenden Zahlen streichen.
Ich kenne das Spiel jetzt auch nur grad vom Forum hier aber solche "Tricks" müsste es eigentlich viele geben, sonst könnte man es ja nicht von Kopf lösen. Es wäre ja irgendwie langweilig wenn es viele Punkte gäbe wo man mit der Logik einfach nicht mehr weiter kommt und man einfach so raten muss. Es gibt sicher so Fälle, die Enden dann aber hoffentlich bald in einer Inkonsistenz... Aber wer weiss, vielleicht ist ja genau das, was ein schweres Nonogramm "schwer" (oder besser gesagt mühsam) macht.
Ein Programm, welches zusätzlich für jeden Schritt noch eine Begründung ausgibt ist doch viel interessanter als eines, welches das Spiel einfach so durch pures Backtracking oder probieren löst. Da könnte man ja gleich einfach alle Möglichkeiten durchprobieren. Das finde ich aber weniger spannend.

PS: Es geht glaube ich auch nicht nur ums Einfüllen. Man kann einem Feld auch ein "sicher weiss" zuordnen.


LLCoolDave - Sa 20.05.06 16:29

Horst: Nein, mein Ansatz stellt für mich kein Brute Forcing dar. Brute Force wäre z.B. das erstellen und prüfen aller 2^(zeilenzahl*spaltenzahl) möglichen Nonogramme, und dann prüfen, ob es zu dem gegebenen Spielfeld passt. Auch die intelligentere Variante, nur Nonogramme zu erstellen, die die richtige Anzahl an schwarzen Feldern haben, ist immer noch pures rumprobieren und raten. Auch das Backtracking ist für mich ein gewisses Bruteforce, wenn auch etwas zielgerichteter. Trotzdem wird dort erst mal etwas probiert, und wenn es auf einen Wiederspruch stößt, kann da ja wohl nicht das richtige gewesen sein.

Mein Ansatz hingegen ahmt die menschliche Vorgehensweise nach. Zunächst einmal sind für eine einzelne Zeile/Spalte alle möglichkeiten möglich. Jedoch gibt es bei bestimmten Zeilen/Spalten, wie von delphifan schon erwähnt, Felder, die mit Sicherheit eingefärbt sind. Das ist der Angriffspunkt, mit dem ein Mensch an ein solches Rätsel herangeht, und genau das selbe macht mein Programm auch. Wenn man alle möglichkiten einer solchen Zeile/Spalte miteinander vergleicht, stellt man fest, das eben in allen Möglichkeiten bestimmte Felder immer eingefärbt sind. Da es für diese dann keine andere Möglichkeit mehr gibt, werden sie eingefärbt. Nach eben diesem Verfahren geht mein Programm vor. Es probiert also nicht willkürlich herum, sondern geht strikt nach dem Ausschlussverfahren logisch vor. Das es dabei nicht unbedingt effizient arbeitet, ist mir klar, aber ich würde das sicher nicht als Brute Force bezeichnen.

Nunja, hier mal mein Code, wer Verbesserungsvorschläge hat (die Grundidee des Algorithmus würde ich gerne beibehalten, mein Ziel ist es wie schon erwähnt nicht, einen effizienten Solver zu schaffen, sondern einen, der die menschliche Logik nachahmt) soll sie ruhig nennen, schneller unf effektiver darf das Ganze schon laufen, vielleicht kriegen wir das ja noch so getunet, das auch ein 50x50 Nonogramm mit meinen bescheidenen 512MB Ram zu schaffen sind, auch wenn es ne Stunde oder so dauert.


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
158:
159:
160:
161:
162:
163:
164:
165:
166:
167:
168:
169:
170:
171:
172:
173:
174:
175:
176:
177:
178:
179:
180:
181:
182:
183:
184:
185:
186:
187:
188:
189:
190:
191:
192:
193:
194:
195:
196:
197:
198:
199:
200:
201:
202:
203:
204:
205:
206:
207:
208:
209:
210:
211:
212:
213:
type TSArray = Array of string;
type TIArray = Array of integer;
type TFeld = Array of Array of char;
type TSpielfeld = record
     Spalten: Array of string;
     Zeilen: Array of string;
     Sppos: Array of TSArray;
     Zlpos: Array of TSArray;
     Feld,FeldBackup: TFeld;
     spaltenzahl,zeilenzahl: integer;
     end;

//Gibt die am weitesten rechts liegende Startposition des Blocks m[0] bei der Blockliste m und der Feldbreite n zurück (BSP: GetMaxPosition(6,(1,2)) = 2)
function GetMaxPosition(n: integer; m:TIArray):integer;  
var i,j: integer;
begin
j:= -1;
for i:=0 to High(m) do j:=j+m[i]+1;
result := n - j;
end;

//Dafür gibts zwar auch in Delphi eine function, aber mir ist der Name nicht eingefallen ;)
function MakeString(n: integer; c:char):string;
var i: integer;
begin
result := '';
for i:=1 to n do result := result+c;
end;

//Fügt den string s an das Ende des TSArray ouput
procedure AddString(s: stringvar output: TSarray);
begin
setlength(output,length(output)+1);
output[high(output)] := s;
end;


//Gibt einen TSArray zurück, der s+jeden String aus input enthält
procedure AddStrings(s: string; input: TSarray; var output: TSarray);
var i: integer;
begin
for i:=0 to high(input) do
  begin
  Addstring(s+input[i], output);
  end;
end;

//Gibt für die Blöcke m und der Feldbreite n alle möglichen Anordnungen zurück
function positions(n: integer; m: TIArray):TSArray;
var i: integer;
    tempsarray: TSArray;
begin
setlength(tempsarray,0);
for i:=0 to GetMaxPosition(n,m) do
  begin
    if length(m) > 1 then
    Addstrings(MakeString(i,'1')+MakeString(m[0],'2')+'1',positions(n-i-m[0]-1,copy(m,1,length(m))),tempsarray)
    else Addstring(MakeString(i,'1')+MakeString(m[0],'2')+MakeString(n-i-m[0],'1'),tempsarray);
  end;
result := tempsarray;
end;

//Macht ein Backup eines TFeld (zum Vergleich, ob sich etwas geändert hat)
procedure Backup(from: TFeld; var backup: TFeld);
var i,j: integer;
begin
for i:=0 to high(from) do
  for j:=0 to high(from[0]) do
    backup[i,j] := from[i,j];
end;

//Vergleicht zwei TFeld
function CompareFields(field1,field2: TFeld): boolean;
var i,j: integer;
begin
result := false;
for i:=0 to high(field1) do
  for j:=0 to high(field1[0]) do
    if not(field1[i,j] = field2[i,j]) then exit;
result := true;
end;

//Bisher hab ich noch keine Brauchbare Eingabe, läuft derzeit über Strings, daher diese Umwandlungsfunktion
function stringtoTIArray(s: string):TIArray;
var tempiarray: TIArray;
begin
  setlength(tempiarray,0);
  while pos(',',s) > 0 do
  begin
    setlength(tempiarray,length(tempiarray)+1);
    tempiarray[high(tempiarray)] := strtoint(copy(s,1,pos(',',s)-1));
    delete(s,1,pos(',',s));
  end;
  Result := tempiarray;
end;

//Initialisiert das Spielfeld
procedure Initialize(var Spielfeld: TSpielfeld);
var i,j: integer;
begin
with Spielfeld do
begin
  spaltenzahl := strtoint(Form1.Edit1.text);
  zeilenzahl := strtoint(Form1.Edit2.text);
  Setlength(Spalten,spaltenzahl);
  Setlength(Zeilen,zeilenzahl);
  Setlength(Sppos,spaltenzahl);
  Setlength(Zlpos,zeilenzahl);
  Setlength(Feld,spaltenzahl,zeilenzahl);
  Setlength(FeldBackup,spaltenzahl,zeilenzahl);
  for i:=0 to zeilenzahl-1 do for j:=0 to spaltenzahl-1 do
    begin
    Feld[j,i] := '0'; FeldBackup[j,i] := '1';
    end;
  for i:=0 to spaltenzahl-1 do
    begin
    Form1.Caption := 'Initialisieren: Spalte '+inttostr(i+1);
    Application.Processmessages;
    if abbruch then exit;
    Spalten[i] := Form1.Memo1.lines[i];
    Sppos[i] := positions(zeilenzahl,stringtoTIArray(Spalten[i]));
    end;
  for i:=0 to zeilenzahl-1 do
    begin
    Form1.Caption := 'Initialisieren: Zeile '+inttostr(i+1);
    Application.Processmessages;
    if abbruch then exit;
    Zeilen[i] := Form1.Memo1.lines[i+spaltenzahl];
    Zlpos[i] := positions(spaltenzahl,stringtoTIArray(Zeilen[i]));
    end;
end;
end;

//Entfernt ein Element aus einem Array, gibt sicher auch schon ne procedure dafür
procedure removearray(i: integer; var SArray: TSArray);
var j: integer;
begin
for j:=i to high(SArray)-1 do
  SArray[j] := SArray[j+1];
Setlength(Sarray,length(SArray)-1);
end;

//Entfernt alle Möglichkeiten aus dem TSArray, die nicht zu der derzeitigen Spielsituation (s) kompatibel sind
procedure RemoveSolutions(s: stringvar SArray: TSArray);
var i,j: integer;
begin
for i:=1 to length(s) do
  if not (s[i] = '0'then
  for j:=high(SArray) downto 0 do
    if not (SArray[j][i] = s[i]) then removearray(j, SArray);
end;

//Erstellt basierend auf der derzeitigen Situation (s) und dem TSArray aller möglichkeiten einen String, der Felder enthält, die sich als logisch zwingend erwiesen haben
function CreateEntryString(s: string; SArray: TSArray): string;
var i,j: integer;
    b: boolean;
    tempstring: string;
begin
tempstring := '';
for i:=1 to length(s) do
  if s[i] = '0' then
  begin
  b := true;
  for j:=1 to high(SArray) do
    if not (SArray[j][i] = SArray[j-1][i]) then
    begin
      b := false;
      break;
    end;
  if (b) and (length(SArray)>0then tempstring := tempstring + SArray[0][i] else tempstring := tempstring + '0';
  end else tempstring := tempstring + '0';
result := tempstring;
end;

//Eigentliche Lösungsschleife
procedure Solveloop(var spielfeld: TSpielfeld);
var counter,i,j: integer;
    tempstring: string;
begin
counter := 0;
with Spielfeld do
begin
  while CompareFields(Feld,FeldBackup) = false do
  begin
    inc(counter);
    Form1.Caption := 'Durchgang '+InttoStr(counter);
    Application.ProcessMessages;
    Backup(Feld,FeldBackup);
    for i:=0 to high(Spalten) do
      begin
      tempstring := '';
      for j:=0 to high(Feld[i]) do
        tempstring := tempstring + Feld[i,j];
      RemoveSolutions(tempstring,Sppos[i]);
      tempstring := CreateEntryString(tempstring,Sppos[i]);
      for j:=1 to length(tempstring) do
        if not(tempstring[j] = '0'then
          Feld[i,j-1] := tempstring[j];
      end;
    for i:=0 to high(Zeilen) do
      begin
      tempstring := '';
      for j:=0 to high(Feld) do
        tempstring := tempstring + Feld[j,i];
      RemoveSolutions(tempstring,Zlpos[i]);
      tempstring := CreateEntryString(tempstring,Zlpos[i]);
      for j:=1 to length(tempstring) do
        if not(tempstring[j] = '0'then
          Feld[j-1,i] := tempstring[j];
      end;
  end;
end;
end;


Ist etwas unübersichtlich ;) Kleine Anmerkung: '0' = unbekannt '1' = Schwarz '2' = weiß


Horst_H - Mo 22.05.06 20:23

Hallo,

vielleicht leuchtet Dir mal ein, warum ich Min und MaxStartPos fuer jeden Block haben wollte.
wenn Max-Min < Laenge => sicher zu markierende Stellen.
Du haettest ruhig mal ein Beispielnonogramm und den kompletten Code (*.pas;*.dpr;*.dfm) anhaengen koennen, damit man es auch mal sieht.
Ich habe es bei mir etwas veraendert, indem ich einfach noch eine Liste Zeiger auf die einzelnen Spalten,Zeilen, die ich nach den niedrigsten Anzahl an freien Stellen sortiere(egal ob Spalte oder Zeile).
Dann trage ich erst alle sicheren Stellen in das Feld[Spalte,Zeile] ein

Delphi-Quelltext
1:
2:
3:
4:
5:
tFeldPos = record
             belegt: boolean;
             ZeilenBlock,
             SpaltenBlock: integer;//-1 , falls nicht eindeutig
           end;

Dann trage ich also erst komplett alle fixen Werte ein.
Und dann bin ich noch nicht weiter ;-).
Jetzt muesste ich aus fixen Werten fuer ZeileBlk,SpalteBlk auf die SpalteBlk, ZeileBlk schliessen.
Also ich finde hinter einem fixen SpaltenBlk eine Spalte weiter etwas durch einen ZeileBlk gefixtes, was bedeutet das fuer den Block dieser Spalte->MinStartpos erhoeht sich um eins.Falls davor sinkt MaxStartPos um 1 usw.
Habe ich den Letzen oder ersten Block gibt es ja zusaetzliche Einschraenkungen.
Eine in der ersten Zeile belegete,gefixte Stelle heisst autmatisch, dass dort der erste Spaltenblock beginnt, den man dann komplett eintragen und aus der Liste der freien Bloecke dieser Spalte entfernt(ans Ende setzt und einen Zaehler verringert) kann.
Dann muss man sich vielleicht noch die Zusammenhaenge, aufeinanderfolgender Bloecke in einer Reihe mal zu Gemuete fuehren, dass man z.B einen gefixten Punkt genau einem quer-Block zu ordnen kann , sodass sofort alle Vorgaenger und Nachfolger in Ihrer Position weiter eingeschraenkt werden.
Wenn MinStartPos= MaxStartpos ist ja alles in Butter.
Das macht noch Arbeit.

Gruss Horst