Backtracking: Königinnenproblem
Das Königinnenproblem
Die erste Anwendung der Klasse Backtrack.
Queens.py
# File queens.py
from backtrack import Backtrack
import datetime
class Queens(Backtrack):
vector = [ ]
diag1 = [ ]
diag2 = [ ]
solutionNr: int = 0
def __init__(self, test:bool = False, showAttempts:bool = False,
beautify:bool = False, size:int = 8):
self.beautify = beautify
self.size = size
super().__init__(test, showAttempts)
def moves(self, level: int):
return range(self.size)
def valid(self, level: int, move:int) -> bool:
return not( # Was ist besetzt…
move in self.vector or # …die Spalte?
level-move in self.diag1 or # …die Diagonale 1: links-oben nach rechts-unten
level+move in self.diag2 # …die Diagonale 2: links-unten nach rechts-oben?
)
def record(self, level: int, move):
self.vector.append(move)
self.diag1.append(level-move)
self.diag2.append(level+move)
def undo(self, level: int, move):
self.vector.pop()
self.diag1.pop()
self.diag2.pop()
def done(self, level: int):
if level==self.size-1:
self.solutionNr += 1
print(f"\nLösung {self.solutionNr:4} {self.vector}")
if self.beautify:
self.show()
return True
return False
def show(self):
# „Schöne“ Ausgabe
for r in range(self.size):
for c in range(self.size):
print("Q " if self.vector[r]==c else ". ", sep="", end="")
print()
now = datetime.datetime.now()
print(f"Berechnet am {now:%d. %b. %Y} um {now:%H:%M} Uhr.")
if __name__ == "__main__":
Queens(test=False, showAttempts=True, beautify=True, size=4)
Das Programm im Detail
Mit super.__init__(...) wird die Steuerung an die __init__-Methode der Basisklasse Backtrack übergeben.
Diese Klasse wird als Modul in der Datei queens.py gespeichert. Zum Aufruf kann ein neues Python-Programm mit zwei Zeilen eingesetzt werden:
from queens import Queens
Queens(test=False, showAttempts=True, beautify=True, size=4)
Einfacher ist es aber, die folgenden zwei Zeilen am Ende von queens.py einzufügen:
if __name__ == "__main__":
Queens(test=False, showAttempts=True, beautify=True, size=4)
showAttemptssteuert die Anzeige der Anzahl der Versuche und der Dauer.beautifygleichTruebewirkt die Ausgabe der Lösung als Quadrat.sizehat den Standardwert 8 und wird hier auf 4 gesetzt.
Oder kürzer, aber weniger übersichtlich:
if __name__ == "__main__":
Queens(False, True, True, 4)
Die letzten zwei Zeilen erlauben es, Queens direkt aus der Python IDLE heraus zu starten:

