0Pricing
Coding Interview Prep · Lektion

DSU mit Pfadkompression

Implementieren Sie find mit Pfadkompression, sodass alle Knoten auf dem Pfad direkt auf die Wurzel zeigen und find amortisiert nahezu O(1) erreicht.

DSU mit Pfadkompression ist eine kostenlose Coding Interview Prep-Lektion auf CoddyKit. Dies ist Lektion 1 von 4. Du kannst die komplette Lektion unten kostenlos lesen – dann übst du sie direkt im Browser mit einem integrierten Code-Editor und einem KI-Tutor rund um die Uhr. Sie ist Teil des Coding Interview Prep-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der Coding Interview Prep-Kurs umfasst insgesamt 4 Lektionen.

Was ist Disjoint Set Union?

Disjoint Set Union (DSU), auch Union-Find genannt, ist eine Datenstruktur, die eine Sammlung disjunkter (nicht überlappender) Mengen verwaltet. Sie unterstützt zwei grundlegende Operationen: find (zu welcher Menge gehört Element x?) und union (die Mengen zusammenführen, die x und y enthalten). DSU eignet sich ideal für Probleme mit dynamischer Konnektivität, bei denen Gruppen im Laufe der Zeit zusammengeführt, aber nie aufgeteilt werden.

Jedes Element beginnt als eigene Menge. Während wir Kanten oder Beziehungen verarbeiten, führen wir Mengen zusammen. Die Herausforderung besteht darin, dies effizient zu tun — naive Implementierungen benötigen O(n) pro Operation, mit Optimierungen nähern wir uns amortisiert O(1).

# Naive DSU without optimisations
class DSU:
    def __init__(self, n):
        self.parent = list(range(n))  # each node is its own parent

    def find(self, x):
        while self.parent[x] != x:
            x = self.parent[x]
        return x

    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px != py:
            self.parent[px] = py

Das Problem mit naivem Find

In der naiven DSU läuft find(x) die Elternkette nach oben, bis sie einen Knoten erreicht, der auf sich selbst zeigt (die Wurzel). Ist der Baum balanciert, beträgt die Laufzeit O(log n). Wenn wir jedoch immer die zweite Wurzel unter die erste hängen, können wir eine Kette (degenerierten Baum) der Länge n erzeugen, wodurch jeder Aufruf von find O(n) benötigt.

Betrachten Sie die Vereinigungen 0→1→2→3→4 in dieser Reihenfolge. Der Aufruf von find für Knoten 0 muss die gesamte Kette durchlaufen. Mit Pfadkompression beseitigen wir dieses Problem, indem jeder besuchte Knoten während der find-Operation selbst direkt auf die Wurzel zeigt.

# Worst case without compression: a chain
# parent = [1, 2, 3, 4, 4]  => find(0) takes 4 steps
# After path compression: parent = [4, 4, 4, 4, 4]  => find(0) takes 1 step

parent = [1, 2, 3, 4, 4]
print('Before:', parent)
# Simulate find(0) with naive approach
x = 0
steps = 0
while parent[x] != x:
    x = parent[x]
    steps += 1
print('Root:', x, 'Steps taken:', steps)

Pfadkompression: Rekursiv in einem Durchlauf

Pfadkompression verändert die find-Operation so, dass nach dem Finden der Wurzel jeder Knoten entlang des Pfads so aktualisiert wird, dass er direkt auf die Wurzel zeigt. Zukünftige find-Aufrufe für diese Knoten benötigen dann O(1). Die rekursive Variante erreicht dies elegant in einem einzigen Durchlauf.

Die entscheidende Erkenntnis: Nachdem der rekursive Aufruf die Wurzel zurückgegeben hat, setzen wir vor der Rückgabe self.parent[x] = root. Dadurch wird der Baum abgeflacht – alle Knoten auf dem Suchpfad zeigen nun direkt auf die Wurzel. Dies ändert nicht, zu welcher Menge ein Knoten gehört; es verkürzt lediglich zukünftige Suchpfade.

class DSU:
    def __init__(self, n):
        self.parent = list(range(n))

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # path compression
        return self.parent[x]

    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px != py:
            self.parent[px] = py

dsu = DSU(5)
dsu.union(0, 1)
dsu.union(1, 2)
dsu.union(2, 3)
print('Root of 0:', dsu.find(0))
print('Parent array after compression:', dsu.parent)

Pfadkompression: Iterativ in zwei Durchläufen

Die iterative Variante der Pfadkompression verwendet zwei Durchläufe: Im ersten Durchlauf wird die Kette nach oben durchlaufen, um die Wurzel zu finden; im zweiten Durchlauf wird jeder Knoten des Pfads erneut besucht und sein Elternknoten direkt auf die Wurzel gesetzt. Dadurch entfällt der Aufwand für den Rekursionsstack, und die Variante ist auch bei sehr tiefen Bäumen nahe am Rekursionslimit von Python sicher.

