0Pricing
DSA Interview Prep · Lektion

Redundante Verbindung und Zykluserkennung

Erkennen Sie in einem ungerichteten Graphen die Kante, die einen Zyklus erzeugt, indem Sie auf jede Kante Union anwenden und prüfen, ob zwei Knoten bereits verbunden sind.

Redundante Verbindung und Zykluserkennung ist eine kostenlose DSA Interview Prep-Lektion auf CoddyKit. Dies ist Lektion 3 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 DSA Interview Prep-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der DSA Interview Prep-Kurs umfasst insgesamt 4 Lektionen.

Was ist eine redundante Verbindung?

Das Problem Redundant Connection (LeetCode 684) gibt Ihnen einen Baum mit n Knoten und einer zusätzlichen Kante, die genau einen Zyklus bildet. Ihre Aufgabe besteht darin, die Kante zu finden, deren Entfernung den Baum wiederherstellt. Falls mehrere Antworten möglich sind, geben Sie diejenige zurück, die in der Eingabeliste zuletzt vorkommt.

Ein Baum mit n Knoten hat genau n-1 Kanten, ist zusammenhängend und enthält keine Zyklen. Das Hinzufügen einer weiteren Kante erzeugt genau einen Zyklus. Die hinzugefügte (redundante) Kante verbindet zwei Knoten, die bereits in derselben Komponente lagen – ein klassischer Anwendungsfall für die Zykluserkennung mit DSU.

# Example
# n=5, edges = [[1,2],[1,3],[2,3],[2,4],[3,5]]
# Adding edge [2,3] creates cycle 1-2-3-1
# So [2,3] is the redundant connection

# Key insight: process edges one by one with DSU
# The FIRST edge where both endpoints are already connected is the redundant one
print('Tree property: n nodes, n-1 edges, no cycles')
print('Adding 1 edge: n nodes, n edges, exactly 1 cycle')
print('DSU approach: find the edge that connects already-connected nodes')

Zykluserkennung mit DSU

DSU erkennt Zyklen auf natürliche Weise: Prüfen Sie vor dem Hinzufügen einer Kante (u, v), ob find(u) == find(v) gilt. Wenn beide Knoten dieselbe Wurzel haben, sind sie bereits verbunden – das Hinzufügen dieser Kante erzeugt einen Zyklus. Dies ist die redundante Kante.

Dieser Ansatz funktioniert für ungerichtete Graphen. Für jede Kante führen wir entweder erfolgreich eine Vereinigung der beiden Komponenten durch (noch kein Zyklus) oder stellen fest, dass beide Endpunkte bereits in derselben Komponente liegen (Zyklus gefunden). Die Zeitkomplexität beträgt O(n × alpha(n)), also nahezu O(n).

def find_redundant_connection(edges):
    n = len(edges)
    parent = list(range(n + 1))  # 1-indexed
    rank = [0] * (n + 1)

    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:
            return False           # same component => cycle found
        if rank[px] < rank[py]: px, py = py, px
        parent[py] = px
        if rank[px] == rank[py]: rank[px] += 1
        return True

    for u, v in edges:
        if not union(u, v):
            return [u, v]     # this edge creates the cycle

edges = [[1,2],[1,3],[2,3],[2,4],[3,5]]
print(find_redundant_connection(edges))  # [2, 3]

Den Algorithmus Schritt für Schritt nachvollziehen

Betrachten wir [[1,2],[1,3],[2,3]] Schritt für Schritt. Anfangs bildet jeder Knoten seine eigene Komponente: {1}, {2}, {3}.

  • Kante [1,2]: find(1)=1, find(2)=2, verschieden – führen Sie sie zusammen. Komponenten: {1,2}, {3}
  • Kante [1,3]: find(1)=root, find(3)=3, verschieden – führen Sie sie zusammen. Komponenten: {1,2,3}
  • Kante [2,3]: find(2)=root, find(3)=root – gleiche Wurzel! Zyklus erkannt. Geben Sie [2,3] zurück.

Der Algorithmus verarbeitet die Kanten in ihrer Reihenfolge und gibt die erste Kante zurück, die einen Zyklus schließt. Da das Problem genau eine zusätzliche Kante garantiert, ist dies immer die richtige redundante Kante.

def find_redundant_trace(edges):
    parent = list(range(len(edges) + 1))

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

    for u, v in edges:
        pu, pv = find(u), find(v)
        print(f'Edge ({u},{v}): find({u})={pu}, find({v})={pv}', end=' => ')
        if pu == pv:
            print('CYCLE DETECTED!')
            return [u, v]
        parent[pv] = pu
        print('merged')
    return []

result = find_redundant_trace([[1,2],[1,3],[2,3]])
print('Redundant edge:', result)

