Backtracking: Unterschied zwischen den Versionen

Aus SibiWiki
Zur Navigation springen Zur Suche springen
Zeile 87: Zeile 87:
   
   
     '''// Attribute fuer das Backtracking'''
     '''// Attribute fuer das Backtracking'''
     public ListWithViewer<GraphNode> besterWeg;
     public ListWithViewer<Vertex> besterWeg;
     public ListWithViewer<GraphNode> aktuellerWeg;
     public ListWithViewer<Vertex> aktuellerWeg;
     public int besteLaenge;
     public int besteLaenge;
     public int aktuelleLaenge;
     public int aktuelleLaenge;
     public GraphNode start;
     public Vertex start;
     public GraphNode ziel;
     public Vertex ziel;
   
   
     '''//Methoden'''
     '''//Methoden'''
   
   
     '''public ListWithViewer<GraphNode> kuerzestenWegFinden(String pStart, String pZiel){'''
    /**
         graph.resetMarks();
    * Kopiert den Inhalt einer Liste in eine andere Liste.
    * Der Inhalt der Ziel-Liste wird dabei ersetzt.
         start = graph.getNode(pStart);
    * @param herkunftsListe
        ziel = graph.getNode(pZiel);
    * @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<>();
         besterWeg = new ListWithViewer<>();
         aktuellerWeg = new ListWithViewer<>();
         aktuellerWeg = new ListWithViewer<>();
         besteLaenge = 10000000;
         besteLaenge = 10000000;
         aktuelleLaenge = 0;
         aktuelleLaenge = 0;
         start.setMark(true);
         start.mark();
         aktuellerWeg.append(start);
         aktuellerWeg.append(start);
     
         kuerzestenWegFindenBacktracking(start);
         kuerzestenWegFindenBacktracking(start);
 
         return besterWeg;
         return besterWeg;
     }
     }
 
 
     '''private void kuerzestenWegFindenBacktracking(GraphNode pNode) {'''
     '''private void kuerzestenWegFindenBacktracking(Vertex pNode) {'''
         if(pNode.equals(ziel)){
         if(pNode.equals(ziel)){
             if(aktuelleLaenge < besteLaenge){
             if(aktuelleLaenge < besteLaenge){
                // besterWeg leeren
                for(besterWeg.toFirst();!besterWeg.isEmpty();){
                    besterWeg.remove();
                }
                 // aktuellerWeg in besterWeg kopieren
                 // aktuellerWeg in besterWeg kopieren
                 for(aktuellerWeg.toFirst();aktuellerWeg.hasAccess();aktuellerWeg.next()){
                 kopiereListeIn(aktuellerWeg,besterWeg);
                    besterWeg.append(aktuellerWeg.getObject());
                }
                 besteLaenge = aktuelleLaenge;
                 besteLaenge = aktuelleLaenge;
             }
             }
             return;
             return;
         }
         }
         List<GraphNode> nachbarn = graph.getNeighbours(pNode);
         List<Vertex> nachbarn = graph.getNeighbours(pNode);
         for(nachbarn.toFirst(); nachbarn.hasAccess(); nachbarn.next()){
         for(nachbarn.toFirst(); nachbarn.hasAccess(); nachbarn.next()){
             GraphNode derNachbar = nachbarn.getContent();
             Vertex derNachbar = nachbarn.getContent();
             if(!derNachbar.isMarked()){
             if(!derNachbar.isMarked()){
                 derNachbar.mark();
                 derNachbar.setMark(true);
                 aktuellerWeg.append(derNachbar);
                 aktuellerWeg.append(derNachbar);
                 double distanz = graph.getEdgeWeight(derNachbar, pNode);
                 double distanz = graph.getEdge(derNachbar, pNode).getWeight();
                 aktuelleLaenge += distanz;
                 aktuelleLaenge += distanz;
             
                 '''kuerzestenWegFindenBacktracking(derNachbar);'''
                 '''kuerzestenWegFindenBacktracking(derNachbar);'''
             
                 aktuelleLaenge -= distanz;
                 aktuelleLaenge -= distanz;
                 aktuellerWeg.toLast();
                 aktuellerWeg.toLast();
                 aktuellerWeg.remove();
                 aktuellerWeg.remove();
                 derNachbar.unmark();
                 derNachbar.setMark(false);
             }
             }
         }
         }

Version vom 16. Januar 2018, 18:15 Uhr


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

Graph mit fünf Städten

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 von besterWeg
  • aktuellerWeg: Der Weg, der gerade untersucht wird.
  • aktuelleLaenge: Die Länge von aktuellerWeg

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 Rahmenmethode public 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[] dabei: In diesem Array wird festgehalten, welche der Gewichte bei der aktuell untersuchten Lösung dabei sind.
  • int erreichtesGewicht: Hier wird festgehalten, welches Gewicht die aktuell untersuchte Lösung hat.
  • boolean[] besteLoesung: In diesem Array wird die bisher beste Loesung festgehalten.
  • int bestesGewicht: Hier wird festgehalten, welches Gewicht die bisher beste Lösung hat.

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[] dabei = new boolean[gewichte.length];
   public int erreichtesGewicht = 0;
  
   public boolean[] besteLoesung = new boolean[gewichte.length];
   public int bestesGewicht = 0;
  
   public void sucheBesteLoesung(){
       //Vorbereitung
       for(int i=0; i<dabei.length; i++){
           dabei[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<dabei.length; i++){
           if(b[i]){
               System.out.print(gewichte[i]+",");
           }
       }
       System.out.println();
   }
  
   public void dabeiArrayAusgeben(){
       for(int i=0; i<dabei.length; i++){
           if(dabei[i]){
               System.out.print("+");
           }
           else{
               System.out.print("-");
           }
       }
       System.out.println();
   } 
  
   public void kopiereInBesteLoesung(){
       for(int i=0; i<dabei.length; i++){
           besteLoesung[i] = dabei[i];
       }
   }
  
   public int berechneGewicht(boolean[] p){
       int ergebnis = 0;
       for(int i=0; i<p.length; i++){
           if(p[i]){
               ergebnis += gewichte[i];
           }
       }
       return ergebnis;
   }
  
   public static void main(String[] args) {
       Rucksackproblem rp = new Rucksackproblem();
       rp.sucheBesteLoesung();
   }
}