Bei beiden Ansätzen bleibt die Korrektheit unverändert – find gibt weiterhin dieselbe Wurzel zurück. Der einzige Unterschied besteht darin, dass die Elternzeiger als Nebeneffekt aktualisiert werden, wodurch alle zukünftigen find-Aufrufe für diese Knoten O(1) benötigen.

class DSU:
    def __init__(self, n):
        self.parent = list(range(n))

    def find(self, x):
        root = x
        while self.parent[root] != root:
            root = self.parent[root]          # first pass: find root
        while self.parent[x] != root:
            nxt = self.parent[x]
            self.parent[x] = root             # second pass: compress
            x = nxt
        return root

    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px != py:
            self.parent[px] = py
            return True
        return False  # already connected

dsu = DSU(6)
for a, b in [(0,1),(1,2),(2,3),(3,4)]:
    dsu.union(a, b)
print('Parent before find(0):', dsu.parent[:])
dsu.find(0)
print('Parent after  find(0):', dsu.parent[:])

Amortisierte Komplexität der Pfadkompression

Allein die Pfadkompression erreicht über eine Folge von m Operationen eine amortisierte Laufzeit von O(log n) pro Operation. Eine find-Operation kann beim ersten Durchlaufen einer Kette teuer sein, flacht diese Kette jedoch ab, sodass jeder spätere find-Aufruf für diese Knoten O(1) benötigt. Die Gesamtkosten verteilen sich auf viele Operationen.

Die formale Analyse verwendet die Potenzialmethode: Das Potenzial der DSU sinkt jedes Mal, wenn sich der Elternpfad eines Knotens verkürzt, und diese Verringerung bezahlt die Kosten des Durchlaufens. Ohne Union by Rank liefert die Pfadkompression allein eine amortisierte Laufzeit von O(log n) – bereits eine enorme Verbesserung gegenüber dem naiven O(n).

# Demonstrating amortised benefit
import time

def build_chain(n):
    parent = list(range(n))
    for i in range(n - 1):
        parent[i] = i + 1  # chain: 0->1->2->...->n-1
    return parent

n = 1000
parent = build_chain(n)

# First find on a chain: visits n nodes
x = 0
root = x
while parent[root] != root:
    root = parent[root]
# Compress
while parent[x] != root:
    nxt = parent[x]; parent[x] = root; x = nxt
print('After first find, parent[0]:', parent[0])  # should be n-1
print('Second find cost: O(1) since parent[0] is now the root')

Anzahl zusammenhängender Komponenten

Eine häufige Anwendung der DSU ist das Zählen zusammenhängender Komponenten in einem Graphen. Wir initialisieren einen Zähler components mit n (einen Zähler pro Knoten). Jede erfolgreiche Union-Operation (das Zusammenführen zweier verschiedener Mengen) verringert den Zähler um 1. Am Ende enthält der Zähler die Anzahl der verschiedenen Komponenten.

Das ist effizienter, als für Zusammenhangsabfragen BFS oder DFS auszuführen, insbesondere wenn Kanten schrittweise (online) eintreffen. Die DSU verarbeitet jede Kante unabhängig vom Zeitpunkt ihres Eintreffens in nahezu O(1) amortisierter Zeit.

class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
        self.components = n

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px == py:
            return False
        self.parent[px] = py
        self.components -= 1
        return True

dsu = DSU(7)
edges = [(0,1),(1,2),(3,4),(5,6)]
for u, v in edges:
    dsu.union(u, v)
print('Components:', dsu.components)  # 4: {0,1,2}, {3,4}, {5,6}, {6 alone was merged}
# Node 6 is alone => 4 total: {0,1,2},{3,4},{5,6},{6} wait
# Let me recalculate: 7 nodes, 4 edges merged 4 pairs => 7-4=3... no
# {0,1,2} one union, {3,4} one, {5,6} one => 7-3=4 components
print('Expected: 4')

DSU für Graphprobleme: Anzahl der Provinzen

Das Problem Number of Provinces gibt eine n×n-Adjazenzmatrix vor und fragt, wie viele Gruppen direkt oder indirekt verbundener Städte existieren. Das ist genau ein Problem zur Bestimmung zusammenhängender Komponenten, das sich mit DSU elegant lösen lässt. Wir durchlaufen alle Paare (i, j), für die isConnected[i][j] == 1 gilt, und rufen union(i, j) auf.

Nach der Verarbeitung aller Verbindungen ist dsu.components die gesuchte Antwort. Das ist einfacher und schneller, als von jedem noch nicht besuchten Knoten aus eine BFS zu starten, und verarbeitet die Matrixdarstellung direkt, ohne zuvor eine Adjazenzliste erstellen zu müssen.

