DSA Interview Prep · Les

Accounts Merge en verbonden componenten

Groepeer accounts die een e-mailadres delen door e-mailadressen als DSU-knopen te behandelen en verzamel vervolgens alle e-mailadressen per component om de samengevoegde accounts te reconstrueren.

Les 4 van 413 stappen

Accounts Merge en verbonden componenten is een gratis DSA Interview Prep-les op CoddyKit. Dit is les 4 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject DSA Interview Prep. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus DSA Interview Prep bevat in totaal 4 lessen.

Probleem: accounts samenvoegen

Het probleem Accounts Merge (LeetCode 721) geeft je een lijst met accounts, die elk een lijst met tekenreeksen zijn waarbij het eerste element de accountnaam is en de rest e-mailadressen zijn. Twee accounts horen bij dezelfde persoon als ze minstens één e-mailadres delen. Voeg alle accounts van dezelfde persoon samen en geef gesorteerde lijsten met e-mailadressen terug.

Dit is in wezen een probleem met samenhangende componenten, waarbij e-mailadressen de knopen zijn en een gedeeld account ze met elkaar verbindt. DSU is het ideale hulpmiddel: verenig alle e-mailadressen binnen hetzelfde account en verzamel daarna de e-mailadressen per component.

# 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')

E-mailadressen aan gehele ID's koppelen

DSU werkt met gehele indices, maar onze knopen zijn tekenreeksen met e-mailadressen. We moeten elk uniek e-mailadres aan een geheel ID koppelen. We moeten ook onthouden welke naam bij elk e-mailadres hoort. Gebruik een woordenboek email_to_id om oplopende ID's toe te wijzen en email_to_name om de accountnaam bij elk e-mailadres bij te houden.

Elk uniek e-mailadres krijgt één ID. Als hetzelfde e-mailadres in meerdere accounts voorkomt, wordt het aan dezelfde ID gekoppeld — en door de ID's van e-mailadressen binnen één account te verenigen, verbind je ze tot één component. De naam die bij de ID van het e-mailadres van de wortel hoort, is de naam van het samengevoegde account.

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]})')

E-mailadressen binnen elk account verenigen

Voor elk account verenigen we de ID's van alle e-mailadressen die erin staan. We kiezen het eerste e-mailadres in het account als vertegenwoordiger en verenigen de ID van elk ander e-mailadres ermee. Zo worden alle e-mailadressen in het account met elkaar verbonden in één component.

Na het verwerken van alle accounts hebben e-mailadressen die samen voorkwamen (direct of transitief via gedeelde e-mailadressen in verschillende accounts) allemaal dezelfde DSU-wortel. Dit is de essentiële stap die connectiviteit door meerdere accounts heen doorgeeft.

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

E-mailadressen per component verzamelen

Nadat alle verenigingen zijn uitgevoerd, doorlopen we elk e-mailadres, zoeken we de DSU-wortel en groeperen we e-mailadressen per wortel met een woordenboek van lijsten. De ID van de wortel wordt de sleutel. Ten slotte halen we voor elke groep de accountnaam op, sorteren we de lijst met e-mailadressen en voegen we de naam vooraan toe.

Het sorteren van de e-mailadressen is vereist door het probleem — binnen een samengevoegd account moeten e-mailadressen in lexicografische volgorde staan. De naam kan worden opgehaald van elk e-mailadres in de groep, omdat alle e-mailadressen in één component bij dezelfde persoon horen.

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)

Volledige oplossing voor het samenvoegen van accounts

Hier is de volledige oplossing die alle drie stappen combineert: de koppeling van e-mailadressen aan ID's opbouwen, e-mailadressen binnen elk account verenigen en gegroepeerde e-mailadressen per DSU-wortel verzamelen. De totale tijdcomplexiteit is O(n × m × alpha(n × m)), waarbij n het aantal accounts is en m het maximale aantal e-mailadressen per account, dus praktisch O(n × m).

De ruimtecomplexiteit is O(n × m) voor de e-mailkoppelingen en DSU-arrays. Deze oplossing verwerkt transitief samenvoegen correct: als account A e-mailadres X deelt met account B en account B e-mailadres Y deelt met account C, worden A, B en C allemaal samengevoegd tot één groep.

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)

Alternatief voor het samenvoegen van accounts met BFS/DFS

Een alternatieve aanpak bouwt een graaf van e-mailadressen naar accounts, waarin e-mailadressen knopen zijn en verbindingen e-mailadressen koppelen die in hetzelfde account voorkomen. Vervolgens vindt BFS/DFS elke verbonden component. Hoewel dit correct is, moet je de graaf expliciet opbouwen en BFS uitvoeren vanaf elk nog niet bezocht e-mailadres — meer code en lastiger te doorgronden dan DSU.

DSU is overzichtelijker, omdat de union-find-structuur op natuurlijke wijze weergeeft bij welke component een item hoort, zonder dat je een expliciete lijst met aangrenzende knopen nodig hebt. BFS heeft hier alleen de voorkeur als je het daadwerkelijke pad of de keten van gedeelde e-mailadressen tussen twee accounts moet reconstrueren.

# 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)

Generaliseren: verbonden componenten in grafen

