Forberedelse til kodeinterviews · Lektion

Sammenfletning af konti og sammenhængende komponenter

Gruppér konti, der deler en e-mailadresse, ved at behandle e-mailadresser som DSU-noder, og saml derefter alle e-mailadresser pr. komponent for at rekonstruere de sammenflettede konti.

Lektion 4 af 413 trin

Sammenfletning af konti og sammenhængende komponenter er en gratis Forberedelse til kodeinterviews-lektion på CoddyKit. Dette er lektion 4 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Forberedelse til kodeinterviews, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Forberedelse til kodeinterviews-kurset indeholder 4 lektioner i alt.

Problem: Sammenlægning af konti

Problemet Sammenlægning af konti (LeetCode 721) giver dig en liste af konti, hvor hver konto er en liste af strenge, og det første element er kontonavnet, mens resten er e-mailadresser. To konti tilhører samme person, hvis de deler mindst én e-mailadresse. Sammenlæg alle konti, der tilhører samme person, og returnér sorterede e-maillister.

Dette er grundlæggende et problem med sammenhængskomponenter, hvor e-mailadresser er knuder, og en fælles konto forbinder dem. DSU er det ideelle værktøj: Forbind alle e-mailadresser inden for den samme konto, og indsaml derefter e-mailadresser pr. komponent.

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

Knytning af e-mailadresser til heltals-ID'er

DSU arbejder med heltalsindekser, men vores knuder er e-mailstrenge. Vi skal knytte hver unik e-mailadresse til et heltals-ID. Vi skal også huske, hvilket navn der ejer hver e-mailadresse. Brug ordbogen email_to_id til at tildele stigende ID'er, og email_to_name til at holde styr på det kontonavn, der er knyttet til hver e-mailadresse.

Hver unik e-mailadresse får ét ID. Hvis den samme e-mailadresse optræder i flere konti, knyttes den til det samme ID — og når ID'erne for e-mailadresserne i én konto forbindes, knyttes de sammen til én komponent. Navnet, der er knyttet til rode-mailadressens ID, er navnet på den sammenlagte konto.

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

Forbind e-mailadresser inden for hver konto

For hver konto forbinder vi ID'erne for alle e-mailadresser, der er angivet sammen. Vi vælger den første e-mailadresse i kontoen som repræsentant og forbinder alle andre e-mailadressers ID med den. På den måde knyttes alle e-mailadresser i kontoen sammen i én komponent.

Efter behandlingen af alle konti har e-mailadresser, der optrådte sammen (direkte eller transitivt via fælles e-mailadresser på tværs af konti), den samme DSU-rod. Dette er det afgørende trin, der viderefører sammenhængen på tværs af flere konti.

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

Indsamling af e-mailadresser pr. komponent

Efter alle sammenføjninger er udført, gennemgår vi hver e-mailadresse, finder dens DSU-rod og grupperer e-mailadresser efter denne rod ved hjælp af en ordbog med lister. Rod-ID'et bliver nøglen. Til sidst henter vi for hver gruppe kontonavnet, sorterer e-maillisten og sætter navnet foran.

Sortering af e-mailadresserne kræves af opgaven — i en sammenlagt konto skal e-mailadresserne stå i leksikografisk rækkefølge. Navnet kan hentes fra enhver e-mailadresse i gruppen, fordi alle e-mailadresser i én komponent tilhører den samme person.

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)

Komplet løsning på sammenlægning af konti

Her er den komplette løsning, der kombinerer alle tre trin: opbyg tilknytningen fra e-mailadresse til ID, forbind e-mailadresser inden for hver konto, og indsaml grupperede e-mailadresser efter DSU-rod. Den samlede tidskompleksitet er O(n × m × alpha(n × m)), hvor n er antallet af konti, og m er det maksimale antal e-mailadresser pr. konto, hvilket i praksis er O(n × m).

Pladskompleksiteten er O(n × m) for e-mailtilknytningerne og DSU-tabellerne. Denne løsning håndterer den transitive sammenlægning korrekt: Hvis konto A deler e-mailadresse X med konto B, og konto B deler e-mailadresse Y med konto C, bliver A, B og C samlet i én gruppe.

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)

Alternativ til BFS/DFS ved sammenlægning af konti

En alternativ tilgang opbygger en graf, der forbinder e-mails med konti, hvor e-mails er knuder, og kanter forbinder e-mails, der optræder i den samme konto. Derefter finder BFS/DFS hver sammenhængende komponent. Selvom det er korrekt, kræver denne tilgang, at du eksplicit opbygger grafen og kører BFS fra hver e-mail, der endnu ikke er besøgt — mere kode og sværere at gennemskue end DSU.

DSU er mere enkelt, fordi union-find-strukturen naturligt repræsenterer komponentmedlemskab uden behov for en eksplicit naboskabsliste. BFS er kun at foretrække her, hvis du har brug for at rekonstruere den faktiske sti eller kæde af fælles e-mails mellem to konti.

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

Generalisering: sammenhængende komponenter i en graf

Mønsteret for kontosammenlægning generaliserer til alle problemer med sammenhængende-komponenter-med-etiketter: du har en samling elementer, nogle elementer erklæres ækvivalente (forbundne), og du vil samle alle transitivt ækvivalente elementer. Eksempler omfatter klyngedannelsesproblemer, vennegrupper i sociale netværk og registrering af duplikerede poster.