def find_provinces(isConnected):
    n = len(isConnected)
    parent = list(range(n))

    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]

    def union(x, y):
        px, py = find(x), find(y)
        if px != py:
            parent[px] = py
            return True
        return False

    count = n
    for i in range(n):
        for j in range(i + 1, n):
            if isConnected[i][j] == 1:
                if union(i, j):
                    count -= 1
    return count

matrix = [[1,1,0],[1,1,0],[0,0,1]]
print(find_provinces(matrix))  # 2: cities {0,1} and {2}

Varianten der Pfadkompression: Halbierung

Neben der Kompression in zwei Durchläufen gibt es eine einfachere Variante in einem Durchlauf, die Pfadhalbierung genannt wird: Während wir die Kette nach oben durchlaufen, lassen wir jeden Knoten auf seinen Großelternknoten statt auf seinen Elternknoten zeigen. Dadurch wird die Pfadlänge bei jedem Durchlauf halbiert, ohne dass ein zweiter Durchlauf erforderlich ist. In Kombination mit Union by Rank wird dieselbe amortisierte Komplexität von O(alpha(n)) erreicht.

Pfadhalbierung wird beim Competitive Programming häufig bevorzugt, weil sie aus einer einzigen übersichtlichen Schleife besteht und weder Rekursion noch einen zweiten Durchlauf benötigt. Jeder Schritt führt self.parent[x] = self.parent[self.parent[x]]; x = self.parent[x] aus.

class DSUHalving:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x):
        while self.parent[x] != x:
            self.parent[x] = self.parent[self.parent[x]]  # point to grandparent
            x = self.parent[x]
        return x

    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px == py:
            return False
        if self.rank[px] < self.rank[py]:
            px, py = py, px
        self.parent[py] = px
        if self.rank[px] == self.rank[py]:
            self.rank[px] += 1
        return True

dsu = DSUHalving(8)
for u, v in [(0,1),(2,3),(4,5),(6,7),(0,2),(4,6),(0,4)]:
    dsu.union(u, v)
print('All in one component:', dsu.find(0) == dsu.find(7))

Zusammenhang nach Union-Operationen prüfen

Um zu prüfen, ob zwei Knoten zusammenhängend sind (also zur selben Komponente gehören), rufen Sie find(x) == find(y) auf. Wenn beide Aufrufe dieselbe Wurzel zurückgeben, gehören die Knoten zur selben Komponente. Diese Zusammenhangsabfrage benötigt mit Pfadkompression amortisiert nahezu O(1) Zeit.

In Interviewaufgaben treten Zusammenhangsabfragen häufig zwischen Union-Operationen auf. DSU verarbeitet beides online – Sie können Union-Operationen und Abfragen in beliebiger Reihenfolge abwechseln. Das unterscheidet DSU von Algorithmen für statische Graphen wie BFS/DFS, die nach jeder strukturellen Änderung erneut ausgeführt werden müssen.

class DSU:
    def __init__(self, n):
        self.parent = list(range(n))

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px != py:
            self.parent[px] = py

    def connected(self, x, y):
        return self.find(x) == self.find(y)

dsu = DSU(10)
dsu.union(0, 3)
dsu.union(3, 7)
dsu.union(1, 5)
print(dsu.connected(0, 7))   # True: 0-3-7
print(dsu.connected(0, 5))   # False: different components
print(dsu.connected(1, 5))   # True: 1-5

Häufige Fehler bei der DSU-Implementierung

Ein häufiger Fehler besteht darin, find aufzurufen und anschließend parent falsch zu verändern. Rufen Sie immer find für beide Elemente auf, bevor Sie sie auf Gleichheit prüfen – andernfalls vergleichen Sie möglicherweise einen Knoten fälschlicherweise mit seiner eigenen Wurzel. Ein weiterer Fehler besteht darin zu vergessen, dass union keine Wirkung haben sollte, wenn beide Elemente bereits dieselbe Wurzel besitzen.

In Python kann das Rekursionstiefenlimit (standardmäßig 1000) bei großen Ketten mit rekursivem find einen RecursionError verursachen. Verwenden Sie entweder die iterative Variante mit zwei Durchläufen, erhöhen Sie das Limit mit sys.setrecursionlimit oder verwenden Sie die iterative Pfadhalbierung, um tiefe Rekursion vollständig zu vermeiden.

import sys
sys.setrecursionlimit(10000)  # needed for large recursive DSU

