Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

6.15 Accounts Merge

Source: src/main/kotlin/disjointset/AccountMerge.kt Pattern: Union-Find over emails · Core page

The Problem

Given accounts [name, email1, email2, ...], merge accounts sharing an email. Return [name, sorted unique emails] per merged account.

  • Constraints: accounts ≤ 1000; emails ≤ 10 per account.

Examples

Input:  accounts = [["John","johnsmith@mail.com","john_newyork@mail.com"],
                    ["John","johnsmith@mail.com","john00@mail.com"],
                    ["Mary","mary@mail.com"],
                    ["John","johnnybravo@mail.com"]]
Output: [["John","john00@mail.com","john_newyork@mail.com","johnsmith@mail.com"],
         ["Mary","mary@mail.com"],
         ["John","johnnybravo@mail.com"]]

Intuition — emails are the nodes; accounts union them

Two accounts belong together iff they share an email. So: each email is a Union-Find node; every email of an account is unioned with the account’s first email. Then group emails by their root:

emailToName = {}          # email -> owner name (first seen)
uf = UnionFind()

for (name, *emails) in accounts:
    for email in emails:
        emailToName[email] = name
        uf.union(emails[0], email)     # link all of this account's emails

groups = {}               # root -> sorted emails
for email in emailToName:
    groups[uf.find(email)].append(email)

return [[emailToName[root], *sorted(emails)] for root, emails in groups]

Why union with the account’s first email? It’s the account’s representative — every email in the account gets connected through it, so all accounts sharing any email converge to the same root. The 6.6 Union-Find engine, nodes = emails.

Why the owner name from the first sighting? The problem guarantees the same name per merged account; storing it when first seen avoids the union-by-name dance. find(email) returns the representative email; its stored name is the account’s.

Approach 1 — BFS/DFS over an email graph (adjacency + traversal)

Build email→email edges, traverse components: correct, more machinery than needed.

Approach 2 — Union-Find over emails (the repo’s version, optimal)

class AccountMerge {
    class UnionFind<T> {
        data class Node<T>(var parent: T, var rank: Int)

        private val nodes = mutableMapOf<T, Node<T>>()

        fun add(x: T) {
            nodes.putIfAbsent(x, Node(x, 0))
        }

        fun find(x: T): T {
            val node = nodes[x] ?: throw IllegalAccessException("Value $x not found")
            if (node.parent != x) {
                node.parent = find(node.parent)          // path compression
            }
            return node.parent
        }

        fun union(x: T, y: T) {
            val rootX = find(x)
            val rootY = find(y)
            if (rootX != rootY) {
                val nodeX = nodes[rootX]!!
                val nodeY = nodes[rootY]!!
                when {
                    nodeX.rank > nodeY.rank -> nodeY.parent = rootX   // union by rank
                    nodeX.rank < nodeY.rank -> nodeX.parent = rootY
                    else -> {
                        nodeY.parent = rootX
                        nodeX.rank++
                    }
                }
            }
        }
    }

    /**
     * @param accounts [name, email...] lists
     * @return        merged accounts with sorted unique emails
     */
    fun accountsMerge(accounts: List<List<String>>): List<List<String>> {
        val emailToName = mutableMapOf<String, String>()
        val uf = UnionFind<String>()

        accounts.forEach { account ->
            val name = account[0]
            val firstEmail = account[1]

            account.drop(1).forEach { email ->
                emailToName[email] = name
                uf.add(firstEmail)
                uf.add(email)
                uf.union(firstEmail, email)      // link this account's emails
            }
        }

        val groups = mutableMapOf<String, MutableList<String>>()
        emailToName.keys.forEach { email ->
            groups.getOrPut(uf.find(email)) { mutableListOf() }.add(email)
        }

        return groups.values.map { emails ->
            listOf(emailToName[emails[0]]!!) + emails.sorted()
        }
    }
}
import java.util.*;

public class AccountsMerge {
    private int[] parent, rank;