Zykluserkennung in ungerichteten Graphen mit DFS

Eine Alternative zu DSU für die Zykluserkennung in ungerichteten Graphen ist DFS mit Verfolgung des Elternknotens. Während der DFS haben wir eine Rückwärtskante gefunden, wenn wir einen Knoten erreichen, der bereits besucht wurde und nicht der direkte Elternknoten des aktuellen Knotens ist – dies weist auf einen Zyklus hin.

Der DFS-Ansatz benötigt jedoch O(V + E) Zeit und liefert zwar die Information, ob ein Zyklus existiert, aber nicht ohne Weiteres, welche konkrete Kante redundant ist. DSU wird für Probleme bevorzugt, bei denen Sie die konkrete redundante Kante bestimmen sollen, weil Sie sie automatisch finden, sobald die Vereinigung fehlschlägt.

from collections import defaultdict

def has_cycle_dfs(n, edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = set()

    def dfs(node, parent):
        visited.add(node)
        for nb in graph[node]:
            if nb == parent:
                continue           # skip the edge we came from
            if nb in visited:
                return True        # back edge => cycle
            if dfs(nb, node):
                return True
        return False

    for node in range(1, n + 1):
        if node not in visited:
            if dfs(node, -1):
                return True
    return False

print(has_cycle_dfs(3, [[1,2],[1,3],[2,3]]))  # True
print(has_cycle_dfs(3, [[1,2],[1,3]]))        # False

Zykluserkennung in gerichteten Graphen

Für gerichtete Graphen funktioniert die Zykluserkennung mit DSU nicht direkt, da Kanten eine Richtung haben. Verwenden Sie stattdessen DFS mit Drei-Farben-Markierung: Weiß (unbesucht), Grau (im aktuellen DFS-Pfad), Schwarz (vollständig verarbeitet). Eine Rückwärtskante zu einem grauen Knoten weist auf einen Zyklus hin.

In einem ungerichteten Graphen bedeutet jede Rückwärtskante einen Zyklus. In einem gerichteten Graphen ist eine Querkante zu einem schwarzen Knoten kein Zyklus – nur Rückwärtskanten zu grauen Knoten sind es. Dieser Unterschied ist entscheidend und wird in Problemen zur Kursplanung geprüft.

def has_cycle_directed(n, edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)

    # 0=white(unvisited), 1=grey(in stack), 2=black(done)
    color = [0] * (n + 1)

    def dfs(node):
        color[node] = 1            # grey: currently visiting
        for nb in graph[node]:
            if color[nb] == 1:
                return True        # back edge to grey node => cycle
            if color[nb] == 0:
                if dfs(nb):
                    return True
        color[node] = 2            # black: fully processed
        return False

    for node in range(1, n + 1):
        if color[node] == 0:
            if dfs(node):
                return True
    return False

from collections import defaultdict
print(has_cycle_directed(3, [[1,2],[2,3],[3,1]]))  # True: 1->2->3->1
print(has_cycle_directed(3, [[1,2],[1,3],[2,3]]))  # False

Redundant Connection II: Variante für gerichtete Graphen

LeetCode 685 erweitert das Problem auf gerichtete Graphen, in denen jeder Knoten genau einen Elternknoten hat (ein verwurzelter Baum mit einer zusätzlichen Kante). Es gibt zwei Fälle: Entweder hat ein Knoten zwei Elternknoten (Eingangsgrad 2), oder es gibt einen Zyklus, ohne dass ein Knoten zwei Elternknoten hat.

Die Lösung sucht zunächst nach Knoten mit Eingangsgrad 2. Wenn ein solcher Knoten gefunden wird, muss eine seiner beiden eingehenden Kanten die gesuchte sein. Anschließend bestimmt die Zykluserkennung mit DSU, welche der beiden Kandidatenkanten entfernt werden muss. Dieser zweistufige Ansatz behandelt alle Fälle korrekt.

def find_redundant_directed(edges):
    n = len(edges)
    parent_map = {}          # node -> its parent in the input
    candidate1 = candidate2 = None

    for u, v in edges:
        if v in parent_map:                # v already has a parent
            candidate1 = [parent_map[v], v]  # earlier edge
            candidate2 = [u, v]              # later edge
        else:
            parent_map[v] = u

    # DSU cycle detection, skipping candidate2 if it exists
    dsu = list(range(n + 1))
    def find(x):
        while dsu[x] != x: dsu[x] = dsu[dsu[x]]; x = dsu[x]
        return x
    def union(x, y):
        px, py = find(x), find(y)
        if px == py: return False
        dsu[px] = py; return True

    for u, v in edges:
        if candidate2 and [u, v] == candidate2: continue   # skip candidate2
        if not union(u, v):              # cycle found without candidate2
            return candidate1 if candidate1 else [u, v]

    return candidate2   # no cycle when excluding candidate2 => candidate2 is redundant

print(find_redundant_directed([[1,2],[1,3],[2,3]]))  # [2,3]
print(find_redundant_directed([[1,2],[2,3],[3,4],[4,1],[1,5]]))  # [4,1]

Gültigkeit des Graphen nach dem Entfernen einer Kante

Nachdem wir die redundante Kante identifiziert haben, können wir das Ergebnis überprüfen, indem wir sicherstellen, dass ihre Entfernung einen gültigen Baum zurücklässt: genau n-1 Kanten, alle Knoten verbunden und keine Zyklen. Für die Zwecke des Vorstellungsgesprächs garantiert DSU dies automatisch – wenn wir die Kante zurückgeben, bei der die Vereinigung fehlgeschlagen ist, bleiben genau die n-1 Kanten übrig, deren Vereinigung erfolgreich war und die einen Spannbaum bilden.

Diese Garantie macht DSU für dieses Problem so übersichtlich: Erfolgreiche Vereinigungen bauen den Baum schrittweise auf, und die fehlgeschlagene Vereinigung identifiziert die eine Kante, die nicht dazugehört.

def verify_tree(n, edges, removed_edge):
    parent = list(range(n + 1))

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

    components = n
    for u, v in edges:
        if [u, v] == removed_edge:
            continue         # skip the removed edge
        pu, pv = find(u), find(v)
        if pu == pv:
            print('CYCLE DETECTED after removal! Wrong answer.')
            return False
        parent[pv] = pu
        components -= 1

    if components != 1:
        print(f'Graph not connected ({components} components). Wrong answer.')
        return False
    print('Valid tree after removing edge:', removed_edge)
    return True

edges = [[1,2],[1,3],[2,3]]
verify_tree(3, edges, [2,3])
verify_tree(3, edges, [1,2])  # wrong removal

Analyse der Zeit- und Speicherkomplexität

Die DSU-basierte Lösung für Redundant Connection verarbeitet jede der n Kanten genau einmal, und jede union/find-Operation kostet amortisiert O(alpha(n)). Gesamtlaufzeit: O(n × alpha(n)), also effektiv O(n).

Die Speicherkomplexität beträgt O(n) für die parent- und rank-Arrays. Das ist optimal – Sie müssen mindestens alle n Kanten einlesen und für jeden Knoten einen Zustand speichern. Vergleichen Sie dies mit einem naiven Ansatz, der nach jedem Einfügen einer Kante eine DFS ausführt: O(n²) Zeit und O(n + E) Speicher.

# Summary of complexities
complexity = {
    'Naive (DFS after each edge)': {'time': 'O(n^2)', 'space': 'O(n)'},
    'DSU (path compression + rank)': {'time': 'O(n * alpha(n))', 'space': 'O(n)'},
    'Sorting + DSU (Kruskal style)': {'time': 'O(n log n)', 'space': 'O(n)'},
}
for approach, costs in complexity.items():
    print(f'{approach}:')
    print(f'  Time:  {costs["time"]}')
    print(f'  Space: {costs["space"]}')
    print()
print('alpha(n) <= 4 for all practical n, so DSU is effectively O(n).')

Sonderfall: Schleife auf sich selbst

Eine Schleifenkante [u, u] erzeugt sofort einen Zyklus, da beide Endpunkte derselbe Knoten sind. In DSU gilt find(u) == find(u) immer, daher schlägt die Vereinigung sofort fehl und [u, u] wird als redundante Kante zurückgegeben.

Die Einschränkungen der meisten Probleme schließen Schleifen auf sich selbst aus, aber robuster Code sollte damit umgehen können. Die DSU-Implementierung behandelt diesen Fall automatisch, ohne einen Sonderfall zu benötigen – die Zyklusprüfung if find(u) == find(v) erkennt ihn, bevor eine Vereinigung versucht wird. Überprüfen Sie den Code immer mit Eingaben für Sonderfälle, etwa Schleifen bei einem einzelnen Knoten und Eingaben mit minimaler Größe.

def find_redundant_robust(edges):
    n = len(edges)
    parent = list(range(n + 1))

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

    for u, v in edges:
        pu, pv = find(u), find(v)
        if pu == pv:
            return [u, v]   # handles self-loops too: u==v => pu==pv always
        parent[pv] = pu
    return []

# Self-loop test
print(find_redundant_robust([[1,2],[2,2]]))    # [2,2] self-loop
# Minimum tree test
print(find_redundant_robust([[1,2],[2,3],[1,3]]))  # [1,3]
# Standard test
print(find_redundant_robust([[1,2],[1,3],[2,3],[2,4],[3,5]]))  # [2,3]

Zykluserkennung über verschiedene Algorithmen hinweg verallgemeinern

Mehrere Algorithmen können Zyklen erkennen, wobei jeder für unterschiedliche Szenarien geeignet ist:

  • DSU: ungerichtete Graphen, Eintreffen von Kanten online, O(alpha(n)) pro Kante – am besten zum Zählen oder Finden der redundanten Kante
  • DFS mit Verfolgung des Elternknotens: ungerichtete Graphen, alle Kanten im Voraus bekannt, O(V+E) – am besten, wenn Sie den Zykluspfad benötigen
  • DFS mit Drei-Farben-Markierung: gerichtete Graphen, Erkennung von Rückwärtskanten, O(V+E) – am besten für Kursplanungs- und topologische-Sortierung-Probleme
  • Topologische Sortierung (Kahns Algorithmus): gerichtete Graphen, erkennt einen Zyklus anhand übrig gebliebener Knoten mit einem Eingangsgrad ungleich null – am besten, wenn Sie zusätzlich eine Reihenfolge benötigen
# When to use which cycle-detection method:
# Problem type => preferred algorithm

problems = [
    ('Redundant Connection (undirected)', 'DSU'),
    ('Course Schedule (directed)', 'DFS three-color or Kahn topological sort'),
    ('Detect cycle in undirected graph', 'DFS with parent tracking or DSU'),
    ('Find cycle members in directed graph', 'DFS three-color + backtrack'),
    ('Online graph edges with cycle check', 'DSU'),
    ('Minimum spanning tree validity', 'DSU (Kruskal)'),
]
for problem, solution in problems:
    print(f'{problem}\n  => {solution}\n')

Vollständige Lösung mit Sonderfällen

Hier ist eine Lösung in Produktionsqualität für Redundant Connection, die alle Sonderfälle behandelt: 1-basierte Knotenindizierung, genau eine redundante Kante und die Garantie, dass ihre Entfernung einen gültigen Baum hinterlässt. Sie verwendet die optimale DSU mit Pfadhalbierung und Vereinigung nach Rang.

Versuchen Sie nach dem Absenden die Zusatzfrage: Was wäre, wenn der Graph mehrere redundante Kanten enthalten könnte? Sie müssten alle Kanten verfolgen, die einen Zyklus schließen, und die letzte Kante in der Eingabe zurückgeben – dieselbe gierige Strategie funktioniert weiterhin, da DSU die Kanten in ihrer Reihenfolge verarbeitet.

def find_redundant_connection(edges):
    n = len(edges)
    parent = list(range(n + 1))
    rank = [0] * (n + 1)

    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]   # path halving
            x = parent[x]
        return x

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

    for u, v in edges:
        if not union(u, v):
            return [u, v]
    return []  # should never reach here given valid input