Het patroon voor het samenvoegen van accounts kan worden gegeneraliseerd naar elk probleem met verbonden componenten en labels: je hebt een verzameling items, sommige items zijn als gelijkwaardig (verbonden) aangemerkt en je wilt alle transitief gelijkwaardige items samen groeperen. Voorbeelden zijn clusteringproblemen, vriendengroepen in sociale netwerken en het detecteren van dubbele records.

Het algemene algoritme is altijd: (1) wijs elk item een geheelgetal-ID toe, (2) voeg de ID's van als gelijkwaardig aangemerkte items samen, (3) groepeer items op basis van hun DSU-wortel. DSU is in wezen een groeperingsmechanisme voor equivalentierelaties.

# 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))

Randgevallen afhandelen

Belangrijke randgevallen bij het samenvoegen van accounts:

  • Accounts met één e-mailadres: een account met slechts één e-mailadres vormt een eigen component, tenzij een ander account dat e-mailadres deelt.
  • Dezelfde naam, verschillende personen: het voorkomen van 'John' in twee accounts betekent niet dat het om dezelfde persoon gaat — alleen gedeelde e-mailadressen voegen accounts samen. De naam wordt per e-mailadres opgeslagen, niet per component.
  • Lege accounts: een account zonder e-mailadressen moet worden overgeslagen om indexfouten te voorkomen.

Controleer altijd of je oplossing accounts niet samenvoegt alleen omdat ze dezelfde naam hebben. De DSU-verbindingen worden uitsluitend bepaald door gedeelde e-mailadressen.

# 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)

Aantal verbonden componenten in een graaf

Een verwant probleem (LeetCode 323) vraagt naar het aantal verbonden componenten in een ongerichte graaf. Dit is eenvoudiger dan het samenvoegen van accounts: initialiseer DSU met n knopen, verwerk alle verbindingen met union en tel daarna het aantal verschillende wortels.

De meest beknopte manier om componenten te tellen is een count-variabele bij te houden die begint op n en deze telkens met één te verlagen wanneer een geslaagde union twee verschillende componenten samenvoegt. Je kunt ook aan het einde het aantal knopen i tellen waarvoor 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

Kleinste en grootste component

Zodra je DSU hebt uitgebreid met het bijhouden van groottes, kun je vragen beantwoorden zoals 'wat is de grootte van de grootste verbonden component?' of 'hoeveel componenten hebben precies 3 knopen?' in O(n) door de array met groottes bij wortelknopen te doorlopen.

Deze vragen komen voor in problemen zoals 'vind het grootste verbonden eiland' in een raster of 'identificeer de kleinste netwerkpartitie'. Nadat alle unions zijn voltooid, doorloop je de knopen i waarvoor find(i) == i (dit zijn wortels) en bekijk je hun groottes.

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)])

Interviewtips voor DSU-problemen

Wanneer je een probleem tegenkomt met groepen samenvoegen, connectiviteitsvragen of de extra verbinding vinden, denk dan direct aan DSU. Noem tijdens interviews beide optimalisaties (padcompressie + union op basis van rang/grootte) om diepgang te tonen, zelfs als een eenvoudige naïeve DSU gezien de beperkingen zou volstaan.

Veelgemaakte fouten die je moet vermijden zijn: vergeten het geval af te handelen waarin beide eindpunten al verbonden zijn (union doet niets), 0- en 1-gebaseerde indexering door elkaar halen en de uitvoer voor het samenvoegen van accounts niet sorteren (het probleem vereist gesorteerde e-maillijsten). Verduidelijk altijd de invoerbeperkingen voordat je code schrijft.

# 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)

Korte controle

Toets je begrip van de concepten van Data Structures & Algorithms — Coding Interview Prep uit deze les.

Samenvatting van de les

In deze les heb je geleerd: het samenvoegen van accounts is een probleem met verbonden componenten waarin e-mailadressen knopen zijn en accounts e-mailadressen aan elkaar koppelen, DSU lost dit op door e-mailadressen aan gehele-ID's te koppelen, ID's binnen elk account samen te voegen en op basis van de wortel te groeperen en hetzelfde DSU-groeperingssjabloon geldt voor elk probleem met equivalentieklassen of clustering. Hierna schakelen we over naar bitmanipulatie, te beginnen met de fundamentele AND-, OR-, XOR-, NOT- en verschuivingsoperatoren.

Gratis beginnen

Leer Python met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
30
Lessen
120

Veelgestelde vragen

Is de les “Accounts Merge en verbonden componenten” gratis?

Ja — je kunt hier op het web alle 3 lessen van het leerpad DSA Interview Prep, waaronder “Accounts Merge en verbonden componenten”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus DSA Interview Prep bevat in totaal 4 lessen.

Wat leer ik in “Accounts Merge en verbonden componenten”?

Groepeer accounts die een e-mailadres delen door e-mailadressen als DSU-knopen te behandelen en verzamel vervolgens alle e-mailadressen per component om de samengevoegde accounts te reconstrueren. Je oefent met DSA Interview Prep door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met DSA Interview Prep te beginnen?

Ervaring vooraf is niet nodig. DSA Interview Prep op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 4 van 4.

Hoe lang duurt de les “Accounts Merge en verbonden componenten”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over DSA Interview Prep?

Ja. Elke les over DSA Interview Prep bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. DSU met padcompressie
  2. Union by Rank en de inverse-Ackermann-grens
  3. Redundant Connection en cyclusdetectie
  4. Accounts Merge en verbonden componenten
← Terug naar DSA Interview Prep