class DSU:
    def __init__(self, n):
        self.parent = list(range(n))

    def find(self, x):
        # Safe iterative path compression
        root = x
        while self.parent[root] != root:
            root = self.parent[root]
        while self.parent[x] != root:
            nxt = self.parent[x]
            self.parent[x] = root
            x = nxt
        return root

    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px == py:
            return False  # already same component — do nothing
        self.parent[px] = py
        return True

dsu = DSU(5)
print(dsu.union(0, 1))  # True: merged
print(dsu.union(0, 1))  # False: already merged — no double-counting

Größenverwaltung in DSU

Bei manchen Problemen benötigen Sie die Größe jeder Komponente und nicht nur ihre Wurzel. Fügen Sie ein size-Array hinzu, das mit lauter 1en initialisiert wird. Beim Zusammenführen zweier Komponenten addieren Sie die Größe der kleineren Wurzel zur größeren Wurzel. Dadurch sind Abfragen der Komponentengröße nach jeder Union-Operation in O(1) möglich.

Die Größenverwaltung bildet auch die Grundlage für Union by Size (eine Alternative zu Union by Rank): Hängen Sie den kleineren Baum immer unter die Wurzel des größeren Baums. Dadurch bleibt die Baumhöhe garantiert bei O(log n), was dieselbe asymptotische Garantie wie Union by Rank liefert.

class DSUWithSize:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px == py:
            return
        if self.size[px] < self.size[py]:
            px, py = py, px           # attach smaller under larger
        self.parent[py] = px
        self.size[px] += self.size[py]

    def get_size(self, x):
        return self.size[self.find(x)]

dsu = DSUWithSize(6)
for u, v in [(0,1),(1,2),(3,4)]:
    dsu.union(u, v)
print('Size of component containing 0:', dsu.get_size(0))  # 3
print('Size of component containing 3:', dsu.get_size(3))  # 2
print('Size of component containing 5:', dsu.get_size(5))  # 1

Schnelle Überprüfung

Testen Sie Ihr Verständnis der Konzepte von Data Structures & Algorithms — Coding Interview Prep aus dieser Lektion.

Zusammenfassung der Lektion

In dieser Lektion haben Sie gelernt: DSU verwaltet disjunkte Mengen mit den Operationen find und union, Pfadkompression flacht den Baum ab, indem sie alle durchlaufenen Knoten direkt auf die Wurzel zeigen lässt, und dadurch wird für find eine amortisierte Laufzeit von nahezu O(1) erreicht. Als Nächstes betrachten wir Union by Rank, das Bäume von oben nach unten flach hält, um die Schranke der inversen Ackermann-Funktion zu erreichen.

Häufig gestellte Fragen

Ist die Lektion „DSU mit Pfadkompression“ kostenlos?

Ja — der vollständige Text von „DSU mit Pfadkompression“ ist hier im Web kostenlos zu lesen. Um sie interaktiv zu üben (integrierter Code-Editor und 24/7 KI-Tutor) und den Rest des Coding Interview Prep-Kurses freizuschalten, upgrade auf CoddyKit PRO. Der Coding Interview Prep-Kurs umfasst insgesamt 4 Lektionen.

Was lerne ich in „DSU mit Pfadkompression“?

Implementieren Sie find mit Pfadkompression, sodass alle Knoten auf dem Pfad direkt auf die Wurzel zeigen und find amortisiert nahezu O(1) erreicht. Du übst Coding Interview Prep mit praktischem Code, den du direkt im Browser ausführst, und ein 24/7 KI-Tutor beantwortet deine Fragen während du die Lektion bearbeitest.

Brauche ich Erfahrung, um Coding Interview Prep zu starten?

Keine Vorkenntnisse erforderlich. Coding Interview Prep auf CoddyKit ist für Anfänger bis fortgeschrittene Lernende strukturiert, sodass du hier starten oder von Anfang an beginnen und in deinem eigenen Tempo voranschreiten kannst. Dies ist Lektion 1 von 4.

Wie lange dauert die Lektion „DSU mit Pfadkompression“?

Die meisten CoddyKit-Lektionen dauern etwa 5–10 Minuten. Jede ist kompakt und interaktiv, sodass du stetig Fortschritte machst und genau dort weitermachst, wo du aufgehört hast – im Web und in der App.

Kann ich in dieser Coding Interview Prep-Lektion Code schreiben und ausführen?

Ja. Jede Coding Interview Prep-Lektion enthält einen integrierten Code-Editor, sodass du echten Code direkt in deinem Browser schreibst und ausführst und sofort KI-Feedback erhältst — ohne lokale Einrichtung erforderlich.

Alle Lektionen in diesem Kurs

  1. DSU mit Pfadkompression
  2. Union nach Rang und die inverse-Ackermann-Schranke
  3. Redundante Verbindung und Zykluserkennung
  4. Accounts Merge und zusammenhängende Komponenten
← Zurück zu Coding Interview Prep