Python Patterns - Implementierung von Graphen
Warnung
Diese Seite bleibt aus historischen Gründen erhalten und kann veraltete oder falsche Informationen enthalten.
find_shortest_path "nahezu optimal" sei. 11.08.19: Korrektur der versehentlichen Verwendung von find_graph() anstelle von find_path()
Copyright (c) 1998, 2000, 2003, 2019 Python Software Foundation. All rights reserved. Licensed under the PSF license.
Graphen sind Netzwerke, die aus Knoten bestehen, die durch Kanten oder Bögen verbunden sind. In gerichteten Graphen haben die Verbindungen zwischen Knoten eine Richtung und werden Bögen genannt; in ungerichteten Graphen haben die Verbindungen keine Richtung und werden Kanten genannt. Wir diskutieren hauptsächlich gerichtete Graphen. Algorithmen für Graphen umfassen das Finden eines Pfades zwischen zwei Knoten, das Finden des kürzesten Pfades zwischen zwei Knoten, das Bestimmen von Zyklen im Graphen (ein Zyklus ist ein nicht-leerer Pfad von einem Knoten zu sich selbst), das Finden eines Pfades, der alle Knoten erreicht (das berühmte "Problem des Handlungsreisenden"), und so weiter. Manchmal sind den Knoten oder Bögen eines Graphen Gewichte oder Kosten zugeordnet, und wir sind daran interessiert, den günstigsten Pfad zu finden.
Es gibt beträchtliche Literatur über Graphenalgorithmen, die ein wichtiger Bestandteil der diskreten Mathematik sind. Graphen haben auch praktische Anwendung in vielen Computer-Algorithmen. Offensichtliche Beispiele finden sich in der Verwaltung von Netzwerken, aber Beispiele gibt es in vielen anderen Bereichen. Zum Beispiel können Aufrufer-Aufrufsempfänger-Beziehungen in einem Computerprogramm als Graph betrachtet werden (wobei Zyklen Rekursion anzeigen und unerreichbare Knoten toten Code darstellen).
Nur wenige Programmiersprachen bieten direkte Unterstützung für Graphen als Datentyp, und Python ist keine Ausnahme. Graphen lassen sich jedoch leicht aus Listen und Wörterbüchern aufbauen. Hier ist zum Beispiel ein einfacher Graph (ich kann keine Zeichnungen in diesen Spalten verwenden, also schreibe ich die Bögen des Graphen auf)
A -> B
A -> C
B -> C
B -> D
C -> D
D -> C
E -> F
F -> C
Dieser Graph hat sechs Knoten (A-F) und acht Bögen. Er kann durch die folgende Python-Datenstruktur dargestellt werden graph = {'A': ['B', 'C'],
'B': ['C', 'D'],
'C': ['D'],
'D': ['C'],
'E': ['F'],
'F': ['C']}
Dies ist ein Wörterbuch, dessen Schlüssel die Knoten des Graphen sind. Für jeden Schlüssel ist der entsprechende Wert eine Liste, die die Knoten enthält, die von diesem Knoten aus durch einen direkten Bogen verbunden sind. Das ist so einfach wie es nur geht (noch einfacher könnten die Knoten durch Zahlen anstelle von Namen dargestellt werden, aber Namen sind praktischer und können leicht mehr Informationen tragen, wie z.B. Städtenamen).Schreiben wir eine einfache Funktion, um einen Pfad zwischen zwei Knoten zu ermitteln. Sie nimmt einen Graphen sowie den Start- und Endknoten als Argumente. Sie gibt eine Liste von Knoten (einschließlich Start- und Endknoten) zurück, die den Pfad bilden. Wenn kein Pfad gefunden werden kann, gibt sie None zurück. Derselbe Knoten wird nicht mehr als einmal im zurückgegebenen Pfad vorkommen (d.h. er enthält keine Zyklen). Der Algorithmus verwendet eine wichtige Technik namens Backtracking: Er versucht jede Möglichkeit der Reihe nach, bis er eine Lösung findet.
def find_path(graph, start, end, path=[]):
path = path + [start]
if start == end:
return path
if not graph.has_key(start):
return None
for node in graph[start]:
if node not in path:
newpath = find_path(graph, node, end, path)
if newpath: return newpath
return None
Ein Beispielaufruf (mit dem obigen Graphen) >>> find_path(graph, 'A', 'D')
['A', 'B', 'C', 'D']
>>>
Die zweite 'if'-Anweisung ist nur notwendig, falls es Knoten gibt, die als Endpunkte von Bögen aufgeführt sind, aber selbst keine ausgehenden Bögen haben und nicht im Graphen aufgeführt sind. Solche Knoten könnten auch im Graphen enthalten sein, mit einer leeren Liste ausgehender Bögen, aber manchmal ist es praktischer, dies nicht zu verlangen.Beachten Sie, dass find_path(), obwohl der Benutzer es mit drei Argumenten aufruft, sich selbst mit einem vierten Argument aufruft: dem bereits durchlaufenen Pfad. Der Standardwert für dieses Argument ist die leere Liste '[]', was bedeutet, dass noch keine Knoten durchlaufen wurden. Dieses Argument wird verwendet, um Zyklen zu vermeiden (das erste 'if' innerhalb der 'for'-Schleife). Das 'path'-Argument wird nicht verändert: die Zuweisung "path = path + [start]" erstellt eine neue Liste. Hätten wir stattdessen "path.append(start)" geschrieben, hätten wir die Variable 'path' im Aufrufer verändert, mit desaströsen Folgen. (Unter Verwendung von Tupeln hätten wir sicherstellen können, dass dies nicht passiert, auf Kosten der Notwendigkeit, "path = path + (start,)" zu schreiben, da "(start)" kein Singleton-Tupel ist – es ist nur ein in Klammern gesetzter Ausdruck.)
Es ist einfach, diese Funktion zu ändern, um anstelle des ersten gefundenen Pfades eine Liste aller Pfade (ohne Zyklen) zurückzugeben
def find_all_paths(graph, start, end, path=[]):
path = path + [start]
if start == end:
return [path]
if not graph.has_key(start):
return []
paths = []
for node in graph[start]:
if node not in path:
newpaths = find_all_paths(graph, node, end, path)
for newpath in newpaths:
paths.append(newpath)
return paths
Ein Beispielaufruf >>> find_all_paths(graph, 'A', 'D')
[['A', 'B', 'C', 'D'], ['A', 'B', 'D'], ['A', 'C', 'D']]
>>>
Eine weitere Variante findet den kürzesten Pfad def find_shortest_path(graph, start, end, path=[]):
path = path + [start]
if start == end:
return path
if not graph.has_key(start):
return None
shortest = None
for node in graph[start]:
if node not in path:
newpath = find_shortest_path(graph, node, end, path)
if newpath:
if not shortest or len(newpath) < len(shortest):
shortest = newpath
return shortest
Beispielaufruf >>> find_shortest_path(graph, 'A', 'D')
['A', 'C', 'D']
>>>
UPDATE: Eryk Kopczyński wies darauf hin, dass diese Funktionen nicht optimal sind. Im Gegenteil, "dieses Programm läuft in exponentieller Zeit, während find_shortest_path mit BFS [Breadth First Search] in linearer Zeit erledigt werden kann. Außerdem ist ein lineares BFS einfacher:"
# Code by Eryk Kopczyński
def find_shortest_path(graph, start, end):
dist = {start: [start]}
q = deque(start)
while len(q):
at = q.popleft()
for next in graph[at]:
if next not in dist:
dist[next] = [dist[at], next]
q.append(next)
return dist.get(end)
Beachten Sie, dass dies den Pfad in einem seltsamen Format zurückgibt, z.B. [[['A'], 'B'], 'D']. Insbesondere len(find_shortest_path(graph, 'A', 'D')) gibt die falsche Antwort (2, da die äußere Liste 2 Elemente hat). Dies liegt daran, dass append als [dist[at], next] statt als dist[at]+[next] durchgeführt wird. Die zweite Methode würde quadratische Zeit und Speicher benötigen, wäre aber für relativ kleine Graphen immer noch in Ordnung; andernfalls ist es einfach, die Liste in das richtige Format zu bringen.Eine weitere Variation wäre, mehr Datenabstraktion hinzuzufügen: eine Klasse zur Darstellung von Graphen erstellen, deren Methoden die verschiedenen Algorithmen implementieren. Das mag dem Wunsch nach strukturiertem Programmieren entgegenkommen, macht den Code aber nicht effizienter (im Gegenteil). Es erleichtert jedoch das Hinzufügen verschiedener Beschriftungen zu den Knoten oder Bögen und das Hinzufügen von Algorithmen, die diese Beschriftungen berücksichtigen (z.B. um die kürzeste Route zwischen zwei Städten auf einer Karte zu finden).Auch dies wird Gegenstand einer weiteren Spalte sein.
