204. Accounts Merge
Problem
Merge accounts represented as [name,email1,...], with at least one email per account. Accounts sharing any email belong to the same person, transitively; connected accounts are guaranteed to have the same name. Equal names alone do not imply identity. Return each group's sorted unique emails after its name, and sort the resulting records lexicographically.
Examples
Example 1
merge_accounts([["Ana","a","b"],["Ana","b","c"],["Ana","z"]])
=> [['Ana', 'a', 'b', 'c'], ['Ana', 'z']]Shared email b merges only the first two accounts.
Example 2
merge_accounts([])
=> []No accounts produce no groups.
Example 3
merge_accounts([["Bo","x","x"]])
=> [['Bo', 'x']]Repeated email occurrences appear once in output.
Reveal the worked solution and reasoning
Why this mechanism works
Emails are identity evidence and account indices are convenient DSU elements. Remember the first account that owns each email; every later occurrence unions its account with that owner. After all unions, group each distinct email by its owner's final representative.
State and boundaries
owner maps each email to an account containing it. DSU components represent all shared-email links processed so far.
Protocol
- Create one DSU element per account.
- Union accounts sharing an email.
- Find the final root for every distinct email's owner.
- Group emails by root.
- Prefix the consistent name, sort emails, and sort records.
Visualization
[Ana,a,b] + [Ana,b,c] -> [Ana,a,b,c]
[Ana,z] stays separate: same name is not shared-email evidencePython implementation
from collections import defaultdict
from kata_201 import DSU
def merge_accounts(accounts):
dsu, owner = DSU(len(accounts)), {}
for i, account in enumerate(accounts):
for email in account[1:]:
if email in owner:
dsu.union(i, owner[email])
else:
owner[email] = i
groups = defaultdict(list)
for email, i in owner.items():
groups[dsu.find(i)].append(email)
return sorted([accounts[root][0]] + sorted(emails)
for root, emails in groups.items())
Worked execution
merge_accounts([["Ana","a","b"],["Ana","b","c"],["Ana","z"]])
L5 merge_accounts: dsu, owner = DSU(len(accounts)), {}
accounts=[['Ana', 'a', 'b'], ['Ana', 'b', 'c'], ['Ana', 'z']]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':3}; owner={}
L6 merge_accounts: for i, account in enumerate(accounts):
accounts=[['Ana', 'a', 'b'], ['Ana', 'b', 'c'], ['Ana', 'z']]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':3}; owner={}; i=0; account=['Ana', 'a', 'b']
L7 merge_accounts: for email in account[1:]:
accounts=[['Ana', 'a', 'b'], ['Ana', 'b', 'c'], ['Ana', 'z']]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':3}; owner={}; i=0; account=['Ana', 'a', 'b']; email='a'
L11 merge_accounts: owner[email] = i
accounts=[['Ana', 'a', 'b'], ['Ana', 'b', 'c'], ['Ana', 'z']]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':3}; owner={'a':0}; i=0; account=['Ana', 'a', 'b']; email='a'
L7 merge_accounts: for email in account[1:]:
accounts=[['Ana', 'a', 'b'], ['Ana', 'b', 'c'], ['Ana', 'z']]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':3}; owner={'a':0}; i=0; account=['Ana', 'a', 'b']; email='b'
L11 merge_accounts: owner[email] = i
accounts=[['Ana', 'a', 'b'], ['Ana', 'b', 'c'], ['Ana', 'z']]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':3}; owner={'a':0, 'b':0}; i=0; account=['Ana', 'a', 'b']; email='b'
L6 merge_accounts: for i, account in enumerate(accounts):
accounts=[['Ana', 'a', 'b'], ['Ana', 'b', 'c'], ['Ana', 'z']]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':3}; owner={'a':0, 'b':0}; i=1; account=['Ana', 'b', 'c']; email='b'
L9 merge_accounts: dsu.union(i, owner[email])
accounts=[['Ana', 'a', 'b'], ['Ana', 'b', 'c'], ['Ana', 'z']]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':2}; owner={'a':0, 'b':0}; i=1; account=['Ana', 'b', 'c']; email='b'
... 14 intermediate state changes omitted ...
L15 <genexpr>: return sorted([accounts[root][0]] + sorted(emails)
.0=dict_itemiterator; accounts=[['Ana', 'a', 'b'], ['Ana', 'b', 'c'], ['Ana', 'z']]
L16 <genexpr>: for root, emails in groups.items())
.0=dict_itemiterator; accounts=[['Ana', 'a', 'b'], ['Ana', 'b', 'c'], ['Ana', 'z']]; root=1; emails=['a', 'b', 'c']
L16 <genexpr>: for root, emails in groups.items())
.0=dict_itemiterator; accounts=[['Ana', 'a', 'b'], ['Ana', 'b', 'c'], ['Ana', 'z']]; root=2; emails=['z']
L16 <genexpr>: for root, emails in groups.items())
.0=dict_itemiterator; accounts=[['Ana', 'a', 'b'], ['Ana', 'b', 'c'], ['Ana', 'z']]; root=2; emails=['z']Result: [['Ana', 'a', 'b', 'c'], ['Ana', 'z']]. Shared email b merges only the first two accounts.
merge_accounts([])
L5 merge_accounts: dsu, owner = DSU(len(accounts)), {}
accounts=[]; dsu=DSU{'parent':[], 'size':[], 'count':0}; owner={}
L12 merge_accounts: groups = defaultdict(list)
accounts=[]; dsu=DSU{'parent':[], 'size':[], 'count':0}; owner={}; groups={}
L15 <genexpr>: return sorted([accounts[root][0]] + sorted(emails)
.0=dict_itemiterator; accounts=[]Result: []. No accounts produce no groups.
Why the solution is correct
Every union is justified by a shared email, and every shared-email occurrence creates the necessary union. DSU transitivity therefore matches exactly the identity relation. Grouping distinct email keys by final roots includes every email once in its correct merged account.
Time and space
O(A+T*alpha(A)+T log T) coarse time for A accounts and T email occurrences, including output sorting; O(A+T) auxiliary/output storage. String comparison costs depend on lengths.
Common mistakes
Merging by name combines unrelated people. Grouping before all unions are complete can split accounts whose connection appears later.
Reasoning card
State: owner maps each email to an account containing it. DSU components represent all shared-email links processed so far.
Invariant: All emails in one root component belong to one merged account.
Decision to rehearse: Union account identities through shared keys, then group by root.
Finish: Prefix the consistent name, sort emails, and sort records
Transfer exercise
Remove the consistent-name guarantee and define how conflicting names should be surfaced instead of silently choosing one.