Den generelle algoritme er altid: (1) tildel hvert element et heltals-ID, (2) foren ID'erne for erklæret ækvivalente elementer, (3) gruppér elementerne efter deres DSU-rod. DSU er i praksis en grupperingsmotor for ækvivalensrelationer.

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

Håndtering af særlige tilfælde

Vigtige særlige tilfælde ved kontosammenlægning:

  • Konti med én e-mail: En konto med kun én e-mail danner sin egen komponent, medmindre en anden konto deler den pågældende e-mail.
  • Samme navn, forskellige personer: At 'John' optræder i to konti, betyder ikke, at der er tale om den samme person — kun delte e-mails får konti til at blive sammenlagt. Navnet gemmes pr. e-mail, ikke pr. komponent.
  • Tomme konti: En konto uden e-mails skal springes over for at undgå indeksfejl.

Kontrollér altid, at din løsning håndterer konti, som ikke skal sammenlægges, blot fordi de deler et navn. DSU-forbindelserne styres udelukkende af delte e-mailadresser.

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

Antal sammenhængende komponenter i en graf

En relateret opgave (LeetCode 323) spørger efter antallet af sammenhængende komponenter i en ikke-rettet graf. Denne opgave er enklere end kontosammenlægning: initialiser DSU med n knuder, behandl alle kanter med union, og optæl derefter de forskellige rødder.

Den mest koncise måde at tælle komponenter på er at vedligeholde en count-variabel, der starter ved n, og mindske den, hver gang en vellykket union-operation sammenlægger to forskellige komponenter. Alternativt kan du til sidst tælle antallet af knuder i, hvor 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

Mindste og største komponent

Når du har DSU med størrelsessporing, kan du besvare forespørgsler som 'hvad er størrelsen på den største sammenhængende komponent?' eller 'hvor mange komponenter har præcis 3 knuder?' i O(n) ved at gennemgå størrelsesarrayet ved rodknuderne.

Disse forespørgsler optræder i opgaver som 'find den største sammenhængende ø' på et gitter eller 'find den mindste netværkspartition'. Når alle union-operationer er udført, skal du gennemgå de knuder i, hvor find(i) == i (det er rødderne), og undersøge deres størrelser.

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 til DSU-opgaver

Når du møder en opgave med sammenlægning af grupper, forespørgsler om sammenhæng eller at finde den ekstra kant, så tænk straks på DSU. Under interviews bør du nævne begge optimeringer (stikomprimering + union efter rang/størrelse) for at vise, at du har en dyb forståelse, selvom en enklere naiv DSU ville være tilstrækkelig med de givne begrænsninger.

Almindelige fejl, du bør undgå: at glemme situationen, hvor begge endepunkter allerede er forbundet (union-operationen gør ikke noget), at bruge 0-baserede og 1-baserede indeks forkert og ikke sortere outputtet ved kontosammenlægning (opgaven kræver sorterede e-maillister). Afklar altid begrænsningerne for inputtet, før du skriver kode.

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

Hurtigt tjek

Afprøv din forståelse af begreberne fra Data Structures & Algorithms — Coding Interview Prep i denne lektion.

Opsummering af lektionen

I denne lektion lærte du, at: kontosammenlægning er et problem med sammenhængende komponenter, hvor e-mails er knuder, og konti forbinder e-mails, DSU løser det ved at knytte e-mails til heltals-ID'er, forene ID'er inden for hver konto og gruppere efter rod, og den samme DSU-skabelon til gruppering kan bruges til alle problemer med ækvivalensklasser eller klyngedannelse. Næste lektion skifter vi til bitmanipulation, begyndende med de grundlæggende AND-, OR-, XOR-, NOT- og skiftoperatorer.

Gratis at komme i gang

Lær Forberedelse til kodeinterviews med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
90
Lektioner
360

Ofte stillede spørgsmål

Er lektionen “Sammenfletning af konti og sammenhængende komponenter” gratis?

Ja — hele teksten til “Sammenfletning af konti og sammenhængende komponenter” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Forberedelse til kodeinterviews-kurset, skal du opgradere til CoddyKit PRO. Forberedelse til kodeinterviews-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Sammenfletning af konti og sammenhængende komponenter”?

Gruppér konti, der deler en e-mailadresse, ved at behandle e-mailadresser som DSU-noder, og saml derefter alle e-mailadresser pr. komponent for at rekonstruere de sammenflettede konti. Du øver dig i Forberedelse til kodeinterviews med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Forberedelse til kodeinterviews?

Der kræves ingen tidligere erfaring. Forberedelse til kodeinterviews på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 4 af 4.

Hvor lang tid tager lektionen “Sammenfletning af konti og sammenhængende komponenter”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Forberedelse til kodeinterviews-lektion?

Ja. Alle Forberedelse til kodeinterviews-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. DSU med sti-komprimering
  2. Union efter rang og den inverse Ackermann-grænse
  3. Redundant forbindelse og cykeldetektion
  4. Sammenfletning af konti og sammenhængende komponenter
← Tilbage til Forberedelse til kodeinterviews