Scalanie kont i spójne składowe
Grupować konta współdzielące adres e-mail, traktując adresy e-mail jako wierzchołki DSU, a następnie zbierać wszystkie adresy z każdej składowej, aby odtworzyć scalone konta
Scalanie kont i spójne składowe to bezpłatna lekcja DSA Interview Prep na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej DSA Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.
Problem: Accounts Merge
Problem Accounts Merge (LeetCode 721) daje Państwu listę kont, z których każde jest listą ciągów znaków: pierwszy element to nazwa konta, a pozostałe to adresy e-mail. Dwa konta należą do tej samej osoby, jeśli mają co najmniej jeden wspólny adres e-mail. Należy scalić wszystkie konta należące do tej samej osoby i zwrócić posortowane listy adresów e-mail.
Jest to w istocie problem spójnych składowych, w którym adresy e-mail są węzłami, a wspólne konto je łączy. DSU jest idealnym narzędziem: należy połączyć wszystkie adresy e-mail należące do tego samego konta, a następnie zebrać adresy z każdej składowej.
# 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')Mapowanie adresów e-mail na identyfikatory całkowite
DSU działa na indeksach całkowitych, ale naszymi węzłami są ciągi znaków zawierające adresy e-mail. Musimy zmapować każdy unikalny adres e-mail na identyfikator całkowity. Musimy także zapamiętać, do kogo należy dany adres. Służy do tego słownik email_to_id, który przypisuje kolejne identyfikatory, oraz email_to_name, który przechowuje nazwę konta powiązaną z danym adresem e-mail.
Każdy unikalny adres e-mail otrzymuje jeden identyfikator. Jeśli ten sam adres pojawia się na wielu kontach, jest mapowany na ten sam identyfikator — połączenie identyfikatorów adresów należących do jednego konta łączy je w jedną składową. Nazwę powiązaną z identyfikatorem adresu e-mail znajdującego się w korzeniu można wykorzystać jako nazwę scalonego konta.
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]})')Łączenie adresów e-mail w obrębie każdego konta
Dla każdego konta wykonujemy union identyfikatorów wszystkich wymienionych adresów e-mail. Wybieramy pierwszy adres e-mail na koncie jako reprezentanta i wykonujemy operację union, łącząc z nim identyfikator każdego pozostałego adresu. W ten sposób wszystkie adresy e-mail z konta trafiają do jednej składowej.
Po przetworzeniu wszystkich kont adresy e-mail, które występowały razem — bezpośrednio lub przechodnio przez wspólne adresy w różnych kontach — mają ten sam korzeń DSU. To kluczowy etap, który propaguje spójność między wieloma kontami.
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)) # FalseZbieranie adresów e-mail według składowych
Po zakończeniu wszystkich operacji union przechodzimy po każdym adresie e-mail, znajdujemy jego korzeń DSU i grupujemy adresy według tego korzenia za pomocą słownika list. Identyfikator korzenia staje się kluczem. Następnie dla każdej grupy pobieramy nazwę konta, sortujemy listę adresów e-mail i umieszczamy nazwę na jej początku.
Sortowanie adresów e-mail jest wymagane przez zadanie — w scalonym koncie muszą one występować w porządku leksykograficznym. Nazwę można pobrać z dowolnego adresu w grupie, ponieważ wszystkie adresy w jednej składowej należą do tej samej osoby.
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)Kompletne rozwiązanie problemu Accounts Merge
Oto kompletne rozwiązanie łączące wszystkie trzy kroki: zbudowanie mapowania adresów e-mail na identyfikatory, wykonanie union adresów należących do tego samego konta oraz zebranie grup adresów według korzenia DSU. Całkowita złożoność czasowa wynosi O(n × m × alpha(n × m)), gdzie n oznacza liczbę kont, a m — maksymalną liczbę adresów e-mail przypadających na konto; w praktyce jest to O(n × m).
Złożoność pamięciowa wynosi O(n × m) ze względu na mapy adresów e-mail oraz tablice DSU. Rozwiązanie poprawnie obsługuje scalanie przechodnie: jeśli konto A ma wspólny adres X z kontem B, a konto B ma wspólny adres Y z kontem C, wszystkie konta A, B i C zostają scalone w jedną grupę.
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)Alternatywa BFS/DFS dla Accounts Merge
Alternatywne podejście polega na zbudowaniu grafu odwzorowującego adresy e-mail na konta, w którym adresy e-mail są węzłami, a krawędzie łączą adresy występujące na tym samym koncie. Następnie BFS/DFS znajduje każdą spójną składową. Rozwiązanie jest poprawne, ale wymaga jawnego zbudowania grafu i uruchomienia BFS dla każdego nieodwiedzonego adresu e-mail — oznacza to więcej kodu i trudniejsze rozumowanie niż w przypadku DSU.
DSU jest prostsze, ponieważ struktura union-find w naturalny sposób reprezentuje przynależność do składowej bez potrzeby tworzenia jawnej listy sąsiedztwa. BFS jest tutaj lepszym wyborem tylko wtedy, gdy trzeba odtworzyć rzeczywistą ścieżkę lub łańcuch wspólnych adresów e-mail między dwoma kontami.
# 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)Uogólnienie: spójne składowe grafu
Wzorzec accounts-merge można uogólnić na dowolny problem spójnych składowych z etykietami: istnieje zbiór elementów, pewne elementy są zadeklarowane jako równoważne (połączone), a celem jest pogrupowanie wszystkich elementów równoważnych przechodnio. Przykłady obejmują problemy klastrowania, grupy znajomych w sieciach społecznościowych oraz wykrywanie zduplikowanych rekordów.
Ogólny algorytm zawsze wygląda tak: (1) przypisz każdemu elementowi identyfikator całkowity, (2) wykonaj union dla identyfikatorów zadeklarowanych jako równoważne, (3) pogrupuj elementy według korzenia DSU. DSU jest w istocie mechanizmem grupowania dla relacji równoważności.
# 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))Obsługa przypadków brzegowych
Ważne przypadki brzegowe w problemie accounts merge:
- Konta z jednym adresem e-mail: konto zawierające tylko jeden adres e-mail tworzy własną składową, chyba że inne konto zawiera ten sam adres.
- Ta sama nazwa, różne osoby: wystąpienie nazwy 'John' na dwóch kontach nie oznacza, że dotyczą one tej samej osoby — konta są scalane wyłącznie na podstawie wspólnych adresów e-mail. Nazwa jest przechowywana dla każdego adresu e-mail, a nie dla całej składowej.
- Puste konta: konto bez adresów e-mail należy pominąć, aby uniknąć błędów indeksowania.
Zawsze należy sprawdzić, czy rozwiązanie nie scala kont tylko dlatego, że mają tę samą nazwę. Połączenia DSU wynikają wyłącznie ze wspólnych adresów e-mail.
# 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)Liczba spójnych składowych grafu
Pokrewny problem (LeetCode 323) polega na znalezieniu liczby spójnych składowych w grafie nieskierowanym. Jest on prostszy niż accounts-merge: należy zainicjalizować DSU z n węzłami, przetworzyć wszystkie krawędzie za pomocą union, a następnie policzyć różne korzenie.
Najbardziej zwięzłym sposobem liczenia składowych jest utrzymywanie zmiennej count z wartością początkową n i zmniejszanie jej za każdym razem, gdy udane union scala dwie różne składowe. Alternatywnie na końcu można policzyć liczbę węzłów i, dla których zachodzi 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 isolatedNajmniejsza i największa składowa
Gdy DSU śledzi rozmiary składowych, można odpowiadać na pytania takie jak „jaki jest rozmiar największej spójnej składowej?” lub „ile składowych ma dokładnie 3 węzły?” w czasie O(n), przeglądając tablicę rozmiarów dla węzłów będących korzeniami.
Takie zapytania pojawiają się w zadaniach takich jak „znalezienie największej połączonej wyspy” na siatce lub „identyfikacja najmniejszej partycji sieci”. Po zakończeniu wszystkich operacji union należy znaleźć węzły i, dla których zachodzi find(i) == i (czyli korzenie), i sprawdzić ich rozmiary.
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)])Wskazówki dotyczące zadań z DSU
Gdy napotkają Państwo problem dotyczący scalania grup, zapytań o spójność lub znalezienia dodatkowej krawędzi, należy od razu pomyśleć o DSU. Podczas rozmów rekrutacyjnych warto wspomnieć o obu optymalizacjach (kompresji ścieżek oraz łączeniu według rangi/rozmiaru), aby wykazać się znajomością tematu, nawet jeśli prostsza, naiwna wersja DSU wystarczyłaby przy danych ograniczeniach.
Typowe błędy, których należy unikać, to: nieuwzględnienie przypadku, w którym oba końce są już połączone (union nic wtedy nie robi), niepoprawne użycie indeksowania od 0 zamiast od 1 lub odwrotnie oraz niesortowanie wyniku dla accounts-merge (problem wymaga posortowanych list adresów e-mail). Przed rozpoczęciem kodowania należy zawsze doprecyzować ograniczenia danych wejściowych.
# 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)Szybki test
Sprawdź swoją znajomość zagadnień Data Structures & Algorithms — Coding Interview Prep z tej lekcji.
Podsumowanie lekcji
W tej lekcji poznali Państwo następujące zagadnienia: accounts-merge to problem spójnych składowych, w którym adresy e-mail są węzłami, a konta łączą adresy e-mail, DSU rozwiązuje go przez przypisanie adresom e-mail identyfikatorów całkowitych, wykonanie union dla identyfikatorów w obrębie każdego konta oraz pogrupowanie elementów według korzenia, a także ten sam wzorzec grupowania DSU można stosować do dowolnego problemu klas równoważności lub klastrowania. W następnej części zmienimy temat na operacje bitowe, zaczynając od podstawowych operatorów AND, OR, XOR, NOT oraz operatorów przesunięcia.
Ucz się Python dzięki korepetycjom AI — za darmo
Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.
- Kursy
- 30
- Lekcje
- 120
Często zadawane pytania
Czy lekcja „Scalanie kont i spójne składowe” jest bezpłatna?
Tak — pełny tekst „Scalanie kont i spójne składowe” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu DSA Interview Prep, przejdź na CoddyKit PRO. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.
Co nauczysz się w „Scalanie kont i spójne składowe”?
Grupować konta współdzielące adres e-mail, traktując adresy e-mail jako wierzchołki DSU, a następnie zbierać wszystkie adresy z każdej składowej, aby odtworzyć scalone konta Ćwiczysz DSA Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć DSA Interview Prep?
Nie wymagamy żadnego doświadczenia. DSA Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.
Ile czasu zajmuje lekcja „Scalanie kont i spójne składowe”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji DSA Interview Prep?
Tak. Każda lekcja DSA Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- DSU z kompresją ścieżek
- Łączenie według rangi i ograniczenie odwrotną funkcją Ackermanna
- Nadmiarowa krawędź i wykrywanie cykli
- Scalanie kont i spójne składowe