Система непересекающихся множеств. Поиск компонент связности, циклов в графе.
Система непересекающихся множеств для эффективного объединения и проверки принадлежности. Path compression + union by rank = O(α(n)) ≈ O(1).
Union-Find (DSU) — это структура данных для работы с непересекающимися множествами. Она позволяет эффективно:
Куча (Heap) — древовидная структура, где родитель больше (max-heap) или меньше (min-heap) детей.
Дерево отрезков (Segment Tree) — структура для диапазонных запросов и обновлений.
Префиксное дерево (Trie) — дерево для хранения строк, где каждый узел представляет префикс.
Система непересекающихся множеств (DSU/Union-Find) — структура для эффективного объединения множеств и проверки принадлежности.
Дерево 1: Дерево 2:
4 5
/ \ |
1 3 6
/
2
parent = [0, 1, 1, 4, 4, 5, 5] # parent[i] = родитель узла i
rank = [0, 1, 0, 0, 2, 1, 0] # rank[i] = высота дерева
| Задача | Union-Find | DFS/BFS | Floyd-Warshall |
|---|---|---|---|
| Компоненты связности | ✅ O(m × α(n)) | ✅ O(n + m) | ❌ O(n³) |
| Динамический граф | ✅ Да | ❌ Нет | ❌ Нет |
| Проверка связности пары | ✅ O(α(n)) | ❌ O(n + m) | ✅ O(1) |
| Реализация | ⚠️ Средняя | ✅ Простая | ✅ Простая |
Вывод: Union-Find лучше для динамических графов и множественных запросов связности.
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.components = 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):
rootX, rootY = self.find(x), self.find(y)
if rootX == rootY:
return False
# Union by rank
if self.rank[rootX] < self.rank[rootY]:
rootX, rootY = rootY, rootX
self.parent[rootY] = rootX
self.rank[rootX] += self.rank[rootX] == self.rank[rootY]
self.components -= 1
return True
def connected(self, x, y):
return self.find(x) == self.find(y)Условие:
Дана матрица смежности isConnected размера n x n, где isConnected[i][j] = 1, если город i напрямую соединён с городом j.
Верните количество провинций. Провинция — группа городов, соединённых напрямую или косвенно.
Примеры:
Ввод: isConnected = [[1,1,0],[1,1,0],[0,0,1]]
Вывод: 2
Ввод: isConnected = [[1,0,0],[0,1,0],[0,0,1]]
Вывод: 3
Ограничения:
💡 Подсказка: Используйте Union-Find для объединения соединённых городов. Количество компонент = количество провинций.
Изначально: 5 элементов (каждый — отдельное множество)
[0] [1] [2] [3] [4]
parent = [0, 1, 2, 3, 4]
Шаг 1: union(0, 1)
[0→1] [2] [3] [4]
parent = [1, 1, 2, 3, 4]
Компоненты: {0,1}, {2}, {3}, {4}
Шаг 2: union(1, 2)
[0→1→2] [3] [4]
parent = [1, 2, 2, 3, 4]
Компоненты: {0,1,2}, {3}, {4}
Шаг 3: union(3, 4)
[0→1→2] [3→4]
parent = [1, 2, 2, 4, 4]
Компоненты: {0,1,2}, {3,4}
Шаг 4: union(2, 4) — объединяем компоненты
[0→1→2→4←3]
parent = [1, 2, 4, 4, 4]
Компоненты: {0,1,2,3,4} — все в одном множестве!
find(0): 0→1→2→4 (возвращает 4)
find(3): 3→4 (возвращает 4)
connected(0, 3): True (оба имеют корень 4)
Решение:
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * 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):
rootX, rootY = self.find(x), self.find(y)
if rootX == rootY:
return False
if self.rank[rootX] < self.rank[rootY]:
rootX, rootY = rootY, rootX
self.parent[rootY] = rootX
self.rank[rootX] += self.rank[rootX] == self.rank[rootY]
self.components -= 1
return True
def findCircleNum(isConnected):
n = len(isConnected)
uf = UnionFind(n)
for i in range(n):
for j in range(i + 1, n):
if isConnected[i][j] == 1:
uf.union(i, j)
return uf.componentsОбъяснение:
Сложность:
Условие:
Дано дерево с n узлами (помечены от 1 до n) и одним дополнительным ребром. Верните ребро, которое можно удалить, чтобы получить дерево.
Если есть несколько ответов, верните последнее ребро во входном массиве.
Примеры:
Ввод: edges = [[1,2],[1,3],[2,3]]
Вывод: [2,3]
Ввод: edges = [[1,2],[2,3],[3,4],[1,4],[1,5]]
Вывод: [1,4]
Ограничения:
💡 Подсказка: Проходите по рёбрам. Первое ребро, где union() возвращает False (корни совпали), создаёт цикл.
До path compression:
4
/|\
1 2 3
/
0
find(0): 0→1→4 (длина пути = 2)
После path compression:
4
/| \
0 1 2 3
find(0): 0→4 (длина пути = 1, узел 0 подключён напрямую к корню!)
Решение:
class UnionFind:
def __init__(self, n):
self.parent = list(range(n + 1))
self.rank = [0] * (n + 1)
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):
rootX, rootY = self.find(x), self.find(y)
if rootX == rootY:
return False
if self.rank[rootX] < self.rank[rootY]:
rootX, rootY = rootY, rootX
self.parent[rootY] = rootX
self.rank[rootX] += self.rank[rootX] == self.rank[rootY]
return True
def findRedundantConnection(edges):
uf = UnionFind(len(edges))
for u, v in edges:
if not uf.union(u, v):
return [u, v]Объяснение:
Сложность:
Ссылка: https://leetcode.com/problems/graph-validity/ (премиум)
Условие:
Дано n узлов и список рёбер. Определите, образует ли граф валидное дерево.
Валидное дерево должно:
Примеры:
Ввод: n = 5, edges = [[0,1],[0,2],[0,3],[1,4]]
Вывод: true
Ввод: n = 5, edges = [[0,1],[1,2],[2,3],[1,3],[1,4]]
Вывод: false
Ограничения:
💡 Подсказка: Проверьте: 1) Количество рёбер = n-1. 2) Нет циклов (union возвращает False). 3) Одна компонента.
Решение:
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * 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):
rootX, rootY = self.find(x), self.find(y)
if rootX == rootY:
return False
if self.rank[rootX] < self.rank[rootY]:
rootX, rootY = rootY, rootX
self.parent[rootY] = rootX
self.rank[rootX] += self.rank[rootX] == self.rank[rootY]
self.components -= 1
return True
def validTree(n, edges):
# Дерево должно иметь ровно n-1 ребро
if len(edges) != n - 1:
return False
uf = UnionFind(n)
for u, v in edges:
if not uf.union(u, v):
return False # Обнаружен цикл
return uf.components == 1Объяснение:
Сложность:
Условие: Дан список аккаунтов, где каждый аккаунт — список строк: первое имя, затем email.
Аккаунты с общими email принадлежат одному человеку. Объедините аккаунты и верните список в формате: [имя, отсортированные email].
Примеры:
Ввод: accounts = [
["John","johnsmith@mail.com","john_newyork@mail.com"],
["John","johnsmith@mail.com","john00@mail.com"],
["Mary","mary@mail.com"]
]
Вывод: [
["John","john00@mail.com","john_newyork@mail.com","johnsmith@mail.com"],
["Mary","mary@mail.com"]
]
Ограничения:
💡 Подсказка: Используйте Union-Find. Для каждого email храните индекс аккаунта. Если email встречается снова — объединяйте аккаунты.
Решение:
import collections
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * 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):
rootX, rootY = self.find(x), self.find(y)
if rootX != rootY:
if self.rank[rootX] < self.rank[rootY]:
rootX, rootY = rootY, rootX
self.parent[rootY] = rootX
self.rank[rootX] += self.rank[rootX] == self.rank[rootY]
def accountsMerge(accounts):
uf = UnionFind(len(accounts))
email_to_account = {}
# Объединяем аккаунты с общими email
for i, account in enumerate(accounts):
for email in account[1:]:
if email in email_to_account:
uf.union(i, email_to_account[email])
else:
email_to_account[email] = i
# Группируем email по корневому аккаунту
emails_by_account = collections.defaultdict(list)
for email, account in email_to_account.items():
root = uf.find(account)
emails_by_account[root].append(email)
# Формируем результат
result = []
for account_idx, emails in emails_by_account.items():
result.append([accounts[account_idx][0]] + sorted(emails))
return resultОбъяснение:
Сложность:
Условие:
Дан неориентированный граф с n узлами и рёбрами трёх типов:
Верните максимальное количество рёбер, которые можно удалить, чтобы и Alice, и Bob могли полностью traversровать граф. Если невозможно, верните -1.
Примеры:
Ввод: n = 4, edges = [[3,1,2],[3,2,3],[1,1,3],[1,2,4],[1,1,2],[2,3,4]]
Вывод: 2
Ограничения:
💡 Подсказка: Приоритет рёбрам типа 3. Сначала обрабатываем их, затем типа 1 (Alice) и типа 2 (Bob).
Решение:
class UnionFind:
def __init__(self, n):
self.parent = list(range(n + 1))
self.rank = [0] * (n + 1)
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):
rootX, rootY = self.find(x), self.find(y)
if rootX == rootY:
return False
if self.rank[rootX] < self.rank[rootY]:
rootX, rootY = rootY, rootX
self.parent[rootY] = rootX
self.rank[rootX] += self.rank[rootX] == self.rank[rootY]
self.components -= 1
return True
def maxNumEdgesToRemove(n, edges):
uf_alice = UnionFind(n)
uf_bob = UnionFind(n)
used = 0
# Тип 3 (оба) — приоритет
for t, u, v in edges:
if t == 3:
if uf_alice.union(u, v):
uf_bob.union(u, v)
used += 1
# Тип 1 (Alice) и 2 (Bob)
for t, u, v in edges:
if t == 1 and uf_alice.union(u, v):
used += 1
elif t == 2 and uf_bob.union(u, v):
used += 1
# Проверка связности
if uf_alice.components > 1 or uf_bob.components > 1:
return -1
return len(edges) - usedОбъяснение:
Сложность:
Условие:
Дана доска m x n, заполненная 'X' и 'O'. Захватите все области, окружённые 'X', заменив 'O' на 'X'.
Область захватывается, если она не соединена с границей доски.
Примеры:
Ввод: board = [
["X","X","X","X"],
["X","O","O","X"],
["X","X","O","X"],
["X","O","X","X"]
]
Вывод: [
["X","X","X","X"],
["X","X","X","X"],
["X","X","X","X"],
["X","O","X","X"]
]
Ограничения:
💡 Подсказка: Используйте dummy узел для границы. Все 'O' на границе объединяйте с dummy. Затем 'O', не connected с dummy, заполняйте 'X'.
Решение:
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * 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):
rootX, rootY = self.find(x), self.find(y)
if rootX != rootY:
if self.rank[rootX] < self.rank[rootY]:
rootX, rootY = rootY, rootX
self.parent[rootY] = rootX
def connected(self, x, y):
return self.find(x) == self.find(y)
def solve(board):
if not board:
return
m, n = len(board), len(board[0])
uf = UnionFind(m * n + 1) # +1 для dummy
dummy = m * n
def idx(r, c):
return r * n + c
# Объединяем 'O' с соседями и с dummy если на границе
for i in range(m):
for j in range(n):
if board[i][j] == 'O':
if i == 0 or i == m-1 or j == 0 or j == n-1:
uf.union(idx(i, j), dummy)
else:
for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
ni, nj = i + dr, j + dc
if board[ni][nj] == 'O':
uf.union(idx(i, j), idx(ni, nj))
# Заполняем 'O', не connected с dummy
for i in range(m):
for j in range(n):
if board[i][j] == 'O' and not uf.connected(idx(i, j), dummy):
board[i][j] = 'X'Объяснение:
Сложность:
| Задача | Ключевая идея |
|---|---|
| Number of Provinces | Подсчёт компонент |
| Redundant Connection | Первое ребро, где union() = False |
| Valid Tree | n-1 рёбер + 1 компонента |
| Accounts Merge | union по email |
| Remove Max Edges | Приоритет типу 3 |
| Surrounded Regions | union с boundary dummy |
Проблема: Без path compression сложность find() становится O(n).
# ❌ Неправильно — нет path compression
def find(self, x):
if self.parent[x] != x:
return self.find(self.parent[x]) # Просто рекурсия!
return self.parent[x]
# Дерево может выродиться в цепочку O(n)
# ✅ Правильно — с path compression
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # Path compression!
return self.parent[x]
# Амортизированно O(α(n)) ≈ O(1)Почему это неправильно: Без path compression дерево может выродиться в цепочку.
Как обнаружить: Time Limit Exceeded на больших тестах.
Как исправить: Добавьте self.parent[x] = self.find(self.parent[x]).
Проблема: Без union by rank дерево может стать несбалансированным.
# ❌ Неправильно — нет union by rank
def union(self, x, y):
rootX, rootY = self.find(x), self.find(y)
if rootX != rootY:
self.parent[rootY] = rootX # Просто прикрепляем!
# Может стать несбалансированным
# ✅ Правильно — с union by rank
def union(self, x, y):
rootX, rootY = self.find(x), self.find(y)
if rootX == rootY:
return False
if self.rank[rootX] < self.rank[rootY]:
rootX, rootY = rootY, rootX
self.parent[rootY] = rootX
self.rank[rootX] += self.rank[rootX] == self.rank[rootY]
self.components -= 1
return TrueПочему это неправильно: Несбалансированное дерево ухудшает производительность.
Проблема: Не уменьшаем счётчик компонент при объединении.
# ❌ Неправильно — забыли уменьшить components
def union(self, x, y):
rootX, rootY = self.find(x), self.find(y)
if rootX != rootY:
self.parent[rootY] = rootX
# components -= 1 забыли!
# ✅ Правильно
def union(self, x, y):
rootX, rootY = self.find(x), self.find(y)
if rootX == rootY:
return False
self.parent[rootY] = rootX
self.components -= 1 # Уменьшаем!
return TrueПочему это неправильно: components будет завышен.
Проблема: Неправильная инициализация parent или rank.
# ❌ Неправильно — parent[i] = 0 для всех
self.parent = [0] * n # Все указывают на 0!
# ✅ Правильно — каждый элемент — свой корень
self.parent = list(range(n)) # parent[i] = iПочему это неправильно: Все элементы будут в одном множестве с начала.
Проблема: Объединение без проверки, что корни разные.
# ❌ Неправильно — нет проверки rootX == rootY
def union(self, x, y):
rootX, rootY = self.find(x), self.find(y)
self.parent[rootY] = rootX # Всегда объединяем!
# components уменьшится даже если уже в одном множестве
# ✅ Правильно — проверка
def union(self, x, y):
rootX, rootY = self.find(x), self.find(y)
if rootX == rootY:
return False # Уже в одном множестве!
self.parent[rootY] = rootX
self.components -= 1
return TrueПочему это неправильно: components станет отрицательным.
Условие: Дан неориентированный граф с n вершинами и список рёбер. Определите, является ли граф деревом (связный граф без циклов).
Пример:
Ввод: n = 5, edges = [[0,1],[0,2],[0,3],[1,4]]
Вывод: True
Объяснение: Граф связный и без циклов
Ввод: n = 4, edges = [[0,1],[1,2],[2,0],[0,3]]
Вывод: False
Объяснение: Есть цикл 0-1-2-0
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * 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):
rootX, rootY = self.find(x), self.find(y)
if rootX == rootY:
return False # Цикл!
if self.rank[rootX] < self.rank[rootY]:
rootX, rootY = rootY, rootX
self.parent[rootY] = rootX
self.rank[rootX] += self.rank[rootX] == self.rank[rootY]
self.components -= 1
return True
def validTree(n, edges):
# Дерево должно иметь ровно n-1 ребро
if len(edges) != n - 1:
return False
uf = UnionFind(n)
for u, v in edges:
if not uf.union(u, v): # Если уже в одном множестве — цикл!
return False
return uf.components == 1 # Должна быть 1 компонентаОбъяснение:
Сложность:
Union-Find эффективен для:
Оптимизации:
Сложность: O(α(n)) ≈ O(1) на операцию.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.