Sudoku-Löser: Fragen zu Pointer und Rekursion

Moch

Bekanntes Mitglied
Anmerkung zur Auswahl des Forums: Ich habe mich für das AnfängerForum entschieden, da die Fragen zu Rekursiv und Pointern die eines Anfängers sind ... dass Sudoku nicht zwingend für den blutigen Anfänger gedacht ist, ist mir durchaus klar, jedoch wollte ich jetzt nicht gleich zwei Topics aufmachen. Ich hoffe, dass ist so okay. Ansonsten entschuldige ich mich dafür und bitte dies einfach zu verschieben.

Wer keine Lust auf den Text hat: Die Fragen stehen gaaanz unten 🙂

Hallo,
Ich programmiere mir derzeit einen Algorithmus, der ein eingegebenes Sudoku lösen soll bzw. bei unlösbarkeit des Selbigen genau diesen Umstand zurückgeben soll. Hierzu habe ich (bisher) einen zwei-Klassen-Ansatz gewählt.

Die 1. Klasse heißt Sudoku. Der Konstruktor besteht aus zwei boolean, von denen einer angibt, ob das Sudoku bereits gelöst ist und der andere, ob es im aktuellen Status (!) noch lösbar ist.
Weiterhin findet sich im Konstruktor ein 9X9 Array aus Feldern (siehe zweite Klasse), das auch sogleich durch eine simple doppel for-schleife vorbelegt wird.

Die 2. Klasse heißt Feld. Jedes Feld repräsentiert eines der 81 Felder, die ein Standardsudoku hat. Dementsprechend "weiß" jedes Objekt Feld:
- seine Koordinaten (X,Y)
- seinen Sektor (damit meine ich diese 3x3-Quadrate, für die ebenfalls die Regel gilt, dass eine Zahl nur einmal vorkommen darf)
- ob es sich beim eingetragenen Wert um einen Fix-Wert handelt (bereits eingetragene Zahl) (boolean)
- welche Zahlen noch eingetragen werden können (Int-Array, das zu Beginn die Länge 9 hat).

Beide Klassen habe ich mit den nötigen Methoden versehen, um Änderungen an Variablen und Arrays vorzunehmen, Überprüfungen der Stati vorzunehmen. Intern gibt es in einigen Methoden Exceptions, die eine Verletzung bestimmter Bedingung verhindern soll (z.B. darf der fix-Boolean eines Felds nicht auf true gesetzt werden, wenn die Lösungsmenge noch größer als 1 ist) ... ja, hätte ich auch über eine einfache IF-Abfrage machen können, aber das macht's mir bei tests einfacher rauszufinden, wo der Fehler auftritt.

Der Algorithmus selbst soll wie folgt arbeiten:
1. User ein Sudoku eingeben lassen
2. Sudokugültigkeit prüfen (kommen schon im Ursprung zwei Zahlen in gleiche zeile/spalte/sektor vor?)

3. prüfen, ob Sudoki bereits gelöst (wenn ja, dann abbruch und Ausgabe)
4. Lösungsmenge der Felder korrigieren
5. prüfen, ob Sudoku in diesem Status noch lösbar (keines Feldes Lösungsmenge darf kleiner 1 werden)
6. prüfen, ob es Felder gibt, deren Lösungsmenge == 1 ist
7. Auswahl der Methode zur weiteren Lösung
8. Rücksprung zu Schritt 3.

Meine Probleme bestehen jetzt im Punkt 7
Ich unterscheide hierbei zwischen zwei wesentlichen Ansätzen:
1. Dem simplen Ansatz
2. Dem backtracking-Ansatz

Der 1. Fall ist nur möglich, wenn es in der Prüfung (Schritt 6) mindestens 1 Feld gibt, dessen Lösungsmenge == 1.
Hierbei werden einfach alle Felder, deren Lösungsmenge = 1 ist mit dem entsprechenden fix-Wert belegt.
Der simple Ansatz ist (meiner Meinung und Programmierung nach) zu bevorzugen und wird wann immer möglich ausgeführt. Begründung hier ist: Der Ansatz ist NICHT rekursiv

