Wir beschreiben nun eine neue Union-Find-Datenstruktur.
Jede Menge der Partition wird durch einen Baum mit Wurzel dargestellt.
In dem Baum sind alle Kanten zur Wurzel hin gerichtet.
Implementieren lässt sich das einfach durch ein Array:
parent[v] ist der Elternknoten von $v$; wenn $v$ selbst
Wurzel ist, setzen wir parent[v] := v.
Das Label einer Menge ist der Name des Wurzelknotens. Für
$\find(v)$ müssen wir also nur den $\parent$-Zeigern nachlaufen,
bis wir an der Wurzel angelangt sind.
Für $\union(u,v)$ finden wir erst einmal die jeweiligen Wurzeln $x$ und $y$
und hängen dann den einen Baum direkt an die Wurzel des anderen - setzen
also $\parent[x] := y$. Oder umgekehrt. Wie sollen wir uns entscheiden?
Die Datenstruktur RelabelMinority aus dem vorherigen
Teilkapitel legt nahe, den kleineren Baum zum größeren zu verbinden.
Wir tun hier etwas Ähnliches: wir verbinden den weniger tiefen Baum
zum tieferen. Hier ist eine Demo:
Um das effizient zu implementieren, müssen wir etwas Buchführung treiben: für jeden Knoten $v$ merken speichern wir seine Höhe $\height(v)$, also die Länge des längsten gerichteten Pfades $x \rightsquigarrow v$. Im unteren Bild haben also $e, c, b, g, f$ die Höhe $0$. Knoten $h$ hat Höhe 2. Knoten $i$ hat mit 3 die größte Höhe aller Knoten.
Wir geben jedem Knoten $v$ einen Rang, kurz $\rank(v)$. In diesem Teilkapitel wird durchwegs $\height(v) = \rank(v)$ gelten; im folgenden Kapitel jedoch verändern wir die Datenstruktur, so dass sich die Höhe eines Elements ändern kann; daher ist es besser, Höhe und Rang konzeptuell zu trenenn. Zu Beginn setzen wir $\rank(v) = 0$ für jeden Knoten. Wenn wir $\union(u,v)$ ausführen, finden wir zuerst die Wurzeln der jeweiligen Bäume: $x := \find(u)$ und $y := \find(v)$. Wir fügen nun eine Kante ein, von der Wurzel mit kleinerem Rang zu der mit größerem. Falls beide gleichen Rang haben, fügen wir die Kante $(x,y)$ ein und erhöhen den Rang von $y$ um eins. Der Kindknoten soll ja schließlich immer einen echt kleineren Rang haben als der Elternknoten. Hier sehen Sie meine Python-Implementierung:
class UnionByRank:def __init__(self, n):self.parent = [i for i in range(n)] # jedes Element ist Wurzel seines eigenen Baumesself.rank = [0 for i in range(n)] # jedes Element hat anfangs Rang 0def find(self,u):if self.parent[u] == u:return uelse:root = self.find(self.parent[u])return rootdef union(self, u, v):x = self.find(u)y = self.find(v)if (x == y):return # u, v sind bereits in der gleichen Mengeelif self.rank[x] < self.rank[y]:self.parent[x] = yelif self.rank[x] > self.rank[y]:self.parent[y] = xelse:self.parent[x] = yself.rank[y] += 1def __str__(self):return (str(self.parent))# example < < > > & &
Übungsaufgabe Zeigen Sie, dass für jedes Element und zu jedem Zeitpunkt
\begin{align} \rank(v) = \height(v) \label{rank-is-height} \end{align}gilt.
Laufzeitanalyse
Die Laufzeit von find(u) ist
proportional zur Länge des Pfades von $u$ zur Wurzel $x$.
Dies ist höchstens die Höhe des Baumes und somit
höchstens $\rank(x)$. Es stellt sich nun die
Frage, wie groß $\rank(x)$ werden kann.
Lemma Wenn es Knoten Höhe $k$ hat, dann hat er mindestens $2^k$ Nachfahren - sich selbst mitgerechnet. Somit gilt $2^{\height(x)} \leq n$ und $\height(x) \leq \log n$
Beweis. Wir bezeichnen mit $\height_t(x)$ die Höhe von $x$ nach $t$ union-Operationen. Mit $\desc_t(x)$ bezeichnen wir die Anzahl der Nachfahren zu diesem Zeitpunkt (als Abkürzung für descendants). Wir beweisen das Lemma mit Induktion über $t$.
Am Anfang gilt $t=0$ und $\height_0(x) = 0$ und $\desc_0(x) = 1$ und die Aussage gilt. Die Anzahl der Nachfahren kann nur zunehmen, also $\desc_{t+1}(x) \geq \desc_t(x)$, also können wir uns auf die Zeitpunkte konzentrieren, wo $\height_t(x)$ zunimmt. Wann ist $\height_t(x) \lt \height_{t+1}(x)$? Dies kann nur geschehen, wenn in der $i+1$-ten union-Operation eine Kante $(y,x)$ eingefügt wird. Dann gilt
\begin{align*} \height_{t+1}(x) = \max(\height_t(x), \height_t(y) + 1) \end{align*}Der Fall $\height_t(x) \lt \height_{t+1}(x)$ kann also nur auf folgende Weise geschehen: es wird eine Kante $(y,x)$ eingefügt und $\height_t(x) = \height_t(y) =: k$ und $\height_{t+1} (x) = k+1$. Nach Induktionshypothese gilt $\desc_t(x), \desc_t(y) \geq 2^k$. Da nach der union-Operation alle Elemente aus $T_y$ nun auch Nachfahren von $x$ sind, gilt
\begin{align*} \desc_{t+1}(x) = \desc_t(x) + \desc_t(y) \geq 2^k + 2^k = 2^{k+1} = 2^{\height_{t+1}(x)} \ , \end{align*}und die Aussage des Lemmas gilt nach wie vor. \(\square\)
Die Kosten einer jeden find-Operation sind also höchstens $O(\log n )$. Eine union-Operation besteht aus zwei find-Operationen plus $O(1)$ Rechenaufwand. Also gilt diese Schranke auch für sie.
Theorem
In der Datenstruktur
UnionByRank mit $n$ Elementen benötigt jede
find- und jede union-Operation $O(\log n)$ Schritte.
Kruskals Algorithmus mit einer UnionByRank-Datenstruktur benötigt somit $O((m+n) \log m)$
Schritte.
Im Hinblick auf eine weitere Verbesserung im nächsten Teilkapitel
zeigen wir eine weitere wichtige Eigenschaft der
UnionByRank-Datenstruktur:
Lemma Zu jedem Zeitpunkt gilt: die Anzahl der Elemente mit Rang $k$ ist höchstens $\frac{n}{2^{k}}$.
Beweis. Wir betrachten einen spezifischen Zeitpunkt. Seien $x_1, \dots, x_s$ alle Elemente mit Rank $k$. Sie haben also Höhe $k$. Nach Lemma 7.5.1 gilt $\desc(x_i) \geq 2^{k}$ for alle $1 \leq i \leq s$. Kein $x_i$ ist ein Nachfahren von einem anderen $x_{i'}$ - Nachfahren haben ja schließlich echt kleineren Rang. Bezeichne $\Desc(x)$ die Menge der Nachfahren von $x$ zu diesem Zeitpunkt. Diese Mengen sind disjunkt und jede hat mindestens $2^{k}$ Elemente, und somit gilt
\begin{align*} s \cdot 2^{k} \leq |\Desc(x_1)| + |\Desc(x_2)| + \cdots + |\Desc(x_s)| = \left| \bigcup_{i=1}^s \Desc(x_i) \right| \leq n \ , \end{align*}denn mehr als $n$ Elemente kann die Vereinigung aller dieser Mengen ja nicht enthalten. Wir schließen: $s \leq \frac{n}{2^{k}}$. \(\square\)