Fusão de contas e componentes conexos
Agrupe contas que compartilham um e-mail tratando os e-mails como nós de DSU; depois, reúna todos os e-mails de cada componente para reconstruir as contas fundidas.
Fusão de contas e componentes conexos é uma aula grátis de DSA Interview Prep no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de DSA Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de DSA Interview Prep inclui 4 aulas no total.
Problema: mesclagem de contas
O problema de mesclagem de contas (LeetCode 721) fornece uma lista de contas, cada uma sendo uma lista de strings em que o primeiro elemento é o nome da conta e os demais são endereços de e-mail. Duas contas pertencem à mesma pessoa se compartilharem pelo menos um e-mail. Mescle todas as contas pertencentes à mesma pessoa e retorne listas de e-mails ordenadas.
Fundamentalmente, este é um problema de componentes conexos, no qual os e-mails são nós e uma conta compartilhada os conecta. DSU é a ferramenta ideal: faça union de todos os e-mails dentro da mesma conta e depois reúna os e-mails por componente.
# 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')Mapeando e-mails para IDs inteiros
DSU funciona com índices inteiros, mas nossos nós são strings de e-mail. Precisamos mapear cada e-mail exclusivo para um ID inteiro. Também precisamos lembrar qual nome é associado a cada e-mail. Use um dicionário email_to_id para atribuir IDs incrementais e email_to_name para acompanhar o nome da conta associado a cada e-mail.
Cada e-mail exclusivo recebe um ID. Se o mesmo e-mail aparecer em várias contas, ele será mapeado para o mesmo ID — e fazer union dos IDs dos e-mails dentro de uma conta os conecta em um único componente. O nome associado ao ID do e-mail raiz será o nome da conta mesclada.
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]})')Fazendo union dos e-mails dentro de cada conta
Para cada conta, fazemos union dos IDs de todos os e-mails listados juntos. Escolhemos o primeiro e-mail da conta como representante e fazemos union do ID de cada outro e-mail com ele. Isso conecta todos os e-mails da conta em um único componente.
Depois de processar todas as contas, os e-mails que apareceram juntos (diretamente ou de forma transitiva por meio de e-mails compartilhados entre contas) terão a mesma raiz de DSU. Esta é a etapa fundamental que propaga a conectividade entre várias contas.
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)) # FalseReunindo e-mails por componente
Depois que todas as operações union forem concluídas, percorremos cada e-mail, executamos find para obter sua raiz de DSU e agrupamos os e-mails por essa raiz usando um dicionário de listas. O ID da raiz se torna a chave. Por fim, para cada grupo, recuperamos o nome da conta, usamos sort na lista de e-mails e adicionamos o nome no início.
A ordenação dos e-mails é exigida pelo problema — dentro de uma conta mesclada, os e-mails devem estar em ordem lexicográfica. O nome pode ser recuperado de qualquer e-mail do grupo (todos os e-mails de um componente pertencem à mesma pessoa).
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)Solução completa para mesclagem de contas
Aqui está a solução completa que combina as três etapas: criar o mapeamento de e-mail para ID, fazer union dos e-mails dentro de cada conta e reunir os e-mails agrupados pela raiz de DSU. A complexidade de tempo geral é O(n × m × alpha(n × m)), em que n é o número de contas e m é o número máximo de e-mails por conta, o que é efetivamente O(n × m).
A complexidade de espaço é O(n × m) para os mapas de e-mails e os vetores de DSU. Esta solução trata corretamente a mesclagem transitiva: se a conta A compartilhar o e-mail X com a conta B, e a conta B compartilhar o e-mail Y com a conta C, então A, B e C serão mescladas em um único grupo.
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)Alternativa com BFS/DFS para mesclagem de contas
Uma abordagem alternativa constrói um grafo de e-mails para contas, no qual os e-mails são nós e as arestas conectam e-mails que aparecem na mesma conta. Em seguida, BFS/DFS encontra cada componente conexo. Embora esteja correta, essa abordagem exige construir o grafo explicitamente e executar BFS a partir de cada e-mail ainda não visitado — mais código e mais dificuldade para raciocinar sobre a solução do que com DSU.
DSU é mais simples porque a estrutura de união e busca representa naturalmente a associação aos componentes sem precisar de uma lista de adjacência explícita. A única situação em que BFS é preferível aqui é quando você precisa reconstruir o caminho ou a cadeia real de e-mails compartilhados entre duas contas.
# 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)Generalização: componentes conexos de um grafo
O padrão de mesclagem de contas pode ser generalizado para qualquer problema de componentes-conexos-com-rótulos: você tem um conjunto de itens, alguns itens são declarados equivalentes (conectados) e deseja agrupar todos os itens transitivamente equivalentes. Entre os exemplos estão problemas de agrupamento, grupos de amigos em redes sociais e detecção de registros duplicados.
O algoritmo geral é sempre: (1) atribuir um ID inteiro a cada item, (2) fazer union dos IDs de itens declarados equivalentes, (3) agrupar os itens pela raiz do DSU. DSU é essencialmente um mecanismo de agrupamento para relações de equivalência.
# 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))Tratamento de casos extremos
Casos extremos importantes na mesclagem de contas:
- Contas com um único e-mail: uma conta com apenas um e-mail forma seu próprio componente, a menos que outra conta compartilhe esse e-mail.
- Mesmo nome, pessoas diferentes: o fato de 'John' aparecer em duas contas não significa que sejam a mesma pessoa — somente e-mails compartilhados mesclam contas. O nome é armazenado por e-mail, não por componente.
- Contas vazias: uma conta sem e-mails deve ser ignorada para evitar erros de índice.
Verifique sempre se sua solução trata contas que não devem ser mescladas apenas por compartilharem um nome. As conexões do DSU são determinadas exclusivamente pelos endereços de e-mail compartilhados.
# 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)Número de componentes conexos em um grafo
Um problema relacionado (LeetCode 323) pede o número de componentes conexos em um grafo não direcionado. Ele é mais simples do que a mesclagem de contas: inicialize o DSU com n nós, processe todas as arestas com union e, em seguida, conte as raízes distintas.
A maneira mais concisa de contar os componentes é manter uma variável count começando em n e decrementá-la sempre que uma union bem-sucedida mesclar dois componentes diferentes. Como alternativa, conte o número de nós i para os quais find(i) == i ao final.
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 isolatedMenor componente e maior componente
Depois de ter um DSU com acompanhamento de tamanhos, você pode responder a consultas como 'qual é o tamanho do maior componente conexo?' ou 'quantos componentes têm exatamente 3 nós?' em O(n), percorrendo o vetor de tamanhos nos nós raiz.
Essas consultas aparecem em problemas como 'encontrar a maior ilha conectada' em uma grade ou 'identificar a menor partição de uma rede'. Depois que todas as unions forem concluídas, percorra os nós i para os quais find(i) == i (essas são as raízes) e examine seus tamanhos.
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)])Dicas para entrevistas sobre DSU
Ao encontrar um problema envolvendo mesclagem de grupos, consultas de conectividade ou encontrar a aresta extra, pense imediatamente em DSU. Durante as entrevistas, mencione as duas otimizações (compressão de caminhos + union por rank/tamanho) para demonstrar profundidade de conhecimento, mesmo que um DSU ingênuo mais simples fosse aprovado com as restrições fornecidas.
Erros comuns a evitar: esquecer de tratar o caso em que os dois extremos já estão conectados (union não faz nada), usar índices começando em 0 ou em 1 de maneira incorreta e não ordenar a saída da mesclagem de contas (o problema exige listas de e-mails ordenadas). Esclareça sempre as restrições da entrada antes de escrever o código.
# 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)Verificação rápida
Verifique sua compreensão dos conceitos de Estruturas de Dados e Algoritmos — Preparação para Entrevistas de Programação apresentados nesta lição.
Recapitulação da lição
Nesta lição, você aprendeu que: mesclagem de contas é um problema de componentes conexos no qual os e-mails são nós e as contas conectam e-mails, DSU resolve o problema mapeando e-mails para IDs inteiros, fazendo union dos IDs dentro de cada conta e agrupando pela raiz e o mesmo modelo de agrupamento com DSU se aplica a qualquer problema de classes de equivalência ou agrupamento. A seguir, mudaremos de assunto para a manipulação de bits, começando pelos operadores fundamentais AND, OR, XOR, NOT e de deslocamento.
Perguntas Frequentes
A aula “Fusão de contas e componentes conexos” é grátis?
Sim — o texto completo de “Fusão de contas e componentes conexos” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de DSA Interview Prep, atualize para CoddyKit PRO. O curso de DSA Interview Prep inclui 4 aulas no total.
O que vou aprender em “Fusão de contas e componentes conexos”?
Agrupe contas que compartilham um e-mail tratando os e-mails como nós de DSU; depois, reúna todos os e-mails de cada componente para reconstruir as contas fundidas. Você pratica DSA Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar DSA Interview Prep?
Nenhuma experiência prévia é necessária. DSA Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.
Quanto tempo leva a aula “Fusão de contas e componentes conexos”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de DSA Interview Prep?
Sim. Cada aula de DSA Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- DSU com compressão de caminhos
- União por classificação e limite do inverso de Ackermann
- Conexão redundante e detecção de ciclos
- Fusão de contas e componentes conexos