Der 2. Fall tritt genau dann ein, wenn der simple Ansatz unmöglich ist, da die Lösungsmengen durchgehend nicht eindeutig sind (Länge der Lösungsmenge größer 1). Hierbei soll der Algorithmus das Feld mit der kleinsten Lösungsmenge raussuchen. Werden mehrere mit gleicher Lösungsmenge gefunden, so wird das erste gefundene Feld ausgewählt.
Nacheinander sollen hier alle für dieses Feld möglichen Lösungen ausprobiert werden.
(Beispiel: noch mögliche Zahlen für Feld A sind 1,3,5 -> das Programm setzt zunächst 1 als FixWert und prüft, ob eine Lösung rauskommt, wenn nein, dann das gleiche mit drei, wenn wieder nein, dann mit 5 und wenn wieder nicht, ist das Sudoku nicht lösbar ... führt eine der Möglichkeiten zu einer Lösung, wird sofort die Ausgabe erzeugt und das Programm endet.)

In beiden Fällen wird das ursprüngliche Sudoku verändert und wieder durch den Lösungsalgorithmus gejagt (Ansatz hierfür ist interativ: eine while-schleife prüft, ob Sudoku gelöst oder unlösbar und beendet sich, wenn diese Bedingung erfüllt)

So viel zu meinem bisherigen Ansatz. Nun zu meinen Fragen, die hierzu aufgetreten sind:

1. Wie schaut es beim Kopieren von Objekt (hier das Objekt Sudoku) mit Pointern aus?
Sagt man bei einem Array einfach, es sei = anderes Array, kopiert man ja nicht das Array, sondern nur den Pointer.
Ist das bei Objekten bzw. Objekten die Arrays mitführen ebenfalls so?
(da müsste ich entsprechend eine tiefergehende kopiermethode schreiben, ähnlich der für Arrays)

2. Wie schaut es mit der Machbarkeit des Backtrackings aus speichertechnischer Sicht aus?
Als WorstCase habe ich den Fall identifiziert, in welchem der User eine vollständig leeres Sudoku eingibt.
Alleine für die komplette erste Zeile gäbe es 9! Möglichkeiten.

