FB Informatik Prof. Dr. R.Nitsch Programmieren 2 Graphen • Darstellung •Traversierung • 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 direkt verbundenen Nachbarknoten (= Kantenmenge E5 ) Adjazenzlisten erlauben Antwort auf Fragen zu Graphen G wie z.B. • 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 Speicherbedarf? proportional zur Anzahl Knoten plus Anzahl Kanten O(|V|+|E|) 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 Adjazenzlisten erlauben Antwort auf Fragen zu Graphen G wie z.B. • Wieviele Kanten enden an vi? • Welche Nachbarn vj hat vi? • Existiert Kante E=(vi,vj) mit Zeitkomplexität 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 Anwendungsbeispiel - Futurecar-Projekt FB Informatik Prof. Dr. R.Nitsch • Die Futurecaranwendung ist ein Testunit für AutopilotAlgorithmen zur autonomen Steuerung von Fahrzeugen. • Das TestUnit besteht aus einem virtuellen Strassenlabyrinth (FCWelt, s. Abb.) in dem sich viele virtuelle Fahrzeuge (Futurecars) gesteuert durch ihren individuellen Autopiloten unfallfrei bewegen müssen. Allgemeine Vorgaben/Einschränkungen: • Die FCWelt soll auf einem zeichenorientierten Display darstellbar sein. – Dazu muss sie rechteckig und in in virtuelle Parzellen (s. Abbildung) unterteilt sein. – Jede Parzelle symbolisiert ein FutureCars, ein Haus oder einen Teil der Strasse. – Alle Strassen sind zweispurig und verlaufen senkrecht zueinander. Sie werden begrenzt durch Häuser. – Jedes Futurecar verfügt über einen Scanner, zum Abtasten seiner unmittelbaren Umgebung – Für Navigationsaufgaben ist das Strassennetz in Form von Adjazenzlisten darzustellen. 09.10.2008 Projekt FutureCar 4 Adjazenzliste des Graphen der FutureCar World # # # # Rasterkarte (Grid) von FC-City # 1 4 # # FB Informatik Prof. Dr. R.Nitsch # x y 2 0,0 1,0 2,0 3,0 0,1 1,1 2,1 3,1 0,2 1,2 2,2 3,2 0,3 1,3 2,3 3,3 Rasterkarte von FC-City mit XY-Koordinaten Graph zur Modellierung der Erreichbarkeitsbeziehungen zwischen den Zellen der Rasterkarte 09.10.2008 1,2 2,1 1,1 3,1 2,1 3,2 1,2 2,2 1,3 2,2 3,2 2,1 3,2 3,1 4,2 Adjazenzlisten 5 3 1,1 Projekt FutureCar 11 21 31 12 22 32 12 11 21 22 32 31 32 13 21 42 Adjazenzlisteninfo als Textsequenz 5 Implementierungsempfehlungen 1,1 FB Informatik Prof. Dr. R.Nitsch Node 1,2 3,1 2,1 3,2 + loc + adjList:List 2,1 1,1 3,1 2,1 3,2 1,2 2,2 1,3 2,2 3,2 2,1 3,2 3,1 4,2 + Konstruktor: 1..N 1 Graph - nodes:Nodes + Konstruktor +… Adjazenzlisten Empfehlungen: • Datentyp für Ortskoordinaten? Location als Alias für std::pair<int,int> • Container zum Verwalten der Knoten? Nodes als Alias für map<Location, Node> • Wie Knoten in Container einfügen? nodes[ loc ] .= Node(…) • Besser Koordinaten oder Adressen der Nachbarknoten in den Adjazenzlisten speichern? Das hängt vom Algorithmus ab, der sie verwendet! • Welcher Datentyp eignet sich für Route? Das hängt vom Algorithmus ab, der sie verwendet! 09.10.2008 Projekt FutureCar 6 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. + adjList Markierung + visited:bool + Konstruktor: 09.10.2008 Projekt FutureCar 7 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 besucht 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. Node + loc:Location + adjList + visited:bool + Konstruktor: procedure traverse-dfs(v) visited(v) := true Markiere v als besucht vnext := adjList[v] Bestimme einen Nachbarknoten von v und nenne diesen vnext WHILE( exist(vnext) WHILE(vnext existiert AND NOT visited(vnext) UND noch nicht besucht ist) traverse-dfs(vnext) beginne weitere Tiefensuche bei vnext Wieder zurück, vnext := succ(v_next) bestimme weiteren Nachbarknoten von v und nenne diesen wieder vnext END WHILE END WHILE 09.10.2008 Projekt FutureCar 8 Beispiel zur (rekursiven) Tiefentraversierung procedure traverse-dfs(v) visited(v) := true vnext := adjList[v] WHILE( exist(vnext) AND NOT visited(vnext) traverse-dfs(vnext) vnext := succ(v_next) END WHILE FB Informatik Prof. Dr. R.Nitsch Adjazenzlisten von Seite 2 Startknoten 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 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 3 6 4 5 6 Projekt FutureCar Alle Knoten und Kanten besucht! 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 s über 1 Kante erreichbaren Knoten besucht – Danach alle über mindestens 2 Kanten erreichbaren Knoten, usw. – Entsteht formal aus Tiefentraversierung, wenn man den Stack durch eine Queue ersetzt. PRE: --Post: Alle Knoten, die von s erreichbar sind, sind markiert, also besucht worden procedure bfs_node(s) t := empty-queue // Definition einer leeren lokalen Queue visited(s) := true // Startknoten s als "besucht" markieren enqueue(t,s) WHILE NOT empty(t) DO v := front(t) // vordersten Knoten in t lesen 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,v_next) // erst mal in queue einreihen, wo sie bis zur // Bearbeitung ihrer Nachbarknoten warten ELSE DO dequeue(t) // Erledigt! Alle Nachbarn von v mit gleichem END IF // Abstand vom Startknoten wurden besucht. 11 09.10.2008 Projekt FutureCar END WHLE procedure bfs_node(s) t := empty-queue visited(s) := true enqueue(t,s) WHILE NOT empty(t) DO v := front(t) vnext := adj[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) ELSE dequeue(t) END IF 2 t: END WHILE Beispiel zur Breitentraversierung FB Informatik Prof. Dr. R.Nitsch Adjazenzlisten von Seite 2 Queue 5 4 1 3 1 2 4 2 5 4 3 5 6 4 1 2 5 5 4 3 2 6 3 6 1 6 Startknoten 1 2 3 1 2 3 1 2 3 4 5 6 4 5 6 4 5 6 1 2 3 1 2 3 4 09.10.2008 5 6 4 5 6 Projekt FutureCar Jetzt sind alle Knoten mit Distanz "1Kante" zum Startknoten besucht Alle Knoten und Kanten besucht! 12 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 s zum Zielknoten d 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 s. – Der Vorgängerknoten, von dem ausgehend der Knoten v betreten wird, verbindet somit v auf dem kürzesten Wege mit s (keine Kantengewichte!). – Im Bearbeitungsschritt merkt sich Knoten v daher seinen Vorgängerknoten – Nachdem Knoten d betreten wurde und dieser sich seinen Vorgängerknoten gemerkt hat, ist die Suche beendet. – Der kürzeste Weg, der d mit s verbindet, ergibt sich nun, indem man, beginnend bei d, die Folge der Vorgängerknoten rekonstruiert. – Rückwärts gelesen (std::reverse) ergibt diese Folge den gesuchten kürzesten Weg. 13 09.10.2008 Projekt FutureCar 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 Verweis auf Vorgängerknoten Node + loc:Location + adjList + pred + visited:bool WHILE NOT empty(t) AND front(t)!=d DO // Abbruch der Suche wenn d besucht + Konstruktor: v := front(t) // vordersten Knoten in t lesen vnext := adj[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(v_next) := 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 14 Wegesuche in gewichtetem Graphen FB Informatik Prof. Dr. R.Nitsch Aufgaben – Von einem Knoten s aus die kürzesten Pfade zu allen anderen Knoten des Graphen finden: Lösung mit single-source shortest path (SSSP) Algorithmen – Für alle Paare von Knoten die kürzesten Pfade finden: Lösung mit all-pair shortest path (APSP) Algorithmen • Kürzeste Wege – im ungewichteten Graph: Pfad mit geringster Kantenzahl – im gewichteten Graph: Pfad mit geringstem Gesamtgewicht • Kürzeste Wege sind nicht eindeutig – wenn 2 Wege das gleiche Gesamtgewicht haben • Kürzeste Wege existieren dann nicht – wenn gar kein Weg zwischen 2 Knoten existiert – falls der Graph Zyklen mit negativem Gesamtgewicht hat (jeder Durchlauf verringert das Gewicht des Pfades. • Wichtige Algorithmen: – Dijkstra-Algorithmus (SSSP): für Graphen mit Kantengewichten 0 – Belman-Ford-Algorithmus: Für Graphen mit negativen Gewichten aber ohne negative Zyklen (= Zyklen mit Kantensumme< 0). 09.10.2008 Projekt FutureCar 15 Dijkstra Algorithmus FB Informatik Prof. Dr. R.Nitsch • Liefert für alle Knoten eines Graphen G die kürzesten Pfade zu einem Startknoten s • Wirkungsweise algorithm dijkstra(G,s) Node – Zusätzlich zu seiner Adjazenzliste erhält Eingabe: Graph G, Startknoten s + loc:Location jeder Knoten v die Angabe FOR EACH Knoten v aus V(G) DO + adjList d2s[v]= • zur minimalen Distanz d2s[v] zum + pred pred[v]=nil Startnoten s + d2s:float OD • des Vorgängerknotens pred[v] auf dem d2s[s]=0 // Startknoten erhält höchste Priorität + Konstruktor: kürzesten Weg zum Startknoten PQ:=V – Einfügen aller Knoten in eine Priorityueue PQ WHILE NOT empty(PQ) DO – Mit jedem Durchlauf wird der Knoten vmin mit vmin = dequeueMin(PQ) der bis dahin geringsten Distanz zu s aus einer FOR EACH vnext OF vmin DO PriorityQueue PQ entnommen d2sneu:=d2s(vmin)+w(vmin,vnext) • die Distanz der über vmin zu all seinen Nachbarknoten führenden Wege ermittelt wobei w(vmin,vnext) das Gewicht der Kante vmin->vnext ist. IF( d2sneu<d2s(vnext) DO • falls die bisher für vnext notierte Distanz d2s(vnext):=d2sneu größer als dist2s(vnext)neu ist, wird diese pred:=vmin aktualisiert und vmin als neuer FI Vorgängerknoten eingetragen OD OD 09.10.2008 Projekt FutureCar 16