Durchläufe durch Graphen SS12

Aus ProgrammingWiki
< AuK
Archivierte Version vom 2012-05-13T13:50:53+00:00. Aktuelle Version

Inhaltsverzeichnis

Der Graphendurchlauf (engl. graph traversal) bildet die Grundlage vieler Graphenalgorithmen. Die Idee besteht darin, alle Knoten bzw. Kanten in einem Graphen systematisch zu besuchen, um beispielsweise Informationen über dessen Struktur zu sammeln.

Croxeldyffic_Labyrinth02.png
Ein Labyrinth mit Startpunkt (grün) und Ziel (rot)

Als Motivation für die Lösung dieses Problems soll folgendes Beispiel dienen: Eine Person befindet sich in einem Labyrinth, und sucht einen Ausgang, indem sie alle möglichen Kreuzungen von Gängen in Augenschein nimmt. Übertragen auf die Graphentheorie, existieren hierfür zwei bekannte algorithmische Lösungsverfahren, die Hauptgegenstand dieser Vorlesung sind. In unserem Beispiel sind die Gänge des Labyrinths die Kanten und alle Kreuzugen die Knoten des Graphen.

Stellen wir uns vor, die eingeschlossene Person folgt einem bestimmten Gang solange, bis sie in eine Sackgasse gerät, zur letzten Kreuzung zurückkehrt und einem anderen Weg auf die gleiche Art verfolgt. Dieses systematische Vordringen in die "Tiefe" nennen wir Tiefensuche. Sucht jedoch eine Gruppe von Leuten den Ausgang, so ist es wahrscheinlicher, dass sie gemeinsam vom Startpunkt ausschwärmen in die "Breite" vordringen. Analog dazu sprechen wir von einer Breitensuche.

Am Ende des Kapitels wird es uns möglich sein, das Labyrinth-Problem mithilfe der beiden Algorithmen zu lösen.

Hinweis: Im Folgenden wird häufig von Suche die Rede sein, obwohl es sich meistens eher um einen Durchlauf handelt. Durch die Übernahme der englischen Begrifflichkeiten DFS und BFS (depth-first und breadth-first search) haben sich die Bezeichnungen Tiefen- und Breitensuche anstelle von -durchlauf eingebürgert. Wir hoffen, damit kein allzu große Verwirrung zu stiften.


Note: Durchlauf (traversal) $\neq$ Suche (search)!
Achso, und laby etwas einfacher, vllt #6, zu demo-zwecken...
Applets:
//note to self2: applet: http://www.cosc.canterbury.ac.nz/mukundan/dsal/GraphAppl.html meh~
//note to self2.1: dfs-applet: http://www.lupinho.net/lupinho.de/gishur/html/DFSApplet.html
//note to self2.2: bfs-applet: http://www.lupinho.net/lupinho.de/gishur/html/BFSApplet.html
//w00t~
//note to self3: eu/hm-applets zum knobeln: http://www.cut-the-knot.org/Curriculum/Combinatorics/GraphPractice.shtml
//note to self4: laby-applet: http://www.swisseduc.ch/informatik/diskrete_mathematik/tiefensuche_labyrinth/applet.html

Wiederholung

Zum Verständnis der später vorgestellten Graphenalgorithmen sind grundlegende Kenntnisse über Graphen und spezielle Datenstrukturen notwendig, die jedoch bereits Themen in vorigen Veranstaltungen waren und im Folgenden nur aufgefrischt werden sollen.

Graphrepräsentation

\rightarrow siehe Vorlesung Graphrepräsentation