Da ich aber davon ausgehe, dass Java die Schritte NACHEINANDER ausführen wird (sprich jeweils nur eine Möglichkeit zur Zeit prüft und dann entweder die Ausgabe erreicht oder einen Schritt zurückgeht), habe ich berechnet, dass die Rekursion eine Tiefe von 60 erreichen wird.
(9X9 Felder = 81 ... davon ausgehend, dass das jeweils letzte Feld eines Sektor wieder nur eine Lösung besitzt, wird hier der simple Ansatz gewählt. Es gibt 9 Sektoren, also 81 - 9 = 72
Weiterhin gilt auch für das letzte Feld einer Zeile wieder Lösungsmenge = 1. Dies gilt für 6 Zeilen => 72 - 6 = 66.
Hinzu kommt, dass für die komplette letzte Zeile wieder die Lösungsmenge = 1 ist. Dies gilt wieder für 6 Felder (nicht für 9, da ich die letzten Felder eines Sektors bereits zu anfang ausgerechnet habe
66 -6 = 60)

Sprich: Der Algorithmus wird im WorstCase 60 Sudokus erzeugen und das letzte als Lösung ausgeben.

Wird das mit hoher Wahrscheinlichkeit zu einem Stack-Overflow führen oder sollte das per se keine Probleme geben
(ich bin mir kein Rekursionen nie sicher, weshalb ich die auch nicht mag ... interativ habe ich hier aber keinen Ansatz gefunden)



3. Keine Frage, aber ich würde mich über Verbesserungsvorschläge für den von mir entwickelten Algorithmus freuen.

Ich habe einen Ansatz, den ich ursprünglich für eine imperative Sprache entwickelte (Ada95) hier für die Objektorientierung umgewandelt und Teile des Ansatzes behalten. Java macht's mir an vielen Stellen leichter als Ada95, daher auch größere Abwandlungen. Behalten habe ich im prinzip den Ken mit der Auswahl der Methode und eben die beiden Methoden.
Was ich jedoch nicht möchte ist ein bereits fertiger Ansatz oder eine fertige Lösung. Ich habe aus Spaß am Programmieren vor zwei Tagen damit begonnen, diesen Algorithmus zu schreiben und möchte am Ende das Gefühl haben, selbst etwas geleistet zu haben und nicht einfach eines anderen Lösung angepasst zu haben ... möchte auch als Student und Offizieranwärter einfach mal stolz drauf sein dürfen :-D

Liebe Grüße
Moch
 
Zuletzt bearbeitet:
Objekte die das Interface Cloneable implementieren können mit .clone() dupliziert werden.
Willst du eigene Objekte duplizieren, würde ich daher raten einfach dieses Interface zu implementiern.

Wie es speichertechnisch aussieht bin ich mir nicht sicher, der Stack sollte aber 60 Rekursion schon aushalten.
 
Danke für Deine Antwort.
Aus Deiner Antwort folgere ich jetzt gleich mal, dass ein einfaches ObjektA = ObjektB nur den Pointer selbst dupliziert... sonst würde die implementiere Methode clone() wohl wenig Sinn geben.

Werde das gleich mal ausprobieren. Momentan fehlen wirklich nur noch die beiden Methoden, die Backtracking und SimpleMethode beinhalten (hoffe ich).

lieben Gruß
Moch
 
Hey, mich würde mal interessieren ob du dein Backtracking schon erfolgreich geschrieben hast.
Da ich auch gerade an so einer Aufgabe sitze und meine Backtracking Methode das Problem besitzt, das sie meißt nach 2 - 4 Zahlen im Sudoku eine ehwigkeit braucht eine Lösung auszuspucken????
 
Danke für Deine Antwort.
Aus Deiner Antwort folgere ich jetzt gleich mal, dass ein einfaches ObjektA = ObjektB nur den Pointer selbst dupliziert... sonst würde die implementiere Methode clone() wohl wenig Sinn geben.

Werde das gleich mal ausprobieren. Momentan fehlen wirklich nur noch die beiden Methoden, die Backtracking und SimpleMethode beinhalten (hoffe ich).

lieben Gruß
Moch

Verwende bitte nicht den Begriff Pointer. Die gibts in Java nicht. Es sind alles Referenzen! Das ist ein wichtiger und entscheidender Unterschied.
 
Ich hab ebenfalls ein Sudoku-Löser geschrieben, leider die Sourcen aufgrund eines Plattencrashs nicht mehr. Ich weiß aber noch ungefähr wie ich das Teil programmiert habe.

Du kannst beim Backtracking einfach dein aktuelles Sudoku-Feld dem Algorithmus übergeben.
Nun arbeitet er mit dem übergebenen Feld auf dem Stack, setzt eine Zahl ein und ruft sich selber wieder auf. Kommt er zu einer Lösung kannst du ja abbrechen lassen, die Lösung speichern und ausgeben.

Als Tip für die Blöcke, kannst du dir eine Methode schreiben, die die Blöcke in ein Array wandelt und mit der gleichen Prüfmethode wie deine normalen Spalten und Zeilen prüfen.

Mein Programm lief in Sekunden, egal ob leer, fast voll usw.
 
Also ich arbeite von anfang an mit einem 2 dimensionalen Array, deswegen weiß ich jetzt nicht direkt wie und mit was ich auf einem Stack arbeiten soll.
 
ok ... dann haben wir wohl an einander vorbei gesprochen ... ich hab mal meine Generator Klasse im anhang.... wäre total cool wenn du mal drüber schauen könntest denn ich weiß einfach nicht warum er ab ein paar zahlen abbricht. wenn du noch fragen dazu hast kann ich das auch gern noch erklären....
 

Anhänge

Ich muss dir leider sagen, dass ich bei deinem Code Gehirnverdrehungen bekomme 😉
Spätestens nach der 3. verschachtelten for-Schleife und der 7. ifelse Anweisung musst du dich fragen, ob dein Code nicht anders strukturiert werden müsste.

Darunter fällt z.B. eine print-Methode für das Sudoku, damit man sowas auslagern kann. Ich würde auch zum Testen ein Sudoku nutzen und bestimmte Tests ablaufen lassen.

1--|2--|3--
--7|-6-|---
4--|--9|---

Tests ala enthält Block 2 die Zahlen 2,6,9. Enthält die Spalte 3 die Zahl 7 usw. Wie groß ist das Array von Feld 0/1 usw.

Im übrigen nutzt du in der Generator ja garkeine Feld-Objekte. Wenn das Feld den eigenen Sektor kennt, kann man sich die Überprüfung nach dem Sektor ja sparen.

Methodenkommentare helfen dir übrigens auch weiter, einem externen sowieso. Kann gut sein, dass ich nicht alles gecheckt habe in der Generator Klasse.

Wenn du die Tests erstamal hast (Junit4 als Tip)
 
Verwende bitte nicht den Begriff Pointer. Die gibts in Java nicht. Es sind alles Referenzen! Das ist ein wichtiger und entscheidender Unterschied.

Okay, das ist auch etwas, dass mir noch unbekannt war (wurde in der Vorlesung wohl nicht erwähnt)... bisher war es bei allen Sprachen, mit denen ich gearbeitet habe bzw. arbeiten musste so, dass man mit Pointern gearbeitet hat.
Ist wohl genauso wie mit den Methoden :-D - die anderen Sprachen, die ich hatte, hatten die Unterscheidungen Functions und Procedures. Musste ich mich auch erstmal dran gewöhnen.
Magst Du mich über den Unterschied aufklären? (möchte nicht wieder auf 1000 Seiten laden, die mit Halbwissen hantieren)

@KingDan:
Ich habe mein Backtracking in Java noch nicht implementiert, habe es für ein Sudoku schon mal in Ada95 gebastelt. (Muss hierbei sagen, dass die beiden zentralen Lösungsmethoden das letzte sind, was ich implementiere. Vorher habe ich erstmal die Objekt-Methoden, Getter- & Setter, sowie ein Nutzer-Interface (sehr einfach, Textbasiert über Konsole) fertig geschrieben und damit verbunden natürlich auch die Methoden, um ein Sudoku auszugeben + zahlreiche Tests für einzelne Funktionen und die Bearbeitung von Sonderfällen.

Dennoch läuft mal Algorithmus (Ausschnitt) wie folgt ab:
- Es wird geprüft, ob es Felder gibt, deren Lösungsmenge nur noch die Größe 1 hat (nur noch eine mögliche Zahl)
- Ist das NICHT der Fall, so wird mein Backtracking aufgerufen
- Dieses sucht sich dann das Feld mit der kleinsten, möglichen Lösungsgröße (bei mehreren Feldern, die gleich sind, wählt es das erste dieser Felder)
- Nun setzt die Methode die erste, noch mögliche Zahl aus der Lösungsmenge in eine Kopie des Sudokus ein und wirft diese wieder in die Methode zum Lösen ein.
- Damit beginnt der Vorgang von Vorne
- Kommt das Programm zu einer gueltigen und abschließenden Lösung, so wird diese ausgegeben und die Lösungsmethoden werden direkt beendet
- Kommt es zu keiner gueltigen Loesung, springt mein Programm zurück zum Backtrackingschritt, wählt die nächste Zahl aus der Lösungsmenge aus und wirft diese wieder in die Methoden rein.
- Kam es bei KEINER Zahl aus der Lösungsmenge zu einem Ergebnis, so wird ausgegeben, dass das Sudoku nicht lösbar ist.

Nicht zwingend effizient, aber hoffentlich effektiv. Ist als die aus Ada95 (imperativ geschriebene) Variante.

Vielleicht noch ein kurzes Beispiel (nur grob):

0. Methode Loesen wird fuer das Sudoku auferufen
1. Das Programm findet kein Feld mehr, das nur noch eine moegliche Loesung hat
2. Gefunden wird das Feld (1/1), dessen Wert noch nicht fix ist und das nur noch zwei moeglichkeiten hat (2 & 4)
3. Methode backtracking wird aufgerufen
4. BT wählt erste Zahl aus (2), kopiert das übergebene Sudoku und setzt bei (1/1) den Wert 2 fix ein
5. BT übergibt kopiertes Sudoku an die Methode loesen
6. loesen kann im weiteren keine gueltige Loesung finden
7. BT wählt zweite Zahl aus (4), kopiert und setzt 4 als fixen Wert wieder ein
8. BT übergibt wieder an loesen
9. Es gibt eine abschließende Loesung, diese wird ausgegeben, Durchlauf beendet.
 
Der Unterschied zwischen Pointern und Referenzen ist meines Erachtens kein so großer. In Java zeigen die Referenzen auch immer auf die gleichen Objekte wenn man etwas nicht mittels new anlegt. Und genauso wie Pointer lassen sich Refernzen verbiegen. Ok, man kann nicht abzählen aber als Anfänger wundert man sich schon, warum sich irgendwelche Werte in irgendwelchen Objekten ändern, die man an einer ganz anderen Stelle gespeichert hat.

Bsp. : Objekt, wird mit new erzeugt, in ein Array gespeichert, innerhalb des Arrays weiter benutzt, außerhalb des Arrays auch, immernoch das identische Objekt.
 
Der Unterschied zwischen Pointern und Referenzen ist meines Erachtens kein so großer. In Java zeigen die Referenzen auch immer auf die gleichen Objekte wenn man etwas nicht mittels new anlegt. Und genauso wie Pointer lassen sich Refernzen verbiegen. Ok, man kann nicht abzählen aber als Anfänger wundert man sich schon, warum sich irgendwelche Werte in irgendwelchen Objekten ändern, die man an einer ganz anderen Stelle gespeichert hat.

Bsp. : Objekt, wird mit new erzeugt, in ein Array gespeichert, innerhalb des Arrays weiter benutzt, außerhalb des Arrays auch, immernoch das identische Objekt.

Der Unterschied wird deutlich wenn man sich Pointer auf Pointer vorstellt. Das geht in Java nicht, da es keine Möglichkeit gibt auf die SPeicheradressen programmatisch zuzugreifen.

Beispiel:

In C++ kann man einer Methode einen PointerPointer übergeben und diesem einen neuen Pointer (new Object ...) zuweisen welcher dann auch darüber verfügbar ist.

Macht man das in Java überschreibt man nur die lokale Referenz in der aufgerufenen Methode. Die Originalreferenz bleibt unverändert, da Referenzen in Java per CallByValue übergeben werden!
 
So, mal wieder zu meinen ursprünglichen Fragen eine Erkenntnis:
Der Stack läuft bei mir bei 2996 Rekursionsschritten über und wirft die Exception.
Fand ich gerade heraus, als ich getestet habe und der Stack immer überlief, wenn eine Situation auftrat, die mehr als eine Möglichkeit pro Feld behielt. Liegt aber nicht am Sudoku oder an Java, sondern muss definitiv ein massiver Programmierfehler hinter stecken.

Meine Ausgaben ergaben, dass das Backtracking zwar ordnungsgemäß aufgerufen wird, jedoch nur das Feld oben links (ArrayPosition [0][0]) einen Wert bekommt.
Muss da noch ein wenig schauen, wo das Problem liegt, aber das eben nur mal zur Information, falls sich das noch wer fragt. 🙂
Mit dem Fehler werde ich (hoffentlich) fertig 🙂 - bestimmt irgendwo ein ganz doofer Fehler :-D
 

Zurück
Oben