Diskrete Modellierung Wintersemester 2016/17 Martin Mundhenk Uni Jena, Institut für Informatik 12. Februar 2017 4.5 Das Small-World-Phänomen Anwendungsbeispiel Die Untersuchung von Graphen hat sich – verstärkt durch die weltweite Vernetzung – zu einem eigenen Forschungsgebiet entwickelt. Das Small-World-Experiment sollte herausfinden, wie kurz der Abstand“ ” zwischen zwei beliebigen Menschen ist, der als Länge der kürzesten Kette von Freunden, Freunden von Freunden usw. definiert ist, die die beiden Menschen verbindet (Stanley Milgram, 1967). Mit den Freundschaftsgraphen“ der sozialen Netzwerke lässt sich das heutzutage ” berechnen. Wir werden sehen, wie man das algorithmisch machen kann. Wir schauen uns nochmal die Implementierung von Graphen mit den in Python vorhandenen Datenstrukturen dict und set an. 4.5.1 Die API für set Für Mengen braucht man Operationen zum Einfügen, Entfernen und Abfragen von Elementen, zum Durchlaufen aller Elemente der Menge und für grundlegende Mengenoperationen. Operation set() Beschreibung eine neue (leere) Menge s.add(item) füge item zur Menge s hinzu s.delete(item) entferne item aus der Menge s item in s gibt True zurück, falls item in der Menge s enthalten ist for item in s: iteriere über alle Elemente der Menge s s.intersection(t) der Durchschnitt der Mengen s und t s.union(t) die Vereinigung der Mengen s und t 4.5.2 Ein Graph (aus Knoten und Kanten) aus: Introduction to Programming in Python (Sedgewick, Wayne, Dondero, 2015) 4.5.3 Wege in Graphen aus: Introduction to Programming in Python (Sedgewick, Wayne, Dondero, 2015) 4.5.4 Noch ein Graph (aus Knoten und Kanten) aus: Introduction to Programming in Python (Sedgewick, Wayne, Dondero, 2015) 4.5.5 Anwendungen von Graphen § Verkehrssysteme § Kommunikationssysteme § Humanbiologie § Soziale Netzwerke § Physikalische Systeme § Softwaresysteme § Finanzsysteme Darstellung von Graphen als Textdateien Pro Zeile: ein Knoten und weitere Knoten, zu denen er eine Kante hat. movies.txt 21 Grams (2003)/Alban, Carlo/Finnell, Michael W./Finnell, Michael/... ... Manhattan (1979)/.../Allen, Woody/.../Keaton, Diane/Farrow, Tisa/... ... fluege.txt ... CEK CEK DME DME DME EGO LAX ... KZN OVB KZN NBC TGK KGD PHX 4.5.7 Die Grundidee für den Datentyp für Graphen aus: Introduction to Programming in Python (Sedgewick, Wayne, Dondero, 2015) 4.5.8 Die (neue) API für Graph Ein Graph soll aus einer Datei eingelesen werden können. Knoten und Kanten können hinzugefügt werden. Die Knoten können durchlaufen werden, ebenso alle Nachbarn eines Knotens. Operation Beschreibung Graph(dateiname,trennzeichen) ein neuer Graph, dessen Beschreibung aus der Datei dateiname eingelesen wird; trennzeichen ist das Trennzeichen zwischen den Knoten einer Zeile in der Datei. Wenn dateiname None ist, wird ein leerer Graph erzeugt. g.kanteHinzufuegen(von,nach) fügt die Kante zwischen den Knoten von und nach in beiden Richtungen zum Graph g hinzu g.knoten() ein iterable für die Knoten von g g.nachbarnVon(k) ein iterable für die Nachbarn von Knoten k im Graph g 4.5.9 Operation g.hatKnoten(k) Beschreibung gibt True zurück, falls Knoten k in Graph g vorkommt g.hatKante(v,n) gibt True zurück, falls eine Kante zwischen v und n in Graph g vorkommt g.grad(k) gibt die Anzahl der Nachbarn von Knoten k im Graph g zurück g.knotenzahl() gibt die Anzahl der Knoten von Graph g zurück g.kantenzahl() gibt die Anzahl der Kanten von Graph g zurück 4.5.10 Ein Teil der Implementierung von Graph from instream import InStream class Graph: def __init__(self, dateiname=None, trennzeichen=None): self._anzahlKanten = 0 self._adj = dict() if dateiname is not None: eingabe = InStream(dateiname) while eingabe.hasNextLine(): zeile = eingabe.readLine() knoten = zeile.split(trennzeichen) for i in range(1,len(knoten)): self.kanteHinzufuegen(knoten[0],knoten[i]) def kanteHinzufuegen(self,von,nach): if not self.hatKnoten(von): self.knotenHinzufuegen(von) if not self.hatKnoten(nach): self.knotenHinzufuegen(nach) if not self.hatKante(von,nach): self._anzahlKanten += 1 self._adj[von].add(nach) self._adj[nach].add(von) 4.5.11 Ein Client für Graph Wir wollen ein Programm schreiben, das einen Graph aus einer Datei einliest und anschließend zu Knoten, die von der Konsole eingegeben werden, die Nachbarn im Graph ausgibt. import sys import stdio from Graph import Graph dateiname = sys.argv[1] trennzeichen = sys.argv[2] graph = Graph(dateiname,trennzeichen) while stdio.hasNextLine(): v = stdio.readLine() if graph.hatKnoten(v): for w in graph.nachbarnVon(v): stdio.writeln(' ' + w) 4.5.12 Wir benutzen drei Beispiel-Graphen: movies.txt enthält Titel/Schauspieler zu 4188 Filmen, moviesg.txt ist ein Teil davon mit 1100 Filmen, und fluege.txt ist eine Datei mit Start-/Zielflughafen von 67000 Linienflügen. $ python zeigeNachbarn.py fluege.txt " " ERF $ python zeigeNachbarn.py movies.txt "/" PMI Manhattan (1979) FUE Streep, Meryl LPA Conroy, Frances AYT Murphy, Michael (I) LGW Allen, Woody TFS ... LEJ BCN Allen, Woody DME Husbands and Wives (1992) ACE Deconstructing Harry (1997) AGA Bananas (1971) IST Stanley Kubrick: A Life in Pictures (2001) FUE New York Stories (1989) FNC ... MUC CGN ... Suche nach (kürzesten) Wegen – PfadFinder Das hatten wir schonmal – Breitensuche. Jetzt implementieren wir dafür eine eigene Klasse. Operation Beschreibung PfadFinder(graph,startknoten) finde kürzeste Pfade von startknoten zu den Knoten im Graph graph pf.erreichbar(k) gibt True zurück, falls Knoten k erreichbar ist pf.distanzZu(k) gibt die Entfernung von Knoten k zurück pf.pfadZu(k) gibt den Pfad zu Knoten k als Array zurück pf.erreichbareKnoten() Iterator über die erreichbaren Knoten pf.maxEntfernung() gibt die maximale Entfernung und einen Knoten mit dieser Entfernung von startknoten zurück import ... class PfadFinder: def __init__(self,graph,startknoten): self._entfernungZu = dict() self._kanteZu = dict() warteschlange = Queue() warteschlange.enqueue(startknoten) self._entfernungZu[startknoten] = 0 self._kanteZu[startknoten] = None while not warteschlange.isEmpty(): k = warteschlange.dequeue() for n in graph.nachbarnVon(k): if n not in self._entfernungZu: warteschlange.enqueue(n) self._entfernungZu[n] = 1 + self._entfernungZu[k] self._kanteZu[n] = k def pfadZu(self, k): pfad = [] while k is not None: pfad += [str(k)] k = self._kanteZu[k] pfad.reverse() return pfad def erreichbar(self,k): return k in self._entfernungZu def erreichbareKnoten(self): return iter(self._entfernungZu) def distanzZu(self, k): return self._entfernungZu[k] def hatPfadZu(self, k): return k in self._entfernungZu def maxEntfernung(self): entf = 0 knoten = None for k,e in self._entfernungZu.iteritems(): if e > entf: entf = e knoten = k return entf, knoten 4.5.16 Clienten für PfadFinder Wir wollen feststellen, ob in unseren Graphen jeder Knoten von jedem anderen aus erreichbar ist. Jeder Graph zerfällt in Komponenten aus gegenseitig erreichbaren Knoten. Eine übliche Vorstellung ist, dass ein Graph nur aus einer Komponente besteht. Das ist aber nicht immer so. Der Client komponenten.py für PfadFinder liest einen Graph aus einer Datei (dazu erhält er den Dateinamen und das Trennzeichen von der Kommandozeile) und zerlegt“ ihn in seine Komponenten. Dazu geht er alle ” Knoten durch. Falls der betrachtete Knoten k nicht in einer bereits gefundenen Komponente liegt, bestimmt er mittels PfadFinder alle von k aus erreichbaren Knoten. Sie bilden die Komponente, die auch k enthält Unter dem Schlüssel k werden sie in das Dictionary komponenten eingetragen. Am Ende enthält komponenten einen Eintrag für jede Komponente des Graphen. Der Client gibt für jede Komponente den Knoten, von dem aus sie gefunden wurde, und die Anzahl der Knoten in der Komponente aus. 4.5.17 # komponenten.py import sys, stdio from Graph import Graph from PathFinder import PfadFinder dateiname = sys.argv[1] trennzeichen = sys.argv[2] graph = Graph(dateiname,trennzeichen) gefundeneKnoten = set() komponenten = dict() for k in graph.knoten(): if k not in gefundeneKnoten: komponenten[k] = set() pf = PfadFinder(graph,k) for ek in pf.erreichbareKnoten(): komponenten[k].add(ek) gefundeneKnoten.add(ek) print "Der Graph hat",len(komponenten), \ "Komponenten." for k,s in komponenten.iteritems(): print k, len(s) $ python komponenten.py movies.txt "/" Der Graph hat 33 Komponenten. Jeon, Jin-bae 16 Mitchell, Paula 118762 Joffroy, Pierre 14 ... Eggleston, Ralph 2 ... Mystery Science Theater 3000: The Movie (1996) 6 ... Suknovalov, Aleksei 11 McGowan, Kathleen (I) 29 Amado, Chisco 18 $ python komponenten.py moviesg.txt "/" Der Graph hat 1 Komponenten. Chambers, Janice 20144 $ python komponenten.py fluege.txt " " Der Graph hat 8 Komponenten. AGN 3397 FRD 4 TKJ 2 OND 4 BLD 2 AKB 4 SPB 2 KNQ 10 Das Small-World-Experiment Nun können wir in einer Komponente das Small-World-Experiment machen – also die maximale Länge der kürzesten Verbindung zwischen zwei Knoten berechnen. Dazu brauchen wir einen Graph, der aus einer Komponente besteht – bei dem man also von jedem Knoten aus jeden anderen Knoten erreichen kann. Auf der letzten Folie haben wir gesehen, dass der Graph in moviesg.txt so ist. Der Graph in fluege.txt besteht aus 8 Komponenten. Es ist leicht, daraus nur die Flüge in der größten Komponente auszuwählen. Wir speichern sie als Datei in fluege2.txt. Der folgende Client findet zwei Knoten, deren kürzeste Verbindung die längste aller kürzesten Verbindungen zwischen zwei Knoten ist. Dazu bestimmt er für jeden Knoten den längsten kürzesten Weg zu einem anderen Knoten, und merkt sich das Maximum aller dieser längsten kürzesten Wege. 4.5.19 # enge.py import sys from PathFinder import PfadFinder from Graph import Graph def maxMinEntfernung(graph): maxe = 0 for k in graph.knoten(): pf = PfadFinder(graph,k) $ python enge.py fluege2.txt " " (entf,knoten) = pf.maxEntfernung() Der Graph hat 3397 Knoten. if entf > maxe: Die groesste Entfernung ist 13 maxe = entf von YPO nach SVR. maxstart = k ['YPO', 'YAT', 'ZKE', 'YFA', 'YMO', maxziel = knoten 'YTS', 'YYZ', 'CPH', 'SFJ', 'JAV', maxpfad = pf.pfadZu(knoten) 'JUV', 'NAQ', 'THU', 'SVR'] return (maxe,maxstart,maxziel,maxpfad) #------------------------------------------------------def test(): dateiname = sys.argv[1] trennzeichen = sys.argv[2] g = Graph(dateiname,trennzeichen) print "Der Graph hat", g.knotenzahl(), "Knoten." erg = maxMinEntfernung(g) print "Die groesste Entfernung ist", str(erg[0]), \ "von", str(erg[1]), "nach", str(erg[2])+"." if __name__=='__main__': test() Small-World-Graphen haben die Eigenschaften § nur Knoten mit wenigen Nachbarn zu haben, § kurze durchschnittliche Entfernung zwischen allen Knoten haben, § aus vielen Clustern mit sehr vielen Verbindungen untereinander zu bestehen. Diese Eigenschaften von Graphen können wir messen. Als Beispiel betrachten wir die durchschnittliche Entfernung zwischen allen Knoten. Die Funktion mittlerePfadlaengeK(g,k) berechnet die durchschnittliche Entfernung von Knoten k zu allen anderen Knoten im Graph g. Dazu bestimmt sie mit PfadFinder die Entfernungen von k zu allen anderen Knoten, summiert sie auf und teilt sie durch die Anzahl der anderen Knoten. Entsprechend berechnet mittlerePfadlaenge(g) den Durchschnitt der durchschnittlichen Entfernung aller Knoten. Außerdem bestimmen wir noch einen zentralen Knoten, dessen durchschnittliche Entfernung zu allen anderen Knoten minimal ist. # graphprops.py import sys from PathFinder import PfadFinder from Graph import Graph def mittlerePfadlaenge(graph): summe = 0 for k in graph.knoten(): summe += mittlerePfadlaengeK(graph,k) return summe / graph.knotenzahl() def mittlerePfadlaengeK(graph,k): p = PfadFinder(graph,k) summe = 0 for n in graph.knoten(): summe += p.distanzZu(n) return 1.0 * summe / (graph.knotenzahl()-1) 4.5.23 def zentralerKnoten(graph): tabelle = dict() for k in graph.knoten(): tabelle[k] = mittlerePfadlaengeK(graph,k) minimum = None for key, value in tabelle.iteritems(): if minimum == None or value<minimum: minkey = key minimum = value return (minkey,minimum) #------------------------------------------------------------------------dateiname = sys.argv[1] trennzeichen = sys.argv[2] g = Graph(dateiname,trennzeichen) print "Der Graph hat", g.knotenzahl(), "Knoten." k,e = zentralerKnoten(g) print "Zentraler Knoten ist", str(k), " mit mittlerer Entfernung", e, "." print "Die mittlere Pfadlaenge ist", mittlerePfadlaenge(g), "." 4.5.24 $ python graphprops.py fluege2.txt " " Der Graph hat 3397 Knoten. Zentraler Knoten ist FRA mit mittlerer Entfernung 2.48851590106 . Die mittlere Pfadlaenge ist 4.1032411679 . $ python graphprops.py moviesg.txt "/" Der Graph hat 20144 Knoten. Zentraler Knoten ist Around the World in Eighty Days (1956) mit mittlerer Entfernung 4.59246388323 . Die mittlere Pfadlaenge ist 6.9264036957 . 4.5.25