Backtracking
Idee
Der folgende Text ist gekürzt aus der Wikipedia [1] übernommen.
Backtracking geht nach dem Versuch-und-Irrtum-Prinzip (trial and error) vor, das heißt, es wird versucht, eine erreichte Teillösung zu einer Gesamtlösung auszubauen. Wenn absehbar ist, dass eine Teillösung nicht zu einer endgültigen Lösung führen kann, wird der letzte Schritt beziehungsweise werden die letzten Schritte zurückgenommen, und es werden stattdessen alternative Wege probiert. Auf diese Weise ist sichergestellt, dass alle in Frage kommenden Lösungswege ausprobiert werden können
Backtracking wird am einfachsten rekursiv implementiert.
Beschreibung des Algorithmus
Die folgende Beschreibung ist gekürzt und leicht verändert aus der Wikipedia [2] übernommen.
Teillösung ist zu Anfang leer.
Stufe ist 0.
Funktion FindeLoesung (Stufe, Teillösung)
1. Abbruchbedingung: Wenn die Stufe zu groß ist
oder es keinen Teil-Lösungsschritt mehr gibt.
2. Abbruchbedingung: Wenn eine Lösung erreicht wurde.
Bearbeite ggf. die Lösung!
3. wiederhole, solange es noch neue Teil-Lösungsschritte gibt:
a) Wähle einen neuen Teil-Lösungsschritt.
b) Erweitere Teillösung um Wahl.
c) rekursiver Aufruf: FindeLoesung(Stufe+1, Teillösung)
d) Mache die Erweiterung der Teillösung rückgängig
Backtracking für kürzeste Wege auf Graphen
Um kürzeste Wege auf Graphen zu bestimmen, kann man den Backtracking-Algorithmus verwenden. Allerdings ist dieser sehr langsam; effizienter wird das Problem mit dem Dijkstra-Algorithmus gelöst.
Beispiel
Gesucht ist die kürzeste Verbindung von Frankfurt nach Dortmund
Damit man keine Möglichkeit auslässt, geht man nach einer Ordnung vor - hier bietet es sich an, die möglichen Nachbarstädte nach dem Alphabet zu betrachten.
Dann ergibt sich für das Backtracking folgende Reihenfolge:
- Frankfurt -> Kassel -> Dortmund: merken!
- Frankfurt -> Kassel -> Würzburg: Sackgasse
- Frankfurt -> Köln -> Dortmund: kürzer, also merken!
- Frankfurt -> Würzburg -> Kassel -> Dortmund: länger, also ignorieren
Backtracking-Algorithmus für den kürzesten Weg
Man braucht die folgenden Attribute:
besterWeg
: Hier wird der jeweils beste gefundene Weg gespeichert.besteLaenge
: Die Länge vonbesterWeg
aktuellerWeg
: Der Weg, der gerade untersucht wird.aktuelleLaenge
: Die Länge vonaktuellerWeg
Mithilfe dieser lässt sich der Algorithmus so beschreiben:
findeKuerzestenWegBacktracking (pNode)
Abbruchbedingung:
Wenn pNode das Ziel ist (d.h. der Lösungsvektor ist vollständig!)
dann vergleiche aktuellerWeg mit besterWeg
und aktualisiere diesen, wenn es nötig ist.
Betrachte der Reihe nach alle Nachbarn von pNode.
Für jeden Nachbarn wird folgendes getan:
- den Nachbar zu aktuellerWeg hinzufügen (d.h. der Lösungsvektor wird erweitert!)
- aktuelleLaenge entsprechend erhöhen
- den Nachbarn markieren
- findeKuerzstestenWegBacktracking (nachbar) aufrufen
- die Änderungen wieder rückgängig machen (d.h. die Wahl wird rückgängig gemacht!)
- den Nachbarn aus aktuellerWeg entfernen
- aktuelleDistanz wieder zurücksetzen
- die Markierung des Nachbarn entfernen
Implementierung
Die Implementierung setzt auf die Klasse Graph auf.
Damit während des Backtracking auf die wesentlichen Daten zugegriffen werden kann, werden diese in Attributen gespeichert.
Die wesentlichen Methoden sind die folgenden:
- die Rahmenmethode
kuerzestenWegFinden(String pStart, String pZiel)
: In der Rahmenmethode wird alles für das rekursive Backtracking vorbereitet. - die rekursive Backtracking-Methode
kuerzestenWegFindenBacktracking(GraphNode pNode)
// Attribute
public GraphWithViewer graph;
// Attribute fuer das Backtracking
public ListWithViewer<Vertex> besterWeg;
public ListWithViewer<Vertex> aktuellerWeg;
public int besteLaenge;
public int aktuelleLaenge;
public Vertex start;
public Vertex ziel;
//Methoden
/**
* Kopiert den Inhalt einer Liste in eine andere Liste.
* Der Inhalt der Ziel-Liste wird dabei ersetzt.
* @param herkunftsListe
* @param zielListe
*/
private void kopiereListeIn(List herkunftsListe, List zielListe){
// besterWeg leeren
for(zielListe.toFirst();!zielListe.isEmpty();){
zielListe.remove();
}
// aktuellerWeg in besterWeg kopieren
for(herkunftsListe.toFirst();herkunftsListe.hasAccess();herkunftsListe.next()){
zielListe.append(herkunftsListe.getContent());
}
}
public ListWithViewer<Vertex> kuerzestenWegFinden(String pStart, String pZiel){
graph.setAllEdgeMarks(false);
start = graph.getVertex(pStart);
ziel = graph.getVertex(pZiel);
besterWeg = new ListWithViewer<>();
aktuellerWeg = new ListWithViewer<>();
besteLaenge = 10000000;
aktuelleLaenge = 0;
start.setMark(true);
aktuellerWeg.append(start);
kuerzestenWegFindenBacktracking(start);
return besterWeg;
}
private void kuerzestenWegFindenBacktracking(Vertex pNode) {
if(pNode.equals(ziel)){
if(aktuelleLaenge < besteLaenge){
// aktuellerWeg in besterWeg kopieren
kopiereListeIn(aktuellerWeg,besterWeg);
besteLaenge = aktuelleLaenge;
}
return;
}
List<Vertex> nachbarn = graph.getNeighbours(pNode);
for(nachbarn.toFirst(); nachbarn.hasAccess(); nachbarn.next()){
Vertex derNachbar = nachbarn.getContent();
if(!derNachbar.isMarked()){
derNachbar.setMark(true);
aktuellerWeg.append(derNachbar);
double distanz = graph.getEdge(derNachbar, pNode).getWeight();
aktuelleLaenge += distanz;
kuerzestenWegFindenBacktracking(derNachbar);
aktuelleLaenge -= distanz;
aktuellerWeg.toLast();
aktuellerWeg.remove();
derNachbar.setMark(false);
}
}
return;
}
Backtracking für Arrays: Magisches Quadrat
In ein magisches Quadrat werden die Zahlen von 1 bis 9 (bzw. 1 bis 16, 1 bis 25, ...) eingetragen. Die Summe muss in jeder Zeile, Spalte und Diagonale gleich sein.
Lösungen dieses Problems lassen sich gut mit Backtracking finden.
Implementierung
Die folgende Implementierung löst ein 3x3 Quadrat.
- Als Datenstruktur muss nur das Quadrat bereitgestellt werden, das ein 3x3-Array ist.
- Die Stufen des Backtracking-Verfahrens sind hier die Felder; dabei gehen die Stufen von 0 bis 8.
- Aus der Stufe kann man die Koordinaten des Feldes berechnen.
- Das eigentliche Backtracking findet in der Methode
public void findeLoesung(int pStufe)
statt; diese Methode wird von der Rahmenmethodepublic void findeLoesung()
aufgerufen.
public class MagischesQuadratFertig {
private int[][] quadrat;
MagischesQuadratFertig(){
quadrat = new int[3][3];
initialisiere();
}
public void initialisiere(){
for ( int y=0 ; y < 3 ; y++ ) {
for ( int x=0 ; x < 3 ; x++ ) {
quadrat[x][y] = 0;
}
}
}
public void findeLoesung(){
findeLoesung(0);
System.out.println("*** Loesung: ***");
this.ausgeben();
}
//Die eigentliche Backtracking-Methode!
public boolean findeLoesung(int pStufe){
ausgeben();
// Abbruch, wenn pStufe ueber das Quadrat hinausgeht.
if(pStufe == 3*3){
return false;
}
// aus pStufe die x-Koordinate und die y-Koordinate berechnen
int x = pStufe % 3;
int y = pStufe / 3;
// jetzt fuer pStude alle Moeglichkeiten durchlaufen,
// d.h. die Zahlen von 1 bis 9
for(int i=1; i<= 3*3; i++){
// die Zahl i an die richtige Stelle in das Quadrat eintragen
quadrat[x][y] = i;
// Wenn es die Zahl schon einmal gibt, dann zur naechsten Zahl weiter.
if(esGibtDoppelte()){
continue;
}
// wenn man ein magisches Quadrat gefunden hat, dann true zurueckgeben.
if(magisch()){
return true;
}
// es gibt keine Doppelten, aber das Quadrat ist (noch) nicht magisch
// d.h. man baut die Teilloesung weiter aus.
// Dafuer ein rekursiver Aufruf fuer die naechsthoehere Stufe.
// Der Aufruf gibt zurueck, ob das Ausbauen der Teilloesung zum Erfolg fuehrte.
boolean fertig = findeLoesung(pStufe + 1);
if(fertig){
return true;
}
} // end for
// Auf dieser Stufe wurden alle Zahlen von 1 bis 9 durchprobiert, ohne Erfolg.
// Man loescht die Information auf dieser Stufe...
quadrat[x][y] = 0;
// und gibt false zurueck
return false;
}
public boolean esGibtDoppelte(){
boolean[] dabei = new boolean[3*3+1];
for ( int y=0 ; y < 3 ; y++ ) {
for ( int x=0 ; x < 3 ; x++ ) {
int index = quadrat[x][y];
if(index != 0 && dabei[index]){
return true;
}
dabei[index] = true;
}
}
return false;
}
public boolean magisch(){
if(esGibtDoppelte()){
return false;
}
// Summen testen
int s=0, t=0;
// 1. Diagonale
for ( int x=0 ; x < 3 ; x++ ) s+=quadrat[x][x];
//2. Diagonale
for ( int x=0 ; x < 3 ; x++ ) t+=quadrat[3-x-1][x];
if (t != s) return false;
// Zeilen
for ( int y=0 ; y < 3 ; y++ ) {
int k=0;
for ( int x=0 ; x < 3 ; x++ ) {
k += quadrat[x][y];
}
if (k != s) return false;
}
for ( int x=0 ; x < 3 ; x++ ) {
int k=0;
for ( int y=0 ; y < 3 ; y++ ) {
k += quadrat[x][y];
}
if (k != s) return false;
}
System.out.println("*** It's magic!!! ***");
return true;
}
public void ausgeben(){
for ( int y=0 ; y < 3 ; y++ ) {
for ( int x=0 ; x < 3 ; x++ ) {
System.out.print(quadrat[x][y]);
}
System.out.print(" ");
}
System.out.println();
}
public static void main(String[] args) {
MagischesQuadratFertig mq = new MagischesQuadratFertig();
mq.findeLoesung();
}
}
Aufgabe: Backtracking für Arrays: Rucksackproblem selber programmieren
Das Rucksackproblem lässt anhand des folgenden Beispiels beschreiben:
- In einen Rucksack kann man 200kg laden (=ganz schön schwer...).
- Es gibt die folgenden Gewichte: 28, 57, 33, 18, 99, 42, 17, 52
- Welche Gewichte muss man in den Rucksack laden, damit die 200kg möglichst gut ausgenutzt, aber nicht überschritten werden?
Implementierung zum Selbermachen
Die folgende Implementierung hält neben gewichte
und maxGewicht
noch folgende Datenstrukturen bereit:
boolean[] teilLoesung
: In diesem Array wird festgehalten, welche der Gewichte bei der aktuell untersuchten Lösung dabei sind.boolean[] besteLoesung
: In diesem Array wird die bisher beste Lösung festgehalten.
Diese Variablen müssen natürlich im Laufe des Backtracking immer aktualisiert werden.
Die Implementierung ist noch nicht vollständig; der Backtracking-Teil muss hier noch programmiert werden; dafür muss die Methode sucheBesteLoesung(int pStufe)
ergänzt werden.
Viel Spaß beim Programmieren!
public class Rucksackproblem {
public int[] gewichte = {28, 57, 33, 18, 99, 42, 17, 52};
public int maxGewicht = 200;
public boolean[] teilLoesung = new boolean[gewichte.length];
public boolean[] besteLoesung = new boolean[gewichte.length];
public void sucheBesteLoesung(){
//Vorbereitung
for(int i=0; i<teilLoesung.length; i++){
teilLoesung[i] = false;
}
kopiereInBesteLoesung();
// los gehts!
sucheBesteLoesung(0);
System.out.println("*** Beste Loesung: ***");
ausgeben(besteLoesung);
}
public void sucheBesteLoesung(int pStufe) {
dabeiArrayAusgeben();
//TODO
}
public void ausgeben(boolean[] b){
System.out.print(berechneGewicht(b)+": ");
for(int i=0; i<b.length; i++){
if(b[i]){
System.out.print(gewichte[i]+",");
}
}
System.out.println();
}
public void dabeiArrayAusgeben(){
for(int i=0; i<teilLoesung.length; i++){
if(teilLoesung[i]){
System.out.print("+");
}
else{
System.out.print("-");
}
}
System.out.println();
}
public void kopiereInBesteLoesung(){
for(int i=0; i<teilLoesung.length; i++){
besteLoesung[i] = teilLoesung[i];
}
}
public int berechneGewicht(boolean[] b){
int ergebnis = 0;
for(int i=0; i<b.length; i++){
if(b[i]){
ergebnis += gewichte[i];
}
}
return ergebnis;
}
public static void main(String[] args) {
Rucksackproblem rp = new Rucksackproblem();
rp.sucheBesteLoesung();
}
}