test_cases = [
    [[1,2],[1,3],[2,3]],
    [[1,2],[2,3],[3,4],[1,4],[1,5]],
    [[1,2],[1,3],[2,3],[2,4],[3,5]],
]
for tc in test_cases:
    print(find_redundant_connection(tc))

Kurzer Test

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

Zusammenfassung der Lektion

In dieser Lektion haben Sie gelernt: Eine redundante Verbindung ist eine Kante, die zwei bereits verbundene Knoten in einem ungerichteten Graphen verbindet, DSU erkennt dies, indem vor union geprüft wird, ob find(u) == find(v) gilt, und gibt diese Kante zurück, und gerichtete Graphen benötigen für die Zykluserkennung eine DFS mit Drei-Farben-Markierung oder Kahns Algorithmus anstelle von DSU. Als Nächstes wenden wir DSU auf das Problem Accounts Merge an, bei dem E-Mail-Adressen die Knoten sind und gemeinsame E-Mail-Adressen Vereinigungen auslösen.

Häufig gestellte Fragen

Ist die Lektion „Redundante Verbindung und Zykluserkennung“ kostenlos?

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

Was lerne ich in „Redundante Verbindung und Zykluserkennung“?

Erkennen Sie in einem ungerichteten Graphen die Kante, die einen Zyklus erzeugt, indem Sie auf jede Kante Union anwenden und prüfen, ob zwei Knoten bereits verbunden sind. Du übst DSA 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 DSA Interview Prep zu starten?

Keine Vorkenntnisse erforderlich. DSA 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 3 von 4.

Wie lange dauert die Lektion „Redundante Verbindung und Zykluserkennung“?

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 DSA Interview Prep-Lektion Code schreiben und ausführen?

Ja. Jede DSA 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 DSA Interview Prep