FB Informatik Prof. Dr. R.Nitsch Programmieren 2 Future Car Projekt Praktikum 6 - Graphen - Speichern von Graphen - Traversieren von Graphen - Kürzeste Wege Reiner Nitsch [email protected] Darstellung von Graphen als Array von Listen • Grundlagen zu Graphen (siehe Vorlesung Mathematik 1) Array von Adjazenzlisten 1 4 2 5 3 6 Ungerichteter Graph G(V,E) V: Knotenmenge E: Kantenmenge 1 2 4 2 5 4 3 5 6 4 1 2 5 5 4 3 2 6 3 6 1 2 2 5 3 5 6 4 2 Gerichteter Graph 5 4 6 6 1 4 2 5 09.10.2008 3 FB Informatik Prof. Dr. R.Nitsch 4 6 Projekt FutureCar Adjazenzlisten 1 Die mit Knoten 5 verbunden Nachbarknoten (= Kantenmenge E5 ) Bietet Antwort auf Fragen zu Graphen G wie z.B. • Speicherbedarf? proportional zur Anzahl Knoten plus Anzahl Kanten O(|V|+|E|) • Wieviele Kanten enden an vi? • Welche Nachbarn vj hat vi? • Existiert Kante E=(vi,vj)? O(|Ei|) Gewichteter Graph: Gewicht als zusätzliche Info der Listenelemente 2 Darstellung von Graphen mit Adjazenzmatrix Adjazenzmatrix a mit Elementen aij aij = 1 wenn Kante E=(vi,vj) in G enthalten, sonst aij=0 1 2 3 4 5 6 Ungerichteter Graph G(V,E) 1 2 3 4 5 6 Gerichteter Graph G(V,E) 09.10.2008 1 2 3 i 4 5 6 1 0 1 0 1 0 0 2 1 0 0 1 1 0 j 3 4 0 1 0 1 0 0 0 0 1 1 1 0 5 0 1 1 1 0 0 6 0 0 1 0 0 1 aij = aji (symmetrisch) 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 1 1 0 0 0 0 0 1 0 0 1 FB Informatik Prof. Dr. R.Nitsch Bietet Antwort auf Fragen zu Graphen G wie z.B. • Welche Kanten enden an vi? • Welche Nachbarn vj hat vi? • Existiert Kante E=(vi,vj)? O(1) Speicherbedarf? O(|V|2) ungünstig wenn G wenige Kanten hat Gewichteter Graph: Gewicht an Stelle von '1' in Matrix eintragen aij ≠ aji (unsymmetrisch) Projekt FutureCar 3 Adjazenzliste des Graphen der FutureCar World 1 #### # ## # # # # # ## ## # ## ## # ## ## # # # ## ## # ## ## Rasterkarte (Grid) von FC-City 3,4 4,4 5,4 6,4 7,4 8,4 3,5 4,5 5,5 6,5 7,5 8,5 2 4 Rasterkarte von FC-City mit XY-Koordinaten 3,6 4,6 5,6 6,6 7,6 8,6 FB Informatik Prof. Dr. R.Nitsch 4,7 5,8 6,5 7,7 5,5 4,6 7,7 5,8 7,6 6,5 5,8 4,6 6,8 7,7 4,6 6,5 3,7 4,7 4,6 3,6 3,7 4,7 5,7 6,7 7,7 8,7 Adjazenzlisten 3,8 4,8 5,8 6,8 7,8 8,8 3,9 4,´9 5,9 6,9 7,9 8,9 5 Graph zur Modellierung der Erreichbarkeitsbeziehungen zwischen den Zellen der Rasterkarte Anmerkung: Wenden ist nicht vorgesehen 3 09.10.2008 Projekt FutureCar 47 55 76 68 37 46 58 46 65 77 47 36 65 77 58 46 77 58 46 65 Adjazenzlisteninfo als Textsequenz 4 Implementierungsempfehlungen 4,7 5,8 6,5 7,7 5,5 4,6 7,7 5,8 7,6 6,5 5,8 4,6 6,8 7,7 4,6 6,5 FB Informatik Prof. Dr. R.Nitsch Node + loc 5,5 + adjList 4,6 7,7 5,8 + Konstruktor: 1..N 1 3,7 4,7 4,6 3,6 Adjazenzlisten Graph - nodes:Node[ ] + Graph(filename:string) +… Empfehlungen: map<Location, Node> nodes • Container zum Verwalten der Knoten? • Knoten in Container einfügen? nodes[ loc ] = Node(…) • Was sollte in den Adjazenzlisten gespeichert werden? Location oder Adressen der Nachbarknoten. • Welcher Datentyp eignet sich für Route? Verwendbarkeit im Navi sicherstellen (Siehe Praktikum 5) 09.10.2008 Projekt FutureCar 5 Traversieren von Graphen FB Informatik Prof. Dr. R.Nitsch • Als Traversieren bezeichnet man das systematische Besuchen aller Knoten und das Durchlaufen jeder Kante eines Graphen. • Algorithmen zum Traversieren eines Graphen dienen als Basis für viele andere grundlegende Algorithmen zur Verarbeitung von Graphen • Man unterscheidet zwischen – Breitentraversierung (breadth-first search, BFS): Die Knoten werden geordnet nach der "Entfernung" von einem Startknoten durchlaufen • zuerst alle Knoten mit 1 Kantenlänge Abstand vom Startknoten • danach alle diejenigen Knoten mit Abstand 2, • danach die mit Abstand 3, usw. – Tiefentraversierung (depth-first search, DFS): Dieser Algorithmus erhöht immer zuerst die Distanz vom Startknoten, bevor er in die Breite geht und Nachbarknoten mit gleicher Distanz besucht (meist rekursiv implementiert) • Bereits besuchte Knoten müssen markiert werden, Node weil sich die Algorithmen sonst in den Kreisen des + loc:Location Graphen verlieren. Markierung + adjList + visited:bool + Konstruktor: 09.10.2008 Projekt FutureCar 6 Tiefentraversierung (Rekursiv) FB Informatik Prof. Dr. R.Nitsch • Rekursiver Algorithmus (in Pseudocode), der ausgehend von einer unmarkierten Ecke vi, alle anderen Knoten vj, j!=i eines Graphen G (genauer: einer Komponente desselben) besucht Node + loc:Location + adjList + visited:bool Funktion: traverse-dfs(v) Zweck: Tiefensuche in einem Graphen Parameter v: Ecke bei dem die Suche beginnt PRE: --POST: Alle Ecken, die von v erreichbar sind, sind gefunden. Markiere v als besucht Bestimme einen Nachbarknoten von v und nenne diesen vnext WHILE(vnext existiert UND noch nicht besucht ist) beginne weitere Tiefensuche bei vnext Wieder zurück, bestimme weiteren Nachbarknoten von v und nenne diesen wieder vnext END WHILE procedure traverse-dfs(v) visited(v) := true vnext := adjList[v] WHILE( exist(vnext) AND NOT visited(vnext)) traverse-dfs(vnext) vnext := succ(vnext) END WHILE Algorithmus in kompakter selbsterklärender, leicht merkbarer Syntax (Programmiersprachenunabhängig) Algorithmus in Umgangssprache 09.10.2008 + Konstruktor: Projekt FutureCar 7 Beispiel zur Tiefentraversierung FB Informatik Prof. Dr. R.Nitsch procedure traverse-dfs(v) visited(v) := true vnext := adjList[v] WHILE( exist(vnext) AND NOT visited(vnext)) traverse-dfs(vnext) vnext := succ(vnext) END WHILE Adjazenzlisten von Seite 2 Startknoten (willkürl.) 2 3 1 2 3 1 2 3 4 5 6 4 5 6 4 5 6 1 2 3 4 09.10.2008 2 5 3 6 2 4 2 5 4 3 5 6 4 1 2 5 5 4 3 2 6 3 6 1 Komplexität: O(|V|+|E|) 1 1 1 4 5 6 Projekt FutureCar Alle Knoten und Kanten besucht! 8 Tiefentraversierung (Iterativ) FB Informatik Prof. Dr. R.Nitsch • Iterativer Algorithmus mit einem Stack, der ausgehend von einer unmarkierten Ecke vi, alle anderen Knoten vj, j!=i eines Graphen G besucht. PRE: --- POST: Alle Ecken, die von v erreichbar sind, sind markiert. procedure traverse-dfs(v) t := empty-stack visited(v) := true push(t,v) WHILE NOT empty(t) DO { v := top(t) vnext := adjList[v] // t ist ein lokaler Stack // markiere v als besucht // lege v auf den Stack // hole oberstes Element aus Stack // hole ersten Nachbarknoten WHILE exist(vnext) AND visited(vnext) DO // schon besucht? vnext := succ(vnext) // Ja! Dann eben den Nächsten END WHILE IF exist(vnext) THEN // Noch einen Unbesuchten gefunden? visit(vnext) // diesen besuchen (und bearbeiten), visited(vnext) := true // als "besucht" markieren und push(t,vnext) // Erst mal auf den Stack damit ... ELSE DO pop(t) END IF END WHILE // Erledigt! Alle Nachbarn von v besucht }09.10.2008 Projekt FutureCar … und hier schon wieder runter! 9 Breitentraversierung FB Informatik Prof. Dr. R.Nitsch • Iterativer Algorithmus, der alle Knoten eines zusammenhängenden Graphen geordnet nach der Entfernung vom Startknoten v durchläuft. – Zuerst werden alle vom Startknoten über 1 Kante erreichbaren Knoten besucht – Danach alle über mindestens 2 Kanten erreichbaren Knoten, usw. – Entsteht formal aus iterativer Tiefentraversierung, wenn der Stack zu einer Queue wird. PRE: --Post: Alle Knoten, die von v erreichbar sind, sind markiert, also besucht worden procedure bfs_node(v) t := empty-queue // Definition einer leeren lokalen Queue visited(v) := true // Starknoten v als "besucht" markieren enqueue(t,v) Die Änderungen gegenüber WHILE NOT empty(t) DO der Tiefensuche sind ROT v := front(t) // vordersten Knoten in t lesen markiert! vnext := adjList[v] // hole ersten Nachbarknoten WHILE exist(vnext) AND visited(vnext) DO // schon besucht? vnext := succ(vnext) // Ja! Dann eben den Nächsten END WHILE IF exist(vnext) THEN // Noch einen Unbesuchten gefunden? visit(vnext) // diesen besuchen (und bearbeiten), visited(vnext) := true // als "besucht" markieren und enqueue(t,vnext) // erst mal in queue einreihen, wo sie bis zur // Bearbeitung ihrer Nachbarknoten warten ELSE DO dequeue(t) // Erledigt! Alle Nachbarn von v wurden besucht END IF 10 Projekt FutureCar END09.10.2008 WHILE Beispiel zur Breitentraversierung FB Informatik Prof. Dr. R.Nitsch procedure bfs_node(v) t := empty-queue visited(v) := true enqueue(t,v) WHILE NOT empty(t) DO v := front(t) vnext := adjList[v] WHILE exist(vnext) AND visited(vnext) DO vnext := succ(vnext) END WHILE IF exist(vnext) THEN visit(vnext) visited(vnext) := true enqueue(t,vnext) 2 t: ELSE dequeue(t) END IF END WHILE Startknoten Adjazenzlisten von Seite 2 5 4 1 3 3 1 2 3 1 2 3 4 5 6 4 5 6 4 5 6 1 2 3 1 2 3 09.10.2008 6 4 5 4 2 5 4 3 5 6 4 1 2 5 5 4 3 2 6 3 6 1 6 2 5 2 Queue 1 4 1 6 Projekt FutureCar Jetzt sind alle Knoten mit Distanz "1Kante" zum Startknoten besucht Alle Knoten und Kanten besucht! 11 Kürzeste Wege mittels Breitensuche FB Informatik Prof. Dr. R.Nitsch • Gesucht ist eine Verbindung (Pfad) zwischen 2 Knoten: – Tiefensuche liefert eine entsprechende Kantenfolge, wenn es eine gibt (aber nicht unbedingt die Kürzeste). – Breitensuche liefert garantiert die Kürzeste. • Aufgabe Mit Hilfe eines Breitensuchverfahrens soll der kürzeste Weg in einem ungewichteten Graphen G vom Startpunkt src zum Zielknoten dest gefunden werden, der über die geringste Anzahl von Kanten verläuft. Dabei wird der Weg so codiert, dass man ihn hinterher rekonstruieren kann. • Lösung – Die Breitentraversierung durchläuft alle Knoten geordnet nach der Kantendistanz zu src. – Der Vorgängerknoten, von dem ausgehend der Knoten v betreten wird, verbindet somit v auf dem kürzesten Wege mit src (keine Kantengewichte!). – Im Bearbeitungsschritt merkt sich der Knoten v daher seinen Vorgängerknoten – Nachdem Knoten dest betreten wurde und dieser sich seinen Vorgängerknoten gemerkt hat, ist die Suche beendet. – Der kürzeste Weg, der dest mit src verbindet, ergibt sich nun, indem man, beginnend bei dest, die Folge der Vorgängerknoten rekonstruiert. – Rückwärts gelesen (std::reverse) ergibt diese Folge den kürzesten Weg. 09.10.2008 Projekt FutureCar 12 Breitensuche des Knotens d ausgehend vom Startknoten s PRE: POST: FB Informatik Prof. Dr. R.Nitsch exist(s), exist(d) Alle Knoten, die von v erreichbar sind, sind markiert, also besucht worden FUNCTION bf_search(s,d) t := empty-queue visited(s) := true pred(s) := nil enqueue(t,s) Ergänzungen zum vorherigen Algorithmus sind ROT markiert // Definition einer leeren lokalen Queue // Starknoten v als "besucht" markieren // s kennt seinen Vorgänger noch nicht Node + loc:Location + adjList Verweis auf Vorgängerknoten + pred WHILE NOT empty(t) AND front(t)!=d DO // Abbruch der Suche wenn d besucht + visited:bool v := front(t) // vordersten Knoten in t lesen + Konstruktor: vnext := adjList[v] // hole ersten Nachbarknoten WHILE exist(vnext) AND visited(vnext) DO vnext := succ(vnext) // Bereits besuchte Knoten überspringen END WHILE IF vnext != nil THEN // Solange unbesuchte Nachbarknoten zu v existieren visit(vnext) // diese besuchen (und bearbeiten), pred(vnext) := v // Vorgänger merken (besuchen & bearbeiten) visited(vnext) := true // als solche markieren und enqueue(t,vnext) // in queue einfügen, wo sie bis zur Bearbeitung // ihrer Nachbarknoten warten ELSE DO dequeue(t) // entferne vorderstes Element aus t END IF // Alle Nachbarn dieses Knotens sind besucht IF empty(t) THEN { kein Pfad von s nach d } 09.10.2008 Projekt FutureCar 13 Implementierung der Wegesuche mittels Breitentraversierung FB Informatik Prof. Dr. R.Nitsch std::list<Location>& Graph::bfSearchForShortestPath( const Location& src, const Location& d, std::list<Location>& route ) { if( world.getTrait(src)==NOTTRAVERSABLE ) // PRE: src is TRAVERSABLE throw "RUNTIME_ERROR GraphUnweighted::bfSearchForShortestPath PRE s"; if ( world.getTrait(d)==NOTTRAVERSABLE ) // PRE: dest is TRAVERSABLE throw "RUNTIME_ERROR GraphUnweighted::bfSearchForShortestPath PRE d"; std::list<Node*> t; // t := empty-queue (contains addresses of neighbors) nodes[s].pred = &nodes[s]; // pred(s) := nil (std::map<Location, Node> nodes) nodes[s].visited = true; // visited(s) := true t.push_back( &nodes[s] ); // enqueue(t,s) while( !t.empty() && !(t.front()->loc==d) ) // WHILE NOT empty(t) AND front(t)!=d DO { Node* v = t.front(); // v := front(t) // Get adjacencylist for node v std::list<Node*>& v_neighbors = nodes[v->loc].adjNodePointerList; std::list<Node*>::iterator iter = v_neighbors.begin(); // vnext := adjList[v] // Find neighbor not yet visited while( iter!=v_neighbors.end() ) { // WHILE exist(vnext) if( !(*iter)->visited ) { // AND NOT visited(vnext) DO Node* vnext = *iter; vnext->visited = true; // visited(vnext) := true vnext->pred = &nodes[v->loc]; // pred(vnext) := v Vorgängerknoten merken t.push_back(vnext); // enqueue(t,vnext) } ++iter; // AND visited(vnext) DO } // END while t.pop_front(); // ELSE DO dequeue(t) } // Fortsetzung nächste Seite 09.10.2008 Projekt FutureCar 14 Path-Container mit Knotensequenz des kürzesten Weges füllen // 1 . // 2. // 3. // 4. // 5. // 6. // 7. // FB Informatik Prof. Dr. R.Nitsch dest in Path-Container für Location-Objekte schreiben v=dest In nodes das mit v assoziierte Node-Objekt suchen und den Vorgängerknoten pred(v) ermitteln pred(v) in Path-Container speichern Solange 2. bis 4. wiederholen, bis pred(v)==src Falls die Knotenreihenfolge im Container dest -> src ist, diese noch umdrehen: src -> dest Die Knotensequenz in eine Folge von Navi-Kommandos (GOAHEAD3, GOAHEAD1, GORIGHT, GOLEFT, TURN ) übersetzen. Die Route ist berechnet … 09.10.2008 Projekt FutureCar 15