7. Graphen mit Kantenkosten
7.4 Union-Find-Datenstrukturen
Um Kruskals Algorithmus effizient
implementieren zu können, brauchen wir eine Datenstruktur,
die die Zusammenhangskomponenten von sameSet(u,v)
, die überprüft, ob connect(u,v)
,
welche die Komponenten von
Definition 7.4.1 Sei
Hier ein paar Beispiele von Partitionen und Nicht-Partitionen
der Menge
Die Menge
Die Partition
Die Partition
Die (triviale) Partition
Die (triviale) Partition
Datenstrukturproblem 7.4.2 - Union Find. Gewünscht ist eine Datenstruktur, die eine Partition auf einer Menge repräsentiert und folgende Operationen unterstützt:
: erstellt die (Repräsentation der) trivialen Partition , wo jedes Element in seiner eigenen Menge der Größe 1 enthalten ist. : gibt die Menge aus, die enthält; da es eine Partition ist, gibt es genau eine solche Menge. Streng genommen wird nicht die Menge selbst ausgegeben, sondern ein eindeutiges Label. Das Label ist im Prinzip beliebigt, wichtig ist nur, dass verschiedene Mengen verschiedene Labels erzeugen. : vereinigt die Mengen und zu einer großen Menge; verändert also die Partition.
Eine Datenstruktur, die diese Operationen unterstützt, heißt Union-Find-Datenstruktur.
Mit einer Union-Find-Datenstruktur können wir
Kruskals Algorithmus implementieren:
um in Schritt 4 zu schauen, ob
Die Datenstruktur Label-Rename
Ich stelle Ihnen jetzt eine erste Implementierung einer Union-Find-Datenstruktur
vor. Es ist wahrscheinlich das erste, worauf die meisten Programmierer kommen
würden. Der Einfachheit halber stelle ich mir die Elemente
vor als Zahlen label
an, so dass
jedes Element u
in der Menge mit Label label[i]
enthalten ist. Für
class UnionByLabel:
def __init__(self, n):
self.label = [i for i in range(n)] # jedes Element i ist in einer Menge mit Label i
self.set = [ [i] for i in range(n) ] # die Menge mit Label i besteht nur aus einem Element: i
def find(self,u):
return self.label[u]
def union(self,u,v):
l1 = self.find(u)
# Das Label der Menge, die u enthältl2 = self.find(v)
# Das Label der Menge, die v enthält
if (l1 == l2):
return
set1 = self.set[l1]
# Die Menge, die u enthält, als Listeset2 = self.set[l2]
# Die Menge, die v enthält, als Liste
for x in set1:
self.label[x] = l2
# Allen Elementen aus set1 ein neues Label geben
self.set[l2] += set1
# Die Menge mit Label l2 wird größerself.set[l1] = []
# Die Menge mit Label l1 verschwindet
def __str__(self):
return (str( [s for s in self.set if len(s) > 0]))
Eine find-Operation braucht also
Übungsaufgabe 7.4.1
Zeigen Sie, dass es eine Folge von UnionByLabel
insgesamt
Es ist also eine nicht sehr effiziente Datenstruktur.
Erinnerin Sie sich: die "Baseline" bei
Kruskals Algorithmus
ist ja
mit Tiefensuche (oder Breitensuche) in
Beobachtung 7.4.3 Die Laufzeit
von Kruskals Algorithmus mit Tiefensuche in Schritt 4 ist
UnionByLabel
ist sie
Unsere Implementierung von UnionByLabel
ist
verschwenderisch, weil wir immer unkritisch die Elemente
der ersten Menge umbenennen. Sinnvoller wäre es,
zu schauen, welche der beiden Mengen kleiner ist und dann
die kleinere umzubennen. Wir nennen diese Implementierung
RelabelMinority
.
Theorem 7.4.4
Die Datenstruktur RelabelMinority
benötigt für eine
Folge von
Beachten Sie, dass es gar nicht mehr als
Beweis.
Wenn
Wenn wir jedes
Um eine bessere Laufzeitabschätzgung zu erzielen, gehen wir anders vor. Die Haupteinsicht ist, dass nicht alle union-Operationen teuer sein können. Bei einer teuren union-Operation verschwindet eine bereits sehr große Menge in einer anderen (noch größeren). Dies kann nicht beliebig oft geschehen. Auf eine teure Operation müssen also viele billige kommen, was sich hoffentlich im Schnitt ausgleicht. Eine solche "ausgleichende" Analyse nennt man amortisierte Analyse. Ich werde zuerst einmal recht formal vorgehen und dann nochmal alles mit mehr Intuition durchgehen.
Wir legen eine Matrix
Leere Einträge in der Matrix stehen für eine
Wieviele Einträge hat Zeile
Wir haben also gerade gesehen: eine Spalte von
Daher: eine Folge von
Was bedeutet dies nun für die Laufzeit von Kruskals Algorithmus?
Beobachtung 7.4.5
Kruskals Algorithmus
führt RelabelMinority
-Datenstruktur benötigt das
Die Laufzeit wird also vom Sortieren der Kanten von billig nach teuer
dominiert. Da wir im Allgemeinen das
Laufzeitanalyse mit Geld-Metapher
Sie haben nun gerade eine amortisierte Laufzeitanalyse gesehen. Eine beliebte Metapher, um diese intuitiver zu machen, ist monetär: jeder Berechnungsschritt einen Euro kostet und muss von irgendjemanden bezahlt werden. Wir gehen wie folgt vor und beziehen uns auf den Python-Code von UnionByLabel:
- find-Operationen verursachen Kosten von
Euro, die sie selbst tragen müssen. - eine union-Operation muss alles bis auf die Umbeschriftungs-Schritte
in Zeile 19/20 (der Schleife
for x in set1
) tragen. -
Die Kosten für Zeile 20 (
self.label[x] = l2
) werden dem Element in Rechnung gestellt.
Durch diesen Buchhaltertrick sind die Kosten für eine union-Operation
von
Wir werden die Geld-Metapher nochmal in Kapitel 7.6 bemühen, wo wir eine deutlich anspruchsvollere Laufzeitanalyse durchführen.