在问 · 算法题解

721. 账户合并

中等图论并查集力扣原题 ↗

题目描述

输入列表 accounts。每一项 accounts[i] 是一个字符串列表:accounts[i][0] 是用户名,后面的元素都是这个账户登记的邮箱。

只要两个账户有至少一个相同的邮箱,就认定它们属于同一个人,这种关系可以传递。名字相同但没有共享邮箱的账户,可能是不同的人。同一个人的所有账户名字一定相同。

把属于同一个人的账户合并成一个,返回合并结果。每个账户第一个元素是名字,后面是此人全部邮箱,去重后按 ASCII 字典序升序排列。各账户之间的先后顺序不限。

示例 1
输入:accounts = [["John","johnsmith@mail.com","john_newyork@mail.com"],["John","johnsmith@mail.com","john00@mail.com"],["Mary","mary@mail.com"],["John","johnnybravo@mail.com"]]
输出:[["John","john00@mail.com","john_newyork@mail.com","johnsmith@mail.com"],["Mary","mary@mail.com"],["John","johnnybravo@mail.com"]]

解释:第 1、2 个账户共享 johnsmith@mail.com,合并为一个;另外两个账户各自独立。

示例 2
输入:accounts = [["Gabe","Gabe0@m.co","Gabe3@m.co","Gabe1@m.co"],["Kevin","Kevin3@m.co","Kevin5@m.co","Kevin0@m.co"],["Ethan","Ethan5@m.co","Ethan4@m.co","Ethan0@m.co"],["Hanzo","Hanzo3@m.co","Hanzo1@m.co","Hanzo0@m.co"],["Fern","Fern5@m.co","Fern1@m.co","Fern0@m.co"]]
输出:[["Gabe","Gabe0@m.co","Gabe1@m.co","Gabe3@m.co"],["Kevin","Kevin0@m.co","Kevin3@m.co","Kevin5@m.co"],["Ethan","Ethan0@m.co","Ethan4@m.co","Ethan5@m.co"],["Hanzo","Hanzo0@m.co","Hanzo1@m.co","Hanzo3@m.co"],["Fern","Fern0@m.co","Fern1@m.co","Fern5@m.co"]]

解释:五个账户互相没有共同邮箱,各自只需把邮箱排序。

数据范围

  • 1 <= accounts.length <= 1000
  • 2 <= accounts[i].length <= 10
  • 1 <= accounts[i][j].length <= 30
  • accounts[i][0] 只含英文字母。
  • j > 0 时 accounts[i][j] 都是合法邮箱。

解题思路:并查集

把账户编号当作并查集的节点。用哈希表 owner 记录每个邮箱第一次出现在哪个账户。扫描账户 i 的邮箱时,如果邮箱已被账户 j 记录,说明 i 和 j 是同一个人,合并两者的根;否则记录 owner[email] = i。最后按每个邮箱的根把邮箱分组,排序后在前面加上根账户的名字。

为什么正确

扫描过程中,两个账户在同一集合当且仅当它们通过已扫描的共享邮箱(直接或间接)相连。

复杂度分析

时间 O(S log S),S 为邮箱总数,主要是排序;并查集操作近似常数。空间 O(S)。

易错点

不能只按名字合并:同名的人可能不是同一个人。也不能只比较相邻账户,必须用并查集处理传递关系(A 与 B 共享、B 与 C 共享,则 A、C 也要合并)。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
        parent = list(range(len(accounts)))
        def find(x):
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x
        owner = {}
        for i, acc in enumerate(accounts):
            for email in acc[1:]:
                if email in owner:
                    parent[find(i)] = find(owner[email])
                else:
                    owner[email] = i
        groups = collections.defaultdict(set)
        for email, i in owner.items():
            groups[find(i)].add(email)
        return [[accounts[root][0]] + sorted(emails) for root, emails in groups.items()]

载入编辑器,自己跑一遍 →

其他解法

两种解法都在求邮箱的连通分量:并查集边读边合并,不需要建图;深度优先搜索先建图再遍历,更符合图论直觉。排序是两者共同的主要开销。

解法 2:建图 + 深度优先搜索 时间 O(S log S),空间 O(S)

把每个邮箱看成图中的点。每个账户里,把第一个邮箱和其余邮箱各连一条无向边,并记录邮箱对应的名字。然后遍历所有邮箱,从未访问的邮箱出发用栈做深度优先搜索,收集同一连通分量的全部邮箱。分量内排序后,在前面加上名字。

思路直接,就是求连通分量。需要显式建邻接表,内存比并查集多一些。并查集还支持边读边合并,适合在线场景。

class Solution:
    def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
        graph = collections.defaultdict(list)
        name_of = {}
        for acc in accounts:
            first = acc[1]
            for email in acc[1:]:
                name_of[email] = acc[0]
                graph[first].append(email)
                graph[email].append(first)
        seen = set()
        res = []
        for email in name_of:
            if email in seen:
                continue
            seen.add(email)
            stack, comp = [email], []
            while stack:
                u = stack.pop()
                comp.append(u)
                for v in graph[u]:
                    if v not in seen:
                        seen.add(v)
                        stack.append(v)
            res.append([name_of[email]] + sorted(comp))
        return res

图论 专题的其他题目

查看 图论 全部题目 →