    private int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);   // path compression
        return parent[x];
    }

    private void union(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return;
        if (rank[rx] < rank[ry]) parent[rx] = ry;          // union by rank
        else if (rank[rx] > rank[ry]) parent[ry] = rx;
        else { parent[ry] = rx; rank[rx]++; }
    }

    /**
     * @param accounts [name, email...] lists
     * @return        merged accounts with sorted unique emails
     */
    public List<List<String>> accountsMerge(List<List<String>> accounts) {
        Map<String, Integer> emailToId = new HashMap<>();
        Map<String, String> emailToName = new HashMap<>();
        parent = new int[10001];
        rank = new int[10001];
        for (int i = 0; i < 10001; i++) parent[i] = i;

        int id = 0;
        for (List<String> account : accounts) {
            String name = account.get(0);
            String first = account.get(1);

            for (int i = 1; i < account.size(); i++) {
                String email = account.get(i);
                emailToName.put(email, name);
                if (!emailToId.containsKey(email)) emailToId.put(email, id++);
                union(emailToId.get(first), emailToId.get(email));
            }
        }

        Map<Integer, List<String>> groups = new HashMap<>();
        for (String email : emailToName.keySet()) {
            int root = find(emailToId.get(email));
            groups.computeIfAbsent(root, k -> new ArrayList<>()).add(email);
        }

        List<List<String>> result = new ArrayList<>();
        for (List<String> emails : groups.values()) {
            Collections.sort(emails);
            List<String> merged = new ArrayList<>();
            merged.add(emailToName.get(emails.get(0)));
            merged.addAll(emails);
            result.add(merged);
        }
        return result;
    }
}
#include <string>
#include <unordered_map>
#include <vector>
#include <algorithm>

class AccountsMerge {
    std::vector<int> parent, rank;

    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);   // path compression
        return parent[x];
    }

    void unite(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return;
        if (rank[rx] < rank[ry]) parent[rx] = ry;
        else if (rank[rx] > rank[ry]) parent[ry] = rx;
        else { parent[ry] = rx; rank[rx]++; }
    }

public:
    /**
     * @param accounts [name, email...] lists
     * @return        merged accounts with sorted unique emails
     */
    std::vector<std::vector<std::string>> accountsMerge(
            std::vector<std::vector<std::string>>& accounts) {
        parent.resize(10001);
        rank.resize(10001, 0);
        for (int i = 0; i < 10001; i++) parent[i] = i;

        std::unordered_map<std::string, int> emailToId;
        std::unordered_map<std::string, std::string> emailToName;
        int id = 0;

        for (auto& account : accounts) {
            for (int i = 1; i < (int)account.size(); i++) {
                emailToName[account[i]] = account[0];
                if (!emailToId.count(account[i])) emailToId[account[i]] = id++;
                unite(emailToId[account[1]], emailToId[account[i]]);
            }
        }

        std::unordered_map<int, std::vector<std::string>> groups;
        for (auto& [email, _] : emailToName) {
            groups[find(emailToId[email])].push_back(email);
        }

        std::vector<std::vector<std::string>> result;
        for (auto& [_, emails] : groups) {
            std::sort(emails.begin(), emails.end());
            emails.insert(emails.begin(), emailToName[emails[0]]);
            result.push_back(emails);
        }
        return result;
    }
};
class UnionFind:
    def __init__(self):
        self.parent = {}
        self.rank = {}

    def add(self, x):
        if x not in self.parent:
            self.parent[x] = x
            self.rank[x] = 0

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])   # path compression
        return self.parent[x]

    def union(self, x, y):
        rx, ry = self.find(x), self.find(y)
        if rx == ry:
            return
        if self.rank[rx] < self.rank[ry]:
            self.parent[rx] = ry
        elif self.rank[rx] > self.rank[ry]:
            self.parent[ry] = rx
        else:
            self.parent[ry] = rx
            self.rank[rx] += 1


def accounts_merge(accounts: list[list[str]]) -> list[list[str]]:
    """
    @param accounts: [name, email...] lists
    @return:        merged accounts with sorted unique emails
    """
    uf = UnionFind()
    email_to_name = {}

    for name, *emails in accounts:
        for email in emails:
            email_to_name[email] = name
            uf.add(email)
            uf.union(emails[0], email)      # link this account's emails

    groups = {}
    for email in email_to_name:
        groups.setdefault(uf.find(email), []).append(email)

    return [[email_to_name[emails[0]], *sorted(emails)] for emails in groups.values()]
