Accounts Merge ja yhtenäiset komponentit
Ryhmitelkää saman sähköpostin jakavat tilit käsittelemällä sähköposteja DSU-solmuina ja kootkaa sitten kunkin komponentin sähköpostit yhdistettyjen tilien muodostamiseksi.
Accounts Merge ja yhtenäiset komponentit on ilmainen DSA Interview Prep-oppitunti CoddyKitissä. Tämä on oppitunti 4/4. Voit lukea tästä oppimispolusta kokonaan mitkä tahansa 3 oppituntia ilmaiseksi — sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä käytännön harjoittelun sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Oppitunti kuuluu DSA Interview Prep-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. DSA Interview Prep-kurssilla on yhteensä 4 oppituntia.
Ongelma: Accounts Merge
Accounts Merge -ongelmassa (LeetCode 721) annetaan luettelo tileistä. Jokainen tili on merkkijonoluettelo, jonka ensimmäinen alkio on tilin nimi ja loput sähköpostiosoitteita. Kaksi tiliä kuuluu samalle henkilölle, jos niillä on vähintään yksi yhteinen sähköpostiosoite. Yhdistäkää kaikki samalle henkilölle kuuluvat tilit ja palauttakaa sähköpostilistat lajiteltuina.
Tämä on pohjimmiltaan yhtenäisten komponenttien ongelma, jossa sähköpostiosoitteet ovat solmuja ja yhteinen tili yhdistää ne. DSU on ihanteellinen työkalu: yhdistäkää kaikki saman tilin sähköpostiosoitteet ja kerätkää sitten sähköpostiosoitteet komponenttien mukaan.
# 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')Sähköpostiosoitteiden yhdistäminen kokonaislukutunnisteisiin
DSU toimii kokonaislukuihin perustuvilla indekseillä, mutta solmumme ovat sähköpostiosoitteita sisältäviä merkkijonoja. Meidän on yhdistettävä jokainen yksilöllinen sähköpostiosoite kokonaislukutunnisteeseen. Meidän on myös muistettava, mikä nimi kuuluu kuhunkin sähköpostiosoitteeseen. Antakaa sanakirjan email_to_id avulla kasvavat tunnisteet ja käyttäkää sanakirjaa email_to_name seuraamaan kuhunkin sähköpostiosoitteeseen liitettyä tilin nimeä.
Jokainen yksilöllinen sähköpostiosoite saa yhden tunnisteen. Jos sama sähköpostiosoite esiintyy useilla tileillä, se yhdistetään samaan tunnisteeseen — ja yhden tilin sähköpostiosoitteiden tunnisteiden yhdistäminen liittää ne samaan komponenttiin. Juurisähköpostin tunnisteeseen liitetty nimi on yhdistetyn tilin nimi.
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]})')Sähköpostiosoitteiden yhdistäminen kunkin tilin sisällä
Yhdistämme jokaisen tilin kaikki yhdessä luetellut sähköpostiosoitteet tunnisteiden avulla. Valitsemme tilin ensimmäisen sähköpostiosoitteen edustajaksi ja yhdistämme siihen kaikkien muiden sähköpostiosoitteiden tunnisteet. Näin kaikki tilin sähköpostiosoitteet liitetään samaan komponenttiin.
Kun kaikki tilit on käsitelty, yhdessä esiintyneillä sähköpostiosoitteilla on sama DSU-juuri joko suoraan tai tilien välisten yhteisten sähköpostiosoitteiden kautta välillisesti. Tämä on keskeinen vaihe, joka levittää yhteyden useiden tilien välillä.
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)) # FalseSähköpostiosoitteiden kerääminen komponenttien mukaan
Kun kaikki yhdistämiset on tehty, käymme jokaisen sähköpostiosoitteen läpi, etsimme sen DSU-juuren ja ryhmittelemme sähköpostiosoitteet juuren mukaan käyttäen luetteloiden sanakirjaa. Juuritunnisteesta tulee avain. Lopuksi haemme kunkin ryhmän tilin nimen, lajittelemme sähköpostiosoitteet ja lisäämme nimen listan alkuun.
Sähköpostiosoitteet on lajiteltava tehtävän vaatimusten mukaisesti — yhdistetyssä tilissä niiden on oltava leksikografisessa järjestyksessä. Nimi voidaan hakea mistä tahansa ryhmän sähköpostiosoitteesta, koska kaikki saman komponentin sähköpostiosoitteet kuuluvat samalle henkilölle.
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)Accounts Merge -ongelman täydellinen ratkaisu
Tässä on täydellinen ratkaisu, joka yhdistää kaikki kolme vaihetta: sähköpostiosoitteiden ja tunnisteiden välisen kuvauksen rakentamisen, sähköpostiosoitteiden yhdistämisen kunkin tilin sisällä sekä sähköpostiosoitteiden keräämisen DSU-juuren mukaan ryhmiteltyinä. Kokonaisaikavaativuus on O(n × m × alpha(n × m)), missä n on tilien määrä ja m yhden tilin sähköpostiosoitteiden enimmäismäärä; käytännössä aikavaativuus on O(n × m).
Tilavaativuus on O(n × m) sähköpostiosoitteiden sanakirjoille ja DSU-taulukoille. Tämä ratkaisu käsittelee myös välilliset yhdistämiset oikein: jos tili A jakaa sähköpostiosoitteen X tilin B kanssa ja tili B jakaa sähköpostiosoitteen Y tilin C kanssa, A, B ja C yhdistetään samaan ryhmään.
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-vaihtoehto Accounts Merge -ongelmaan
Vaihtoehtoinen lähestymistapa rakentaa sähköpostiosoitteiden ja tilien välisen graafin, jossa sähköpostiosoitteet ovat solmuja ja kaaret yhdistävät samassa tilissä esiintyvät sähköpostiosoitteet. Tämän jälkeen BFS/DFS etsii jokaisen yhtenäisen komponentin. Ratkaisu on oikea, mutta siinä graafi on rakennettava erikseen ja BFS suoritettava jokaisesta käsittelemättömästä sähköpostiosoitteesta — koodia tulee enemmän, ja ratkaisua on vaikeampi hahmottaa kuin DSU:ta.
DSU on selkeämpi, koska union-find-rakenne esittää komponenttijäsenyyden luontevasti ilman eksplisiittistä vierekkäisyyslistaa. BFS on tässä parempi vain silloin, kun on tarpeen muodostaa uudelleen kahden tilin välinen todellinen jaettujen sähköpostiosoitteiden polku tai ketju.
# 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)Yleistys: graafin yhtenäiset komponentit
Tilien yhdistämisen malli yleistyy kaikkiin tunnisteellisten yhtenäisten komponenttien ongelmiin: käytössä on joukko alkioita, joista jotkin ilmoitetaan ekvivalenteiksi (yhdistetyiksi), ja tavoitteena on ryhmitellä kaikki transitiivisesti ekvivalentit alkiot yhteen. Esimerkkejä ovat klusterointiongelmat, sosiaalisen verkoston ystäväryhmät ja kaksoistietueiden tunnistaminen.
Yleinen algoritmi on aina: (1) määritä kullekin alkiolle kokonaislukutunnus, (2) yhdistä ekvivalenteiksi ilmoitettujen alkioiden tunnukset union-operaatiolla, (3) ryhmittele alkiot niiden DSU-juuren mukaan. DSU on pohjimmiltaan ekvivalenssirelaatioiden ryhmittelykone.
# 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))Reunatapausten käsittely
Tärkeät reunatapaukset tilien yhdistämisessä:
- Vain yhden sähköpostiosoitteen sisältävät tilit: vain yhden sähköpostiosoitteen sisältävä tili muodostaa oman komponenttinsa, ellei jokin toinen tili jaa samaa sähköpostiosoitetta.
- Sama nimi, eri henkilöt: kahdella tilillä esiintyvä 'John' ei tarkoita, että kyseessä olisi sama henkilö — vain jaetut sähköpostiosoitteet yhdistävät tilit. Nimi tallennetaan sähköpostiosoitekohtaisesti, ei komponenttia kohti.
- Tyhjät tilit: tili, jolla ei ole sähköpostiosoitteita, on ohitettava indeksivirheiden välttämiseksi.
Tarkistakaa aina, että ratkaisunne käsittelee oikein tilit, joita ei pidä yhdistää vain siksi, että niillä on sama nimi. DSU-yhteydet perustuvat yksinomaan jaettuihin sähköpostiosoitteisiin.
# 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)Graafin yhtenäisten komponenttien määrä
Aiheeseen liittyvässä ongelmassa (LeetCode 323) kysytään suuntaamattoman graafin yhtenäisten komponenttien määrää. Tämä on tilien yhdistämistä yksinkertaisempi tehtävä: alusta DSU n solmulle, käsittele kaikki kaaret union-operaatiolla ja laske lopuksi erilliset juuret.
Tiiviimpi tapa komponenttien laskemiseen on ylläpitää muuttujaa count, jonka alkuarvo on n, ja vähentää sitä aina, kun onnistunut union-operaatio yhdistää kaksi eri komponenttia. Vaihtoehtoisesti voit laskea lopuksi niiden solmujen i määrän, joille 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 isolatedPienin ja suurin komponentti
Kun DSU seuraa komponenttien kokoa, voitte vastata kysymyksiin, kuten 'mikä on suurimman yhtenäisen komponentin koko?' tai 'kuinka monessa komponentissa on täsmälleen 3 solmua?', ajassa O(n) käymällä koko-taulukon läpi juurisolmujen kohdalla.
Tällaisia kyselyitä esiintyy esimerkiksi ongelmissa, joissa etsitään 'suurinta yhtenäistä saarta' ruudukosta tai 'tunnistetaan pienin verkko-osio'. Kun kaikki union-operaatiot on suoritettu, käykää läpi solmut i, joille find(i) == i (eli juuret), ja tarkastelkaa niiden kokoja.
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)])Haastatteluvinkkejä DSU-ongelmiin
Kun kohtaatte ongelman, jossa ryhmiä yhdistetään, tehdään yhteyskyselyitä tai etsitään ylimääräistä kaarta, ajatelkaa heti DSU:ta. Haastatteluissa mainitkaa molemmat optimoinnit (polun pakkaus + yhdistäminen rankin/koon perusteella) osoittaaksenne ymmärtävänne aiheen syvällisesti, vaikka yksinkertaisempi naiivi DSU riittäisi annetuilla rajoitteilla.
Yleisiä vältettäviä virheitä ovat tilanteen käsittelemättä jättäminen, jossa molemmat päätepisteet ovat jo yhteydessä toisiinsa (union-operaatio ei tee mitään), 0-indeksoinnin ja 1-indeksoinnin sekoittaminen sekä tulosteen lajittelematta jättäminen tilien yhdistämisessä (ongelma edellyttää lajiteltuja sähköpostilistoja). Selvittäkää aina syötteen rajoitteet ennen koodaamista.
# 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)Pikatesti
Testatkaa, miten hyvin hallitsette tämän oppitunnin Data Structures & Algorithms — Coding Interview Prep -aiheen.
Oppitunnin kertaus
Tässä oppitunnissa opitte: accounts-merge on yhtenäisten komponenttien ongelma, jossa sähköpostiosoitteet ovat solmuja ja tilit yhdistävät sähköpostiosoitteita, DSU ratkaisee sen muuntamalla sähköpostiosoitteet kokonaislukutunnuksiksi, yhdistämällä kunkin tilin tunnukset ja ryhmittelemällä ne juuren mukaan ja sama DSU-ryhmittelymalli sopii kaikkiin ekvivalenssiluokka- tai klusterointiongelmiin. Seuraavaksi siirrymme bittimanipulaatioon aloittaen perusoperaattoreista AND, OR, XOR, NOT ja siirto-operaattoreista.
Opi Python tekoälytuutorin avulla — ilmaiseksi
Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.
- Kurssit
- 30
- Oppitunnit
- 120
Usein kysytyt kysymykset
Onko oppitunti ”Accounts Merge ja yhtenäiset komponentit” ilmainen?
Kyllä — voit lukea täällä verkossa kokonaan ilmaiseksi mitkä tahansa DSA Interview Prep-oppimispolun 3 oppituntia, myös oppitunnin “Accounts Merge ja yhtenäiset komponentit”. Sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä interaktiiviset harjoitukset sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. DSA Interview Prep-kurssilla on yhteensä 4 oppituntia.
Mitä opin oppitunnilla ”Accounts Merge ja yhtenäiset komponentit”?
Ryhmitelkää saman sähköpostin jakavat tilit käsittelemällä sähköposteja DSU-solmuina ja kootkaa sitten kunkin komponentin sähköpostit yhdistettyjen tilien muodostamiseksi. Harjoittelet DSA Interview Prep-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.
Tarvitsenko kokemusta aloittaakseni DSA Interview Prep-opiskelun?
Aiempi kokemus ei ole tarpeen. CoddyKitin DSA Interview Prep-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 4/4.
Kuinka kauan ”Accounts Merge ja yhtenäiset komponentit”-oppitunnin suorittaminen kestää?
Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.
Voinko kirjoittaa ja suorittaa koodia tällä DSA Interview Prep-oppitunnilla?
Kyllä. Jokainen DSA Interview Prep-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.
Kaikki tämän kurssin oppitunnit
- DSU polkujen tiivistyksellä
- Yhdistäminen rangin mukaan ja käänteisen Ackermannin raja
- Redundant Connection ja syklien tunnistus
- Accounts Merge ja yhtenäiset komponentit