Объединение аккаунтов и компоненты связности
Сгруппируйте аккаунты с общим адресом электронной почты, рассматривая адреса как узлы DSU, а затем соберите все адреса каждой компоненты для восстановления объединённых аккаунтов
«Объединение аккаунтов и компоненты связности» — бесплатный урок DSA Interview Prep на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DSA Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DSA Interview Prep содержит 4 уроков всего.
Задача: объединение учетных записей
В задаче «Объединение учетных записей» (LeetCode 721) дан список учетных записей, каждая из которых представляет собой список строк: первый элемент — имя учетной записи, а остальные — адреса электронной почты. Две учетные записи принадлежат одному человеку, если у них есть хотя бы один общий адрес. Объедините все учетные записи одного человека и верните отсортированные списки адресов.
По сути, это задача о связных компонентах, где адреса электронной почты являются вершинами, а общая учетная запись связывает их. DSU — идеальный инструмент: выполните union для всех адресов внутри одной учетной записи, а затем соберите адреса по компонентам.
# Example input
accounts = [
['John', 'john@mail.com', 'john1@mail.com'],
['John', 'john2@mail.com'],
['Mary', 'mary@mail.com'],
['John', 'john1@mail.com', 'john2@mail.com'],
]
# john@mail.com and john1@mail.com are in account[0]
# john1@mail.com and john2@mail.com are in account[3]
# => john@, john1@, john2@ are all the same person
# Expected output:
# ['John', 'john1@mail.com', 'john2@mail.com', 'john@mail.com']
# ['Mary', 'mary@mail.com']
print('Goal: merge accounts sharing any email into one account')Сопоставление адресов электронной почты с целочисленными ID
DSU работает с целочисленными индексами, но наши вершины представлены строками с адресами электронной почты. Поэтому нужно сопоставить каждый уникальный адрес электронной почты с целочисленным ID. Также необходимо запомнить, какое имя связано с каждым адресом. Используйте словарь email_to_id для назначения последовательных ID и email_to_name для хранения имени учетной записи, связанного с каждым адресом.
Каждый уникальный адрес электронной почты получает один ID. Если один и тот же адрес встречается в нескольких учетных записях, ему соответствует один и тот же ID, а выполнение union для ID адресов внутри одной учетной записи соединяет их в одну компоненту. Имя, связанное с ID корневого адреса, становится именем объединённой учетной записи.
accounts = [
['John', 'john@mail.com', 'john1@mail.com'],
['John', 'john2@mail.com'],
['Mary', 'mary@mail.com'],
['John', 'john1@mail.com', 'john2@mail.com'],
]
email_to_id = {}
email_to_name = {}
next_id = [0]
for account in accounts:
name = account[0]
for email in account[1:]:
if email not in email_to_id:
email_to_id[email] = next_id[0]
next_id[0] += 1
email_to_name[email] = name
print('Total unique emails:', len(email_to_id))
for email, eid in email_to_id.items():
print(f' {email} => id {eid} (owner: {email_to_name[email]})')Объединение адресов внутри каждой учетной записи
Для каждой учетной записи мы выполняем union для ID всех перечисленных в ней адресов. Первый адрес в учетной записи выбирается представителем, а ID каждого другого адреса объединяется с ним с помощью union. Так все адреса учетной записи оказываются в одной компоненте.
После обработки всех учетных записей адреса, встречавшиеся вместе напрямую или транзитивно через общие адреса в разных учетных записях, имеют один и тот же корень DSU. Это ключевой шаг, распространяющий связность между несколькими учетными записями.
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):
self.parent[self.find(x)] = self.find(y)
# After building email_to_id (from previous step)
# email_to_id = {'john@mail.com':0, 'john1@mail.com':1,
# 'john2@mail.com':2, 'mary@mail.com':3}
dsu = DSU(5) # 4 unique emails
# For account ['John', 'john@mail.com', 'john1@mail.com']:
dsu.union(0, 1) # john@ and john1@ share account => same component
# For account ['John', 'john1@mail.com', 'john2@mail.com']:
dsu.union(1, 2) # john1@ and john2@ share account => same component
# Now 0,1,2 all share a root; 3 (mary) is separate
print('find(0)==find(2)?', dsu.find(0) == dsu.find(2)) # True
print('find(0)==find(3)?', dsu.find(0) == dsu.find(3)) # FalseСбор адресов по компонентам
После завершения всех операций union переберите каждый адрес электронной почты, найдите его корень DSU и сгруппируйте адреса по этому корню с помощью словаря списков. ID корня становится ключом. Наконец, для каждой группы получите имя учетной записи, выполните sort для списка адресов и добавьте имя в начало.
Сортировка адресов требуется условием задачи: внутри объединённой учетной записи адреса должны располагаться в лексикографическом порядке. Имя можно получить из любого адреса в группе, поскольку все адреса одной компоненты принадлежат одному человеку.
from collections import defaultdict
# After DSU unions, group by root
def collect_components(email_to_id, email_to_name, dsu):
root_to_emails = defaultdict(list)
for email, eid in email_to_id.items():
root = dsu.find(eid)
root_to_emails[root].append(email)
result = []
for root, emails in root_to_emails.items():
# Find the name from any email in this group
name = email_to_name[emails[0]]
result.append([name] + sorted(emails))
return result
# Mock data for illustration
email_to_id = {'john@m.com':0,'john1@m.com':1,'john2@m.com':2,'mary@m.com':3}
email_to_name = {e:'John' for e in list(email_to_id)[:3]}
email_to_name['mary@m.com'] = 'Mary'
class DSU:
def __init__(self,n): self.p=list(range(n))
def find(self,x): self.p[x]=self.p[self.p[x]] if self.p[x]!=x else x; return self.p[x] if self.p[x]==x else self.find(self.p[x])
def union(self,x,y): self.p[self.find(x)]=self.find(y)
dsu=DSU(4); dsu.union(0,1); dsu.union(1,2)
for row in collect_components(email_to_id, email_to_name, dsu):
print(row)Полное решение задачи об объединении учетных записей
Ниже приведено полное решение, объединяющее все три шага: построение сопоставления адресов с ID, выполнение union для адресов внутри каждой учетной записи и сбор сгруппированных адресов по корню DSU. Общая временная сложность составляет O(n × m × alpha(n × m)), где n — количество учетных записей, а m — максимальное количество адресов в одной учетной записи; фактически это O(n × m).
Пространственная сложность составляет O(n × m) для словарей адресов и массивов DSU. Это решение корректно выполняет транзитивное объединение: если учетная запись A имеет общий адрес X с учетной записью B, а учетная запись B имеет общий адрес Y с учетной записью C, то A, B и C объединяются в одну группу.
from collections import defaultdict
def accounts_merge(accounts):
email_to_id = {}
email_to_name = {}
eid = 0
for account in accounts:
name = account[0]
for email in account[1:]:
if email not in email_to_id:
email_to_id[email] = eid
eid += 1
email_to_name[email] = name
parent = list(range(eid))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(x, y):
parent[find(x)] = find(y)
for account in accounts:
first_id = email_to_id[account[1]]
for email in account[2:]:
union(first_id, email_to_id[email])
root_to_emails = defaultdict(list)
for email, i in email_to_id.items():
root_to_emails[find(i)].append(email)
return [[email_to_name[emails[0]]] + sorted(emails)
for emails in root_to_emails.values()]
accounts = [['John','a@m.com','b@m.com'],['John','c@m.com'],
['Mary','d@m.com'],['John','b@m.com','c@m.com']]
for row in accounts_merge(accounts):
print(row)Альтернатива BFS/DFS для объединения аккаунтов
Альтернативный подход строит граф соответствий между адресами электронной почты и аккаунтами, в котором адреса являются узлами, а рёбра соединяют адреса, встречающиеся в одном аккаунте. Затем BFS/DFS находит каждую связную компоненту. Хотя этот подход корректен, он требует явно построить граф и запустить BFS из каждого ещё не посещённого адреса — это больше кода, и рассуждать о таком решении сложнее, чем о DSU.
DSU удобнее, потому что структура объединения и поиска естественным образом представляет принадлежность к компонентам, не требуя явного списка смежности. Единственный случай, когда здесь предпочтительнее BFS, — если необходимо восстановить настоящий путь или цепочку общих адресов электронной почты между двумя аккаунтами.
# BFS alternative (for comparison)
from collections import defaultdict, deque
def accounts_merge_bfs(accounts):
email_to_accounts = defaultdict(set)
for i, account in enumerate(accounts):
for email in account[1:]:
email_to_accounts[email].add(i)
visited_accounts = set()
result = []
for i, account in enumerate(accounts):
if i in visited_accounts:
continue
queue = deque([i])
emails_in_group = set()
while queue:
acc_idx = queue.popleft()
if acc_idx in visited_accounts:
continue
visited_accounts.add(acc_idx)
for email in accounts[acc_idx][1:]:
emails_in_group.add(email)
for j in email_to_accounts[email]:
queue.append(j)
result.append([account[0]] + sorted(emails_in_group))
return result
accounts = [['John','a@m.com','b@m.com'],['John','b@m.com','c@m.com'],['Mary','d@m.com']]
for row in accounts_merge_bfs(accounts):
print(row)Обобщение: связные компоненты графа
Шаблон объединения аккаунтов обобщается на любую задачу о связных компонентах с метками: у вас есть набор элементов, некоторые элементы объявлены эквивалентными (связанными), и нужно объединить все транзитивно эквивалентные элементы в группы. Примеры включают задачи кластеризации, группы друзей в социальных сетях и обнаружение дубликатов записей.
Общий алгоритм всегда один и тот же: (1) назначить каждому элементу целочисленный ID, (2) объединить ID объявленных эквивалентными элементов, (3) сгруппировать элементы по их корню DSU. По сути, DSU — это механизм группировки для отношений эквивалентности.
# Generalised grouping template
def group_equivalents(items, equivalences):
item_to_id = {item: i for i, item in enumerate(items)}
n = len(items)
parent = list(range(n))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(x, y):
parent[find(x)] = find(y)
for a, b in equivalences:
if a in item_to_id and b in item_to_id:
union(item_to_id[a], item_to_id[b])
groups = {}
for item in items:
root = find(item_to_id[item])
groups.setdefault(root, []).append(item)
return list(groups.values())
# Example: merging duplicate customer records
customers = ['Alice-NY','Alice-LA','Bob','Alice-TX','Carol']
links = [('Alice-NY','Alice-LA'),('Alice-LA','Alice-TX')]
print(group_equivalents(customers, links))Обработка граничных случаев
Важные граничные случаи при объединении аккаунтов:
- Аккаунты с одним адресом электронной почты: аккаунт только с одним адресом образует собственную компоненту, если только другой аккаунт не использует тот же адрес.
- Одинаковое имя, разные люди: имя «Джон» в двух аккаунтах не означает, что это один и тот же человек — аккаунты объединяются только при наличии общих адресов электронной почты. Имя хранится для каждого адреса, а не для компоненты.
- Пустые аккаунты: аккаунт без адресов электронной почты следует пропустить, чтобы избежать ошибок индексации.
Всегда проверяйте, что ваше решение не объединяет аккаунты только потому, что у них одинаковое имя. Связи DSU создаются исключительно на основе общих адресов электронной почты.
# Edge case: two Johns with no shared email => separate output
accounts = [
['John', 'john_a@m.com'],
['John', 'john_b@m.com'], # different email => different component
['Mary'], # no emails => skip
]
def accounts_merge_safe(accounts):
email_to_id = {}; email_to_name = {}; eid = 0
for account in accounts:
name = account[0]
for email in account[1:]:
if email not in email_to_id:
email_to_id[email] = eid; eid += 1
email_to_name[email] = name
parent = list(range(eid))
def find(x):
while parent[x]!=x: parent[x]=parent[parent[x]]; x=parent[x]
return x
def union(x,y): parent[find(x)]=find(y)
for account in accounts:
if len(account) < 2: continue # skip no-email accounts
first = email_to_id[account[1]]
for email in account[2:]:
union(first, email_to_id[email])
from collections import defaultdict
groups = defaultdict(list)
for email, i in email_to_id.items():
groups[find(i)].append(email)
return [[email_to_name[e[0]]] + sorted(e) for e in groups.values()]
for row in accounts_merge_safe(accounts):
print(row)Количество связных компонент в графе
В связанной задаче (LeetCode 323) требуется найти количество связных компонент в неориентированном графе. Она проще объединения аккаунтов: инициализируйте DSU с n узлами, обработайте все рёбра с помощью union, а затем подсчитайте различные корни.
Самый краткий способ подсчитать компоненты — поддерживать переменную count, начиная со значения n, и уменьшать её каждый раз, когда успешный union объединяет две разные компоненты. Другой вариант — в конце подсчитать количество узлов i, для которых find(i) == i.
def count_components(n, edges):
parent = list(range(n))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
count = n
for u, v in edges:
pu, pv = find(u), find(v)
if pu != pv:
parent[pu] = pv
count -= 1
return count
print(count_components(5, [[0,1],[1,2],[3,4]])) # 2: {0,1,2} and {3,4}
print(count_components(5, [[0,1],[1,2],[2,3],[3,4]])) # 1: all connected
print(count_components(5, [])) # 5: no edges, all isolatedНаименьшая и наибольшая компоненты
Если DSU отслеживает размеры, с его помощью можно за O(n) отвечать на запросы вроде «каков размер наибольшей связной компоненты?» или «сколько компонент содержат ровно 3 узла?», просматривая массив размеров в узлах-корнях.
Такие запросы встречаются в задачах вроде «найти наибольший связный остров» на сетке или «определить наименьший фрагмент сети». После завершения всех объединений просмотрите узлы i, для которых find(i) == i (это корни), и изучите их размеры.
class DSU:
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
self.parent[py] = px
self.size[px] += self.size[py]
def component_stats(n, edges):
dsu = DSU(n)
for u, v in edges:
dsu.union(u, v)
sizes = [dsu.size[i] for i in range(n) if dsu.find(i) == i]
print('Component sizes:', sizes)
print('Largest component:', max(sizes))
print('Smallest component:', min(sizes))
print('Number of components:', len(sizes))
component_stats(8, [(0,1),(1,2),(3,4),(5,6),(6,7)])Советы для собеседования по задачам на DSU
Если вы встречаете задачу с объединением групп, запросами связности или поиском лишнего ребра, сразу думайте о DSU. На собеседовании упомяните обе оптимизации (сжатие путей и объединение по рангу/размеру), чтобы продемонстрировать глубину знаний, даже если более простой наивный DSU прошёл бы при заданных ограничениях.
Распространённые ошибки, которых следует избегать: не обработать случай, когда обе конечные вершины уже связаны (объединение не выполняет никаких действий), неправильно использовать нумерацию с 0 вместо нумерации с 1 или наоборот, а также не отсортировать результат для объединения аккаунтов (задача требует отсортированных списков адресов электронной почты). Всегда уточняйте ограничения на входные данные перед написанием кода.
# Interview checklist for DSU problems
checklist = [
'1. Identify: is this a grouping/connectivity/cycle problem?',
'2. Map problem entities to integer node IDs if needed',
'3. Implement DSU with path compression + union by rank/size',
'4. Process all relationships (edges/pairs) with union()',
'5. Answer queries using find() and size/count tracking',
'6. Handle edge cases: already connected, single nodes, no edges',
'7. Check output format: sorted? 1-indexed? Name included?',
'8. State time complexity: O(n * alpha(n)) ~ O(n)',
]
for item in checklist:
print(item)Быстрая проверка
Проверьте своё понимание концепций курса «Структуры данных и алгоритмы — подготовка к собеседованию по программированию», изученных в этом уроке.
Итоги урока
В этом уроке вы узнали, что объединение аккаунтов — это задача о связных компонентах, где адреса электронной почты являются узлами, а аккаунты связывают адреса, DSU решает её, сопоставляя адресам целочисленные ID, объединяя ID внутри каждого аккаунта и группируя их по корню, а тот же шаблон группировки DSU применим к любой задаче о классах эквивалентности или кластеризации. Далее мы перейдём к работе с битами, начав с фундаментальных операторов AND, OR, XOR, NOT и операторов сдвига.
Часто задаваемые вопросы
Урок «Объединение аккаунтов и компоненты связности» бесплатный?
Да — полный текст урока «Объединение аккаунтов и компоненты связности» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DSA Interview Prep, подпишись на CoddyKit PRO. Курс DSA Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Объединение аккаунтов и компоненты связности»?
Сгруппируйте аккаунты с общим адресом электронной почты, рассматривая адреса как узлы DSU, а затем соберите все адреса каждой компоненты для восстановления объединённых аккаунтов Ты практикуешь DSA Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать DSA Interview Prep?
Предыдущий опыт не требуется. DSA Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Объединение аккаунтов и компоненты связности»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке DSA Interview Prep?
Да. Каждый урок DSA Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- DSU со сжатием путей
- Объединение по рангу и оценка обратной функции Аккермана
- Избыточное ребро и обнаружение циклов
- Объединение аккаунтов и компоненты связности