Was ist eigentlich … Backtracking?
Backtracking oder Rückwärtsverfolgung beschreibt einen Algorithmus, bei dem die Lösung einer Aufgabe durch systematisches Probieren gesucht wird. Dabei werden die Lösungsschritte für jede Teillösung nach folgendem Schema ermittelt:
- Zuerst wird festgestellt, welche Werte in für die konkrete Teillösung möglich sind.
- Dann wird überprüft, ob der erste Wert zu einer gültigen Lösung führen kann.
- Wenn ja, wird Vorgang für die nächste Teillösung (Schritt (1)) fortgesetzt.
- Wenn nein, wird der nächste Wert für die Teillösung untersucht.
- Wenn keine Werte mehr übrig sind, ist bei der vorhergehenden Teillösung der nächste Wert zu nehmen.
Der Vorgang wird gern anhand des Königinnenproblems (auch Damenproblem genannt) auf einem Schachbrett erklärt. Eine Königin kann eine andere Figur in desselben Zeile, derselben Spalte und derselben Diagonale schlagen. Die Aufgabe lautet, acht Königinnen so auf dem Schachbrett aufzustellen, dass keine eine andere schlagen kann.
In der Wikipedia ist der Vorgang im Detail beschrieben: https://de.wikipedia.org/wiki/Damenproblem
4 Königinnen
Probieren wir das Ganze mit vier Königinnen auf einem 4×4-Schachbrett: das ist leichter überschaubar. Da wir später ein Programm dazu schreiben werden, verwenden wir die in der Informatik übliche Zählweise beginnend mit 0.
Wenn wir die Felder mit der Zeilennummer (row
, r
) und der Spaltennummer (Column
, c
) bezeichnen, sind das die Feldnummern (r
, c
):
- Bild 1: Wir beginnen links oben, Platz
(0,0)
- Bild 2: In der Zeile 0 kann keine weitere Königin stehen. Daher weiter in der Zeile 1. Der erste mögliche Platz ist in Spalte 2
- Bild 3: Aber schon gibt es keinen Platz mehr in der Zeile 1. Daher muss die Königin in Zeile 2 in Spalte 3 rücken
- Bild 4: Der Platz in Zeile 2 und Spalte 1 passt
- Bild 5: Aber nun ist kein Platz in Zeile 3. Die Königin in Zeile 2 hat in dieser Zeile keinen Platz mehr. Auch die Königin in Zeile 1 kann nicht mehr weiterbewegt werden. Daher zurück in Zeile 0 – neue Position in der Spalte 1
- Bild 6: Weiter in Zeile 1
- Bild 7: Weiter in Zeile 2
- Bild 8: Und mit Zeile 3 ist die Lösung fertig
Es gibt noch eine zweite Lösung, die symmetrisch zur ersten ist – Bild 9:
Unter https://www.mathematik.ch/spiele/N_Damenproblem/ wird der Ablauf animiert dargestellt. Größe und die Verzögerung zwischen den einzelnen Schritten können eingestellt werden.
Lösung per Programm
Alle Programme dieses Beitrags können direkt vom Webserver geladen, in ein Verzeichnis kopiert und auch gleich mit Python 3 gestartet werden. Auf weitere Programmdateien wird im Text verwiesen. Sie sind hier nicht abgedruckt sind, aber die Dateien sind dort ebenfalls zu finden. Die Namen der Programmdateien sind hier hervorgehoben.
Der Algorithmus wiederholt im Schritt (c) den Vorgang ab Schritt (a). Programmtechnisch wird das zu einer Rekursion – das ist eine Funktion, die sich selbst aufruft.
Im Internet sind viele Programmbeispiele in unterschiedlichen Sprachen zu finden. Aber wir wollen einen Schritt weiter gehen. Die Idee „Backtracking“ lässt sich auf viele Probleme anwenden, auch auf typische Rätsel, wie sie beispielsweise in der Presse an jedem Sonntag veröffentlicht werden:
https://www.diepresse.com/5789519/die-erweiterte-raetselseite-der-presse-am-sonntag-zum-ausdrucken
Die objektorientierte Programmierung erlaubt die Trennung zwischen dem grundlegenden Algorithmus „Backtracking“ und der konkreten Aufgabenstellung, also einem konkreten Rätsel als abgeleitete Klasse. In einem Artikel in der Zeitschrift der ACM wurde dieser Lösungsweg (erstmalig?) vorgestellt: „An Object-Oriented View of Backtracking“ (Robert E. Noonan et.al., https://dl.acm.org/doi/pdf/10.1145/331795.331886). Der folgende Lösungsvorschlag orientiert sich daran und ist in Python 3 geschrieben.
Backtrack objektorientiert
Zuerst einmal die Klasse Backtrack0
, Programmdatei: backtrack0.py
, in der der grundlegende Backtrack-Algorithmus implementiert wird:
Backtrack0
# File backtrack0.py
# Inspiriert von
# https://dl.acm.org/doi/pdf/10.1145/331795.331886
class Backtrack0(object):
def __init__(self):
self.attempt(0)
def moves(self, level:int):
raise NotImplementedError()
def valid(self, level:int, move):
raise NotImplementedError()
def record(self, level:int, move):
raise NotImplementedError()
def undo(self, level:int, move):
raise NotImplementedError()
def done(self, level:int):
raise NotImplementedError()
def attempt(self, level: int) -> bool:
successful: bool = False
for move in self.moves(level):
if self.valid(level, move):
self.record(level, move)
if self.done(level):
successful = True
self.undo(level, move)
else:
successful = self.attempt(level+1)
if not successful:
self.undo(level, move)
Erklärungen zum Programm
Eine Reihe von Methoden sind Platzhalter, die erst bei einer konkreten Aufgabe im Detail in die abgeleitete Klasse eingefügt werden. Ein Aufruf hier führt zu einer Fehlermeldung.
moves
liefert für die Methodeattempt
jene Werte, die der Reihe nach auszuprobieren sind. Gegebenfallss werden Werte, die zu keiner Lösung führen können, gar nicht in die Liste aufgenommen.valid
überprüft, ob der Wert move im Schrittlevel
ein gültiger Wert ist.record
speichert den vonvalid
überprüften Wert ab.undo
entfernt den Wert wieder, wenn mit ihm keine Lösung möglich ist.done
prüft, ob alle Schritte schon erledigt sind und das Rätsel gelöst ist. In diesem Fall wird die Lösung auch gleich ausgegeben und nach einer weiteren Lösung gesucht.attempt
ist die zentrale Methode, die die vorher genannten Methoden verwendet. Sie wird mit dem Startwert0
beim Initialisieren durch__init__
aufgerufen.
Warum englische Bezeichnungen für die Methoden? Einerseits um den Vergleich mit dem ACM-Artikel zu ermöglichen, andererseits um eine Veröffentlichung dieses Beitrags in anderen Medien zu erleichtern.
Viele Versuche waren für die Lösung vorwendig? Wie lange hat die Suche gedauert? Dazu erweitern wir Backtrack0
zu Backtrack
. Auf Backtrack
bauen dann auch alle folgenden Beispiele auf.
Backtrack
# File backtrack.py
import time, sys
from backtrack0 import Backtrack0
class Backtrack(Backtrack0):
attempts = 0
showAttempts = False
def __init__(self, test:bool = False, showAttempts:bool = False):
self.test = test
start = time.time()
self.attempt(0)
end = time.time()
self.showAttempts = showAttempts
if showAttempts:
print(f„\n{self.attempts:16_d} attempts“
f“ in {end-start:8,.2f} seconds“)
def attempt(self, level: int) -> bool:
if self.attempts>100_000:
if self.attempts % 5_000_000 == 0:
print(f„\n{self.attempts//1_000_000:4_d} Millionen „, end=„\n“)
if self.attempts % 100_000 == 0:
print(„.“, sep=„“, end=„“)
successful: bool = False
for move in self.moves(level):
self.attempts += 1
if self.test:
print(f„attempt-test: level:{level} move:{move}„)
if self.valid(level, move):
self.record(level, move)
if self.done(level):
successful = True
self.undo(level, move)
else:
successful = self.attempt(level+1)
if not successful:
self.undo(level, move)
Weitere Hinweise
- Bei langen Rechnungen sollte angezeigt werden, ob das Programm noch „lebt“. Daher wird nach je 100.000 Versuchen ein Punkt ausgegeben und nach jeweils 5.000.000 Versuchen ein Text.
- Mit
print(..., end="")
werden die einzelnen Punkte nebeneinander und nicht untereinandergeschrieben. - Zahlen können mit einem „_“-Zeichen leichter lesbar gemacht werden. Die Erfinder der IBAN hatten leider keine vergleichbare Idee.
- Das Programm verwendet Formatstrings: formatted string literals, siehe https://docs.python.org/3/tutorial/inputoutput.html und https://docs.python.org/3/library/string.html#formatspec. Wer sie noch nicht kennt: bitte unbedingt eine der vielen Dokumentationen dazu im Internet lesen!
- In
__init__
wird der Formatstring in der nächsten Zeile fortgesetzt. Der Fortsetzungsstring muss auch mit f beginnen.
Links
- Alle Dateien (ZIP-Datei, wird heruntergeladen)
- Was ist eigentlich ‚Backtracking‘? (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