#![allow(unused)]
fn main() {
use std::collections::HashMap;

struct UnionFind {
    parent: HashMap<String, String>,
    rank: HashMap<String, usize>,
}

impl UnionFind {
    fn new() -> Self { Self { parent: HashMap::new(), rank: HashMap::new() } }

    fn add(&mut self, x: String) {
        self.parent.entry(x.clone()).or_insert(x);
        self.rank.entry(x).or_insert(0);
    }

    fn find(&mut self, x: String) -> String {
        let p = self.parent.get(&x).unwrap().clone();
        if p != x {
            let root = self.find(p);
            self.parent.insert(x, root.clone());
            root
        } else {
            x
        }
    }

    fn union(&mut self, x: String, y: String) {
        let rx = self.find(x);
        let ry = self.find(y);
        if rx == ry { return; }
        let (rrx, rry) = (self.rank[&rx], self.rank[&ry]);
        if rrx < rry { self.parent.insert(rx, ry); }
        else if rrx > rry { self.parent.insert(ry, rx); }
        else { self.parent.insert(ry, rx.clone()); self.rank.insert(rx, rrx + 1); }
    }
}

impl Solution {
    /// @param accounts [name, email...] lists
    /// @return        merged accounts with sorted unique emails
    pub fn accounts_merge(accounts: Vec<Vec<String>>) -> Vec<Vec<String>> {
        let mut uf = UnionFind::new();
        let mut email_to_name: HashMap<String, String> = HashMap::new();

        for account in &accounts {
            let name = account[0].clone();
            for email in account.iter().skip(1) {
                email_to_name.insert(email.clone(), name.clone());
                uf.add(email.clone());
                uf.union(account[1].clone(), email.clone());
            }
        }

        let mut groups: HashMap<String, Vec<String>> = HashMap::new();
        for email in email_to_name.keys() {
            groups.entry(uf.find(email.clone())).or_default().push(email.clone());
        }

        let mut result = Vec::new();
        for mut emails in groups.into_values() {
            emails.sort();
            let name = email_to_name[&emails[0]].clone();
            let mut merged = vec![name];
            merged.append(&mut emails);
            result.push(merged);
        }
        result
    }
}
}

Dry run

Input: the 4-account example above.

Union-Find over emails (each account links its emails through its first):
acc0: johnsmith@mail.com — john_newyork@mail.com   (root: johnsmith@mail.com)
acc1: johnsmith@mail.com — john00@mail.com         (johnsmith already a root)
acc2: mary@mail.com alone
acc3: johnnybravo@mail.com alone

Components: {johnsmith, john_newyork, john00}, {mary}, {johnnybravo}

Groups by find(): johnsmith@mail.com -> [john_newyork, johnsmith, john00] -> sorted
Output: [["John","john00@mail.com","john_newyork@mail.com","johnsmith@mail.com"],
         ["Mary","mary@mail.com"],
         ["John","johnnybravo@mail.com"]] ✓

The union through the shared email is the merge: acc0 and acc1 converge because both connect to johnsmith@mail.com — Union-Find’s transitive closure does the “sharing an email means same account” logic automatically. The rank + path-compression keep it near-O(1) per op.

Complexity

Time. Union-Find near-linear:

$$ T(E, \alpha) = O(E \cdot \alpha(E)) $$

Space. Maps for parent/name/groups:

$$ S(E) = O(E) $$

Variants & follow-ups

  • Redundant Connection (6.11) — the same Union-Find engine finding the cycle edge.
  • The Earliest Moment Everyone Became Friends (6.10) — time-ordered unions.
  • Interview follow-up: “Why are emails the nodes and not accounts?” The merge rule is “shares an email” — emails are the edges’ endpoints; unioning them per account connects the whole account. Making accounts the nodes would need email→account edges and a second traversal — the email-node framing is the 6.6 “choose the natural node” lesson.