Fusión de cuentas y componentes conexas
Agrupe las cuentas que comparten un correo electrónico tratando los correos como nodos de DSU; después, reúna todos los correos de cada componente para reconstruir las cuentas fusionadas.
Fusión de cuentas y componentes conexas es una lección gratuita de DSA Interview Prep en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de DSA Interview Prep, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de DSA Interview Prep incluye 4 lecciones en total.
Problema: combinación de cuentas
El problema de combinación de cuentas (LeetCode 721) proporciona una lista de cuentas, cada una de las cuales es una lista de cadenas: el primer elemento es el nombre de la cuenta y el resto son direcciones de correo electrónico. Dos cuentas pertenecen a la misma persona si comparten al menos un correo electrónico. Combine todas las cuentas que pertenecen a la misma persona y devuelva las listas de correos ordenadas.
Fundamentalmente, se trata de un problema de componentes conexas en el que los correos electrónicos son los nodos y una cuenta compartida los conecta. DSU es la herramienta ideal: una todos los correos de una misma cuenta y, después, recopile los correos de cada 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')Asignación de correos electrónicos a identificadores enteros
DSU funciona con índices enteros, pero nuestros nodos son cadenas que representan correos electrónicos. Necesitamos asignar cada correo electrónico único a un identificador entero. También debemos recordar qué nombre es el propietario de cada correo. Utilice un diccionario email_to_id para asignar identificadores incrementales y email_to_name para registrar el nombre de la cuenta asociado a cada correo.
Cada correo electrónico único recibe un identificador. Si el mismo correo aparece en varias cuentas, se asigna al mismo identificador; al unir los identificadores de los correos que pertenecen a una cuenta, estos quedan conectados en una sola componente. El nombre asociado al identificador del correo raíz es el nombre de la cuenta combinada.
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]})')Unión de los correos dentro de cada cuenta
Para cada cuenta, unimos los identificadores de todos los correos que aparecen juntos. Elegimos el primer correo electrónico de la cuenta como representante y unimos a él el identificador de cada uno de los demás correos. De este modo, todos los correos de la cuenta quedan vinculados en una sola componente.
Después de procesar todas las cuentas, los correos que aparecieron juntos (directamente o de forma transitiva mediante correos compartidos entre cuentas) comparten la misma raíz de DSU. Este es el paso clave que propaga la conectividad entre varias cuentas.
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)) # FalseRecopilación de correos por componente
Cuando se han realizado todas las uniones, recorremos cada correo electrónico, buscamos su raíz de DSU y agrupamos los correos por esa raíz mediante un diccionario de listas. El identificador de la raíz se convierte en la clave. Por último, para cada grupo, recuperamos el nombre de la cuenta, ordenamos la lista de correos y anteponemos el nombre.
El problema exige ordenar los correos: dentro de una cuenta combinada, deben estar en orden lexicográfico. El nombre se puede recuperar de cualquier correo del grupo, ya que todos los correos de una componente pertenecen a la misma persona.
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)Solución completa para la combinación de cuentas
A continuación se muestra la solución completa, que combina los tres pasos: crear la correspondencia entre correos e identificadores, unir los correos de cada cuenta y recopilar los correos agrupados por raíz de DSU. La complejidad temporal total es O(n × m × alpha(n × m)), donde n es el número de cuentas y m es el número máximo de correos por cuenta; en la práctica, es O(n × m).
La complejidad espacial es O(n × m) para los mapas de correos y los arrays de DSU. Esta solución gestiona correctamente las combinaciones transitivas: si la cuenta A comparte el correo X con la cuenta B, y la cuenta B comparte el correo Y con la cuenta C, entonces A, B y C se combinan en un solo 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 con BFS/DFS para accounts-merge
Un enfoque alternativo construye un grafo de cuentas y correos electrónicos en el que los correos son nodos y las aristas conectan los correos que aparecen en la misma cuenta. Después, BFS/DFS encuentra cada componente conexa. Aunque es correcto, este enfoque requiere construir explícitamente el grafo y ejecutar BFS desde cada correo electrónico no visitado, lo que implica más código y resulta más difícil de razonar que DSU.
DSU es más limpio porque la estructura union-find representa de forma natural la pertenencia a una componente sin necesitar una lista de adyacencia explícita. El único caso en el que BFS es preferible aquí es si necesita reconstruir la ruta o cadena real de correos electrónicos compartidos entre dos cuentas.
# 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)Generalización: componentes conexas de un grafo
El patrón de accounts-merge se generaliza a cualquier problema de componentes conexas con etiquetas: tiene un conjunto de elementos, algunos están declarados como equivalentes (conectados), y quiere agrupar todos los elementos transitivamente equivalentes. Algunos ejemplos son los problemas de clustering, los grupos de amigos en redes sociales y la detección de registros duplicados.
El algoritmo general siempre es: (1) asignar un ID entero a cada elemento, (2) unir mediante union los ID de los elementos declarados equivalentes y (3) agrupar los elementos según su raíz en DSU. DSU es, en esencia, un motor de agrupación para relaciones de equivalencia.
# 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))Gestión de casos límite
Casos límite importantes en accounts-merge:
- Cuentas con un solo correo electrónico: una cuenta con un único correo forma su propia componente, a menos que otra cuenta comparta ese correo.
- Mismo nombre, personas diferentes: que 'John' aparezca en dos cuentas no significa que se trate de la misma persona; solo los correos electrónicos compartidos fusionan las cuentas. El nombre se almacena por correo electrónico, no por componente.
- Cuentas vacías: una cuenta sin correos electrónicos debe omitirse para evitar errores de índice.
Verifique siempre que su solución gestione correctamente las cuentas que no deben fusionarse solo porque comparten un nombre. Las conexiones de DSU dependen exclusivamente de los correos electrónicos compartidos.
# 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 conexas en un grafo
Un problema relacionado (LeetCode 323) pide calcular el número de componentes conexas de un grafo no dirigido. Es más sencillo que accounts-merge: inicialice DSU con n nodos, procese todas las aristas haciendo union y, después, cuente las raíces distintas.
La forma más concisa de contar las componentes es mantener una variable count que comience en n y decrementarla cada vez que una operación union exitosa fusione dos componentes diferentes. Como alternativa, al final puede contar el número de nodos i para los que 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 isolatedComponente más pequeña y componente más grande
Una vez que tenga DSU con seguimiento de tamaños, podrá responder consultas como «¿cuál es el tamaño de la componente conexa más grande?» o «¿cuántas componentes tienen exactamente 3 nodos?» en O(n), recorriendo el arreglo de tamaños en los nodos raíz.
Estas consultas aparecen en problemas como «encontrar la isla conectada más grande» en una cuadrícula o «identificar la partición de red más pequeña». Después de completar todas las operaciones union, recorra los nodos i para los que find(i) == i (son las raíces) y examine sus tamaños.
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)])Consejos para entrevistas sobre problemas de DSU
Cuando se encuentre con un problema que implique fusionar grupos, consultas de conectividad o encontrar la arista adicional, piense inmediatamente en DSU. Durante las entrevistas, mencione ambas optimizaciones (compresión de rutas + union por rango/tamaño) para demostrar un conocimiento profundo, aunque un DSU ingenuo más sencillo pudiera superar las restricciones dadas.
Errores frecuentes que debe evitar: olvidar gestionar el caso en el que ambos extremos ya están conectados (union no hace nada), usar índices desde 0 en lugar de desde 1, o no ordenar la salida de accounts-merge (el problema exige listas de correos electrónicos ordenadas). Aclare siempre las restricciones de entrada antes de empezar a programar.
# 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)Comprobación rápida
Ponga a prueba su comprensión de los conceptos de Data Structures & Algorithms — Coding Interview Prep de esta lección.
Resumen de la lección
En esta lección ha aprendido que: accounts-merge es un problema de componentes conexas en el que los correos electrónicos son nodos y las cuentas conectan los correos, DSU lo resuelve asignando ID enteros a los correos electrónicos, uniendo los ID dentro de cada cuenta y agrupando por raíz, y la misma plantilla de agrupación con DSU se aplica a cualquier problema de clases de equivalencia o clustering. A continuación cambiaremos de tema para estudiar la manipulación de bits, empezando por los operadores fundamentales AND, OR, XOR, NOT y desplazamiento.
Preguntas frecuentes
¿La lección «Fusión de cuentas y componentes conexas» es gratis?
Sí — el texto completo de «Fusión de cuentas y componentes conexas» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de DSA Interview Prep, actualiza a CoddyKit PRO. El curso de DSA Interview Prep incluye 4 lecciones en total.
¿Qué aprenderé en «Fusión de cuentas y componentes conexas»?
Agrupe las cuentas que comparten un correo electrónico tratando los correos como nodos de DSU; después, reúna todos los correos de cada componente para reconstruir las cuentas fusionadas. Practicas DSA Interview Prep con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar DSA Interview Prep?
No se requiere experiencia previa. DSA Interview Prep en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.
¿Cuánto tiempo toma la lección «Fusión de cuentas y componentes conexas»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de DSA Interview Prep?
Sí. Cada lección de DSA Interview Prep incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- DSU con compresión de caminos
- Unión por rango y cota de Ackermann inversa
- Conexión redundante y detección de ciclos
- Fusión de cuentas y componentes conexas