Kontrollfragen:

      
  1. Was ist der Unterschied zwischen gerichteten und ungerichteten Graphen?
         
    Lösung
           

      In einem ungerichteten Graphen kann eine Kante (u,v) in beliebiger Richtung überquert werden, während in einem gerichteten Graphen die zulässige Durchlaufrichtung durch einen Pfeil gekennzeichnet ist (ähnlich einer Einbahnstraße).

      Will man einen ungerichteten Graphen gerichtet darstellen, so verdoppelt sich die Kantenmenge, d.h. enthält die Kantenmenge K des ungerichteten Graphen die Kante (u,v), muss die Kantenmenge K des gerichteten Graphen die Kanten (u,v) und (v,u) enthalten.

             
           
        
    1. Welche Darstellungsformen für Graphen gibt es?
      1. Wie können diese im Computer realisiert werden?
      2. Welche ist am besten?
      Lösung
           
        • Feld: Ein Adjazenzfeld ist eine sehr einfache Darstellungsform, in der alle Zielknoten in einem Array geordnet nach Startknoten gespeichert werden.
        • Liste: Eine Adjazenzliste ist eine verkettete Liste mit Nachfolgern bzw. Nachbar eines Knotens.
        • Matrix: Eine Adjazenzmatrix ist ein mehrdimensionales (V \times V mit V als Knotenanzahl) Array mit booleschen Variablen, in der a[u,v] auf 1 oder true gesetzt wird, wenn eine Kante (u,v) existiert. Analog dazu gibt eine Inzidenzmatrix (V \times E-großes Array) die Inzidenzbeziehungen zwischen Knoten und Kanten in einem Graphen an.
      1. Wir vergleichen die drei bzw. vier Darstellungsformen miteinander:
        
        
        
        
        
      Adjazenzfeld Adjazenzliste Adjazenzmatrix Inzidenzmatrix
      Speicheraufwand \mathcal{O}\: (|V| + |E|) \mathcal{O}\: (|V| + |E|) \mathcal{O}\: (|V|^2) \mathcal{O}\: (|V| \cdot |E|)
      Nachbarabfrage \mathcal{O}\: (d)^*
      * d = max. Ausgangsgrad
      \mathcal{O}\: (d)^*
      * d = max. Ausgangsgrad
      \mathcal{O}\: (1) \mathcal{O}\: (|E|)
      Vorteile + kompakte Darstellung + kompakte Darstellung
      + effizient für dynamische Graphen
      + schneller Zugriff
      + effizient bei dichten Graphen
      + schnelles Löschen von Kanten
      + besser bei lichten Graphen
      Nachteile - nur für statische Graphen - ineffizient für dichte Graphen - hoher Speicheraufwand
      - ineffizient bei lichten Graphen
      - hoher Speicheraufwand bei dichten Graphen
      - langsames Löschen von Kanten

      Fazit: Die Anzahl der Kanten ist bei den meisten Graphen höher als die Knotenanzahl, daher ist aufgrund der schnellen Zugriffszeit die Adjazenzmatrix als Darstellungsform empfohlen.

              
           
           

    Bäume und Preorder-Traversierung

    \rightarrow siehe Vorlesung Sortierte Folgen

    Datenstrukturen Stack und Queue

    \rightarrow siehe Vorlesung Prioritätswarteschlangen

    Kontrollfragen:

        
    1. Nach welchem Prinzip arbeitet eine Warteschlange?
      Lösung
           

      Eine Warteschlange (engl. queue) arbeitet nach dem FIFO-Prinzip, d.h. das Element, das zuerst in die Warteschlange abgelegt wurde, wird zuerst bearbeitet (ähnlich einer Schlange im Supermarkt).

      Folgende Operationen sind notwendig:

        
    2. Element hinzufügen (enqueue): Das Element wird nach dem letzten Element eingefügt.
    3. erstes Element anzeigen (peek): Das zuerst hinzugefügte Element wird ausgegeben.
    4. Element löschen (dequeue): Das zuerst hinzugefügte Element wird ausgegeben und aus der Schlange entfernt.
    5. Prüfung, ob Schlange leer ist (isEmpty)
    6. Implementierung:

        
    7. Nach welchem Prinzip arbeitet ein Stack?
    8. Lösung

      Im Gegensatz zur Warteschlange arbeitet ein Stack nach dem LIFO-Prinzip, d.h. das Element, das zuletzt auf den Stack abgelegt wurde, wird zuerst bearbeitet (ähnlich einem Stapel Bücher etc.).

      Folgende Operationen sind notwendig:

        
    9. Element hinzufügen (push): Das Element wird an erster Stelle eingefügt.
    10. erstes Element anzeigen (peek): Das zuletzt hinzugefügte Element wird ausgegeben.
    11. Element löschen (pop): Das zuletzt hinzugefügte Element wird ausgegeben und aus dem Stack entfernt.
    12. Prüfung, ob Stack leer ist (isEmpty)
    13. Implementierung:

           

    Grundlagen zum Graphendurchlauf

    Allgemeiner Durchlauf-Algorithmus

    Unsere zentrale Frage lautet: Wie können wir die Knoten eines Graphen durchlaufen, sodass jeder Knoten mindestens einmal besucht wird?

    Um am Ende eine vernünftige Durchlaufordnung angeben zu können, überlegen wir uns folgendes Verfahren:

      
  2. Eingaben:
    1. Graph G als Adjazenzlisten- oder -matrixdarstellung
    2. eine Menge von Knoten v \in V mit den Zuständen unbesucht und besucht
    3. eine Arbeitsliste L, die besuchte aber noch nicht abgearbeitete Knoten beinhaltet.
  3. Algorithmus:
    1. Initialisierung: nur der Startknoten v ist mit besucht markiert und befindet sich in der Arbeitsliste L
    2. solange L nicht leer ist:
      1. wähle einen Knoten v aus L
      2. wähle und markiere einen unbesuchten Nachbarn u von v als besucht und speichere u in die Arbeitsliste L
      3. falls v keine unbesuchten Nachbarn besitzt, entferne v aus L

    Algorithmus: Graphendurchlauf_allgemein (Graph G, Knoten v \in V(G), Arbeitsliste L)
    \begin{array}{l|l} 1 & v \leftarrow besucht\\ 2 & L \leftarrow L \oplus v\\ 3 & \mathbf{while}\; (L \neq\: \emptyset)\; \mathbf{do} \\ 4 &\;\;\;\;\; \text{\small waehle Knoten } v \in L \\ 5 &\;\;\;\;\;\; \mathbf{if}\; (\!\text{\small es gibt } u \in V(G) \text{\small mit } u=Nachbar\; von\; v \text{\small und } u=unbesucht)\; \mathbf{then} \\ 6 &\;\;\;\;\;\;\;\;\;\;\;\; u \leftarrow besucht\\ 7 &\;\;\;\;\;\;\;\;\;\;\;\; L \leftarrow L \oplus u\\ 8 &\;\;\;\;\;\; \mathbf{else}\; L \leftarrow L \ominus u \\ \end{array}
    end Graphendurchlauf_allgemein

  4. Die Arbeitsliste L

    Die Art und Weise wie die einzelnen besuchten Knoten in die Arbeitsliste L eingefügt werden sollen, ist beim Graphendurchlauf von großer Bedeutung. Hierfür gibt es zwei Möglichkeiten.

    Die Arbeitsliste als Stack

    Wird die Arbeitsliste als Stack implementiert, wird jeder besuchte Knoten nach dem LIFO-Prinzip eines Stacks auf L abgelegt.

    Arbeitsweise:

        
    1. markiere den Startknoten v als besucht und legen ihn auf den leeren Stack ab
    2. solange der Stack nicht leer ist:
      1. ermittle alle unbesuchten Nachbarn des obersten Knotens v im Stack und lege sie auf den Stack ab
      2. falls der oberste Knoten keine unbesuchten Nachbarn besitzt, entferne ihn aus dem Stack und fahre mit dem neuen ersten Element fort.

    Mit dieser Implementierung lässt sich die Tiefensuche realisieren.

    Die Arbeitsliste als Queue

    Croxeldyffic_Queue_med.gif
    Arbeitsliste L als Queue

    Wird die Arbeitsliste dagegen als Queue implementiert, werden die besuchten Knoten nach dem FIFO-Prinzip einer Warteschlange an L angefügt.

    Arbeitsweise:

        
    1. markiere den Startknoten v als besucht und füge ihn in die leere Queue ein
    2. solange die Queue nicht leer ist:
      1. ermittle alle unbesuchten Nachbarn des ersten Knotens v in der Queue und hänge sie an die Warteschlange an
      2. falls v keine unbesuchten Nachbarn besitzt, entferne v aus der Queue und fahre mit dem neuen ersten Element fort

    Mit dieser Implementation kann die Breitensuche realisiert werden.

    Zusammenhängende Graphen

    Unsere interne Graphenrepräsentation

    • definitionen von graph, vertex, edge und was sonst nicht alles
    • kleine demonstrationen

    Knoten


    Graph


    Übungsaufgaben:

        
    1. Zeichnen Sie die beiden Graphen G_{bsp01} und G_{bsp02} per Hand.
    2. Sind beide Graphen zusammenhängend?
    Lösung
        
    1. mögliche Anordnung der Knoten:
    2.   
    3. Graph G_{bsp01} ist ein zusammenhängender Digraph, während Graph G_{bsp02} einen unzusammenhängenden, ungerichteten Graphen repräsentiert.

    Tiefensuche

    Die Tiefensuche beginnt mit einem beliebigen Startknoten u \in V(G) und folgt einer mit u adjazenten Kante, bis zum nächsten Knoten v\in V(G) der noch nicht besucht wurde(unmarkiert ist). Von diesem aus wird wieder ein erreichbarer Knoten gesucht. Man folgt also solange einem Kantenzug, bis man ein Blatt erreicht hat. Alle dabei entdeckten Knoten werden markiert.

    Nun verfolgt man den entstandenen Kantenzug solange zurück(backtracing), bis man einen Knoten findet, mit dem noch unbesuchte Knoten verbunden sind. Wird solch ein Knoten entdeckt, wählt man ihn als neuen Startknoten und folgt wieder seinen adjazenten Kanten.

    Anschließend wird solange ein neuer unmarkierter Knoten als Startknoten gewählt und abgearbeitet, bis alle Knoten markiert sind.


    Auf diese Weise entsteht ein Suchwald. Obwohl der Algorithmus einen Graphen G immer vollständig abarbeitet, können abhänig vom Startknoten und der konkreten Implementierung verschiedene Suchwälder entstehen. Meistens ist aber jeder Wald sinnvoll verwendbar.

    Iterativ mit Stack

    Croxeldyffic_Dfs01.gif
    Das Voranschreiten einer iterativen Tiefensuche in einem gerichteten Graphen
    • verfahren

    Algorithmus: Tiefendurchlauf (Graph G, Knoten v \in V(G), Stack S)
    \begin{array}{l|l} 1 & v \leftarrow besucht\\ 2 & S \leftarrow push(v)\\ 3 & \mathbf{while}\; (S \neq\: \emptyset)\; \mathbf{do} \\ 4 &\;\;\;\;\; \text{\small waehle Knoten } v \in S \text{\small mit } v = peek(S) \\ 5 &\;\;\;\;\;\; \mathbf{if}\; (\!\text{\small es gibt } u \in V(G) \text{\small mit } u=Nachbar\; von\; v \text{\small und } u=unbesucht)\; \mathbf{then} \\ 6 &\;\;\;\;\;\;\;\;\;\;\;\; u \leftarrow besucht\\ 7 &\;\;\;\;\;\;\;\;\;\;\;\; S \leftarrow push(u)\\ 8 &\;\;\;\;\;\; \mathbf{else}\; S \leftarrow pop(u) \\ \end{array}
    end Tiefendurchlauf

    Rekursive DFS


    • Eingaben:
          
      1. Graph G als Adjazenzlisten- oder -matrixdarstellung
      2. Eine Menge von Knoten V(G)mit den Zuständen:
        1. farbe=weiss für unbesuchte Knoten
        2. farbe=grau für besuchte Knoten
        3. farbe=schwarz für abgeschlossene Knoten

        4. \pi=nil (der Vorgänger)
        5. d[u]=0 (die Zeit der Entdeckung)
        6. f[u]=0 (die Zeit des Abschließens)

      Algorithmus: DFSrek (Graph G)
      \begin{array}{l|l} 1 & \mathbf{for}\; alle Knoten\; u\in V(G)\\ 2 &\;\;\;\;\; \mathbf{do} farbe[u] \leftarrow weiss\\ 3 &\;\;\;\;\; \pi[u] \leftarrow NIL\\ 4 & zeit \leftarrow 0\\ 5 & \mathbf{for}\; alle Knoten\; u\in V(G)\\ 6 &\;\;\;\;\; \mathbf{do}\;\mathbf{if}\; farbe[u] = weiss\\ 7 &\;\;\;\;\;\;\;\;\;\;\;\mathbf{then} \;DFSvisit(u)\\ \end{array}
      end DFSrek

      Algorithmus: DFSvisit (u)
      \begin{array}{l|l} 1 & farbe[u] \leftarrow grau\\ 2 & zeit \leftarrow zeit+1\\ 3 & \pi[u] \leftarrow zeit\\ 4 & \mathbf{for}\; alle\; v \in Adj(u)\\ 5 & \;\;\;\;\; \mathbf{do}\;\mathbf{if}\; farbe[v] = weiss\\ 6 & \;\;\;\;\;\;\;\;\;\;\;\mathbf{then} \;\pi[v] \leftarrow u\\ 7 & \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\ DFSvisit(v)\\ 8 & farbe[u] \leftarrow schwarz\\ 9 & f[u] \leftarrow zeit \leftarrow zeit+1\\ \end{array}
      end DFSvisit

    Croxeldyffic_Dfsrek.GIF
    Das Voranschreiten der rekursiven Tiefensuche mit Einfärben in einem gerichteten Graphen

    erzeugter Suchbaum

    Breitendurchlauf/ -suche

    • allgemeines mit grafik
    Croxeldyffic_Bfs01.gif
    Das Voranschreiten einer Breitensuche in einem gerichteten Graphen

    Abgeleitet von der allgemeinen Durchlaufvorschrift unter Benutzung einer Warteschlange Q als Arbeitsliste, beschreibt folgender Algorithmus die Arbeitsweise der Breitensuche.

    Algorithmus: Breitendurchlauf (Graph G, Knoten v \in V(G), Queue Q)
    \begin{array}{l|l} 1 & v \leftarrow besucht\\ 2 & Q \leftarrow enqueue(v)\\ 3 & \mathbf{while}\; (Q \neq\: \emptyset)\; \mathbf{do} \\ 4 &\;\;\;\;\; \text{\small waehle Knoten } v \in Q \text{\small mit } v = peek(Q) \\ 5 &\;\;\;\;\;\; \mathbf{if}\; (\!\text{\small es gibt } u \in V(G) \text{\small mit } u=Nachbar\; von\; v \text{\small und } u=unbesucht)\; \mathbf{then} \\ 6 &\;\;\;\;\;\;\;\;\;\;\;\; u \leftarrow besucht\\ 7 &\;\;\;\;\;\;\;\;\;\;\;\; Q \leftarrow enqueue(u)\\ 8 &\;\;\;\;\;\; \mathbf{else}\; Q \leftarrow dequeue(u) \\ \end{array}
    end Breitendurchlauf


    Der Algorithmus kann aufgrund seiner klaren Struktur sehr einfach in Programmcode übertragen werden. Wie wir bereits bei der DFS-Implementierung mit Stack bemerkt haben, können wir mit diesem Algorithmus nur zusammenhängende Graphen durchlaufen.

    Funktion
      
      

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40

    public void bfs (int start)

    {

      System.out.println( "BFS-Durchlaufordnung:" );
      start--;
      knotenliste[start].bereitsBesucht = true;
      displayVertex(start);
      theQueue.enqueue(start);
      while ( !theQueue.istLeer() )
      {
         int v = naechsterUnbesuchterNachbar( theQueue.peek() );
         if (v == -1)
            theQueue.dequeue();
         else
         {
            knotenliste[v].bereitsBesucht = true;
            displayVertex(v);            
            theQueue.enqueue(v); 
         }
      }
      for(int i=0; i<knotenanzahl color="#C92120"><b>if</b>(knotenliste[i].bereitsBesucht==false){
            System.out.println(<font color="#00F"><b>"... Graph nicht zusammenhängend!"</b></font>);
       	 break;}
         <font color="#C92120"><b>else</b></font> continue;
         }
      <font color="#C92120"><b>for</b></font>(<font color="#C92120"><b>int</b></font> j=0; j<knotenanzahl j knotenliste false><p>}</p></knotenanzahl></knotenanzahl>

    Übungsaufgaben:

        
    1. Durchlaufen Sie die folgenden Graphen zunächst per Hand und überprüfen Sie Ihre Lösung anschließend mit dem Programm.
      1. G_1
      2. G_2 (Was fällt bei diesem Graphen auf? Interpretieren Sie die Programmausgabe.)
    2.   
    3. Testen Sie das BFS-Applet auf: *link* mit
      1. einem ungerichteten Graphen.
      2. einem gerichteten Graphen.
      3. einen unzusammenhängenden Graphen.
    • graph geben und bfs per hand berechnen lassen, mit prog überprüfen
    • applet

    erzeugter Suchbaum

    Iterative Tiefensuche (Kombination)

    Anwendung/ weiterführende Themen

    Topologisches Sortieren

    Die Topologische Sortierung reiht alle Knoten eines azyklischen Graphen G in einer Linie auf. u ligt vor v falls es in G eine Kante (u,v) gibt. Alle gerichteten Kanten zeigen nun von links nach rechts.

    Topologisch sortierte Knoten eines Graphen G sind in der umkehrten Reihenfolge ihrer Endzeiten in der DFS angeordnet.

    Topological-Sort(G)

        
    1. DFS(G) aufrufen um die Endzeiten f[u] für alle Knoten u zu bekommen
    2. jeden abgearbeiteten Knoten am Kopf einer Liste L einfügen
    3. L ist nun topologisch sortiert

    Die topologische Sortierung wird zur Darstellung der Priorität von Ereignissen verwendet.

    Das Beispiel:
    Professor Bumstead führt eine topologische Sortierung seiner Kleidung durch, wenn er sich anzieht.

    Wenn man entwas mit den Startwerten der DFS experimentiert, sieht man, dass sich aufgrund der verschiedenen Suchwälder auch verschiedene Topologische sortierungen einstellen. Es wird aber nie z.B. das Jacket vor der Kravatte angezogen. Die Position von isolierten Knoten(der Uhr) ist hingegen beliebig.


    Euler und CPP

    Eulerpfad/-kreis

    Das Eulerkreis Problem hat seinen Ursprung in dem Vorhaben von Leonhard Eulers, alle fünf Stadtteile von Königsberg zu besuchen, dabei jede Brücke aber nur einmal zu überqueren und am Startpunkt wieder anzukommen.
    Siehe dazu:http://programmingwiki.de/AuK/Graphrepr%C3%A4sentationen_SS12#K.C3.B6nigsberger_Br.C3.BCckenproblem
    Bevor man versucht einen Eulerkreis zu finden, wird der Graph G geprüft, ob er überhaupt einen zulässt. Dazu ermittelt man den Grad aller Knoten und zählt wieviele geraden/ungeraden Grades sind.

     
      eulersch wenn:
      gerichtet und
    1. stark zusammenhängend
    2. bei jedem Knoten ist Eingangsgrad=Ausgangsgrad
      ungerichtet und
    1. zusammenhängend
    2. jeder Knoten hat geraden Grad


    Wenn zwei oder kein Knoten ungeraden Grad haben, eistiert in einem ungerichteten, zusammenhängenden Graphen ein Eulerweg/pfad.


    Auch wenn schnellere Algorithmen gibt(z.B. der Algorithmus von Hierholzer) wird hier der Algorithmus von Fleury kurz vorgestellt:

      Algorithmus von Fleury
     
    1. wähle beliebigen Startknoten u \in V(G)
    2. wähle eine an u angrenzende Kante, wähle zuerst Kanten die keine Brückenkanten sind
    3. füge die Kante der Liste des Eulerkreises hinzu, Kante markieren
    4. den anderen Knoten der gewählten Kante als neuen aktuellen knoten wählen
    5. existieren noch unmarkierte Kanten wieder zu Schritt 2

    Laufzeit: \mathcal{O}(|E|^2) \leftarrow (Brückenkante finden \mathcal{O}(|E|), Kanten absuchen \mathcal{O}(|E|), Iterationen |E|


    Hamilton und TSP

    Starke Zusammenhangskomponenten


    Java-Test