Wie werden gültige Plätze für Königinnen gefunden?
Zeilen und Spalten
Vorbemerkung
- Ein Array ist in den meisten Programmiersprachen eine Zusammenfassung von gleichartigen Komponenten, zum Beispiel von ganzen Zahlen. Mehrdimensionale Arrays sind Arrays von Arrays.
- Eine Liste erweitert dieses Konzept. Listen sind in Python sehr wichtige Sprachelemente. Die Liste ähnelt dem Array, kann aber beliebige Komponenten (und natürlich auch wieder Listen) enthalten
vector ist eine Liste, in der in der Reihenfolge der Zeilen jene Spalten abgespeichert werden, die Königinnen enthalten. Zu Beginn ist die Liste leer.
Bild 1 wird durch den vector mit dem Inhalt [0] beschrieben.
Zum besseren Verständnis hier die vector-Darstellung der einzelnen Bilder. Die Spalten diag1 und diag2 werden später erklärt.
Bild | vector | diag1 | diag2 |
1 | [0] | [0] | [0] |
2 | [0, 2] | [0, -1] | [0, 3] |
3 | [0, 3] | [0, -2] | [0, 4] |
4 | [0, 3, 1] | [0, -2, 1] | [0, 4, 3] |
5 | [1] | [-1] | [1] |
6 | [1, 3] | [-1, -2] | [1, 4] |
7 | [1, 3, 0] | [-1, -2, 2] | [1, 4, 2] |
8 | [1, 3, 0, 2] | [-1, -2, 2, 1] | [1, 4, 2, 5] |
9 | [2, 0, 3, 1] | [-2, 1, -1, 2] | [2, 1, 5, 4] |
Diagonale 1
In den Diagonalen darf auch nur je eine Königin stehen. Zuerst einmal betrachten wir die Diagonalen von links oben nach rechts unten. Für jeden Platz des Miniatur-Schachbretts wird eine Zahl mit der Differenz Zeilennummer – Spaltennummer berechnet. Im Programm steht für die Zeilennummer level und für die Spaltennummer in dieser Zeile move. Damit wird einerseits auf den Artikel der ACM Bezug genommen und andererseits werden weitere Programmbeispiele vorbereitet. Damit ergeben sich somit für level-move folgende Zahlen:
| 0 | -1 | -2 | -3 |
| 1 | 0 | -1 | -2 |
| 2 | 1 | 0 | -1 |
| 3 | 2 | 1 | 0 |
diag1 ist eine Liste, die für jede Königin die so errechnete Zahl enthält. Für die erste Lösung enthält diag1 daher die Zahlen [-1, -2, 2, 1]. Jede Zahl darf nur einmal vorkommen – die Königinnen kommen einander in den Diagonalen von links oben nach rechts unten nicht in die Quere
Diagonale 2
Nun zu den Diagonalen von links unten nach rechts oben. Die Zahlen pro Feld werden aus der Summe Zeilennummer + Spaltennummer bestimmt:
| 0 | 1 | 2 | 3 |
| 1 | 2 | 3 | 4 |
| 2 | 3 | 4 | 5 |
| 3 | 4 | 5 | 6 |
Daher enthält die Liste diag2 für die erste Lösung die Zahlen [1, 4, 2, 5]. Auch hier kommt keine Zahl zweimal vor – die Bedingung für die Diagonalen von links unten nach rechts oben ist somit auch erfüllt.
Die Methoden
movesliefert alle Werte, die die Variablemovebei den Versuchen (attempt) annehmen darf.valid: Mit den Vereinbarungen wirdvalidrecht einfach: die neue Position einer Königin in Zeilelevelund Spaltemoveist dann zulässig, wenn wedermoveinvectorenthalten ist nochlevel-moveindiag1und auch nichtlevel+moveindiag2.record: Die den Platz der Königin beschreibenden Zahlen sind an die Listenvector,diag1unddiag2anzuhängen.vectorwird um die Spaltennummermoveerweitert,diag1um die Differenz von Zeilennummer und Spaltennummer, alsolevel-moveunddiag2um die Summe der Zeilen- und Spaltennummernlevel+move.undowird benötigt, wenn es keine weiteren Möglichkeiten fürmovegibt und daher der Schritt rückgängig zu machen ist. Dazu sind jeweils die letzten Einträge aus den Listenvector,diag1unddiag2mitpop()zu entfernen.done: Eine Lösung ist gefunden, wenn in der letzten Zeile eine Königin einen Platz gefunden hat. Für das Anzeigen der Lösung reicht die Ausgabe desvector. Mit der Methodeshowwird die Lösung schöner dargestellt.- Die übersichtliche Ausgabe von Datum und Uhrzeit ist immer wieder eine Herausforderung. Älteren Python-Version borgen sich Funktionen wie
strftimevon C aus. Leider sind diese Funktionen nicht sehr benutzerfreundlich. Mit derformat-Funktion oder mit Formatstrings geht das viel einfacher, wird aber nur selten in Dokumentationen erwähnt. Daher gibt es hier ein Anwendungsbeispiel. Mehr dazu in https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
Gar nicht kompliziert!
Ergebnis
Folgende Lösungen werden gezeigt:
Lösung 1 [1, 3, 0, 2]
. Q . .
. . . Q
Q . . .
. . Q .
Lösung 2 [2, 0, 3, 1] . . Q . Q . . . . . . Q . Q . .
60 attempts in 0.24 seconds
Übrigens: die Berechnung und Ausgabe aller 92 Lösungen des 8-Königinnenproblems dauert nach 15.720 Versuchen 28,9 Sekunden und ohne beautify 1,2 Sekunden.
Zurück zu 4 Königinnen: natürlich sind noch weitere Verbesserungen möglich. moves liefert als zulässige Spaltennummern alle Zahlen von 0 bis zur Anzahl der Königinnen – 1. Aber Spaltennummern, die schon im vector enthalten sind und somit schon vergeben sind, brauchen nicht mehr untersucht zu werden. Das wird mit
def moves(self, level: int):
return list(set(range(self.size))-set(self.vector))
erreicht:
- Zuerst wird mit
rangeeine Liste[0, 1, 2, 3]erzeugt. - Diese Liste wird in eine Menge (
set) umgewandelt. - Ebenso wird aus der Liste der bisher gefundenen Lösungen (
vector) in eine Menge gebildet. - Die Differenz dieser beiden Mengen gibt an, welche Spalten noch frei sind.
- Die Differenzmenge wird für die weitere Verarbeitung wieder in eine Liste verwandelt.
Diese Version ist in der Datei queens_2.py gespeichert.
Nun sind nur mehr 32 Versuche notwendig. Da die meiste Zeit zur Ausgabe der vector-Werte gebraucht wird, sinkt die Gesamtzeit nicht wesentlich. Wenn wir in done die print-Zeile auskommentieren, wird die echte Rechenzeit mit weniger als 0,01 Sekunden angezeigt.
Noch ein paar Zahlen: bei 12 Königinnen werden nach 2.915.740 Versuchen die 14.200 Ergebnisse in 7,0 Sekunden errechnet oder zusammen mit den vector-Werten in 175.9 Sekunden angezeigt.
Es gibt viele weitere Varianten, die auf dem Königinnenproblem aufbauen.
- Statt der Königinnen könnten Springer eingesetzt werden.
- Oder Superköniginnen, die die Eigenschaften der Königin und des Springers vereinigen. Der Name dafür „Maharadscha“. Warum eigentlich nicht „Maharani“? Ich bin kein Gender-Fan, aber in dem Fall täte ein wenig Allgemeinbildung gut.
Wir brauchen nur die Methode valid zu verändern und schon können alle Varianten auch berechnet werden.
Links
- Alle Dateien (ZIP-Datei, wird heruntergeladen)
- Was ist eigentlich ‚Backtracking‘? (clubcomputer.at)
- Backtracking: Königinnenproblem (clubcomputer.at)
- Backtracking: Sudoku (clubcomputer.at)
- Backtracking: Kendoku (clubcomputer.at)
- Backtracking: Skyline (clubcomputer.at)
- Backtracking: Zeltlager (clubcomputer.at)
Martin Weissenböck hat die HTL 3 Rennweg 23 Jahre lang geleitet, schreibt Skripten, Lehrbücher und Artikel für die PCNEWS, leitet die Arbeitsgemeinschaft für Didaktik, Informatik und Mikroelektronik (ADIM) und den Verein SCHUL.InfoSMS. Er hält Vorträge und ist Lehrbeauftragter an der Technischen Universität Wien.
Neueste Kommentare