class Solution:
    def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
        """
        DFS way
        1. build graph email: acnt, vitsit_acnt set to avoid cycle 
        2. dfs traverse email and its owner's other accnt, record path emails and  skip visited acnt
        3.  each time one acnt have done dfs, add sorted 
        emails into res 
        """
        res = []
        em2ac = collections.defaultdict(list) # graph
        seen_acc = set()
        # build graph 
        for i, acc in enumerate(accounts):
            for j in range(1,len(acc)):
                em = acc[j]
                em2ac[em].append(i)
        #print(em2ac)
        # @emails: set(), for temporary cache emails belong to same user 
        def dfs(emails, acc):
            
            if acc in seen_acc:
                return 
            seen_acc.add(acc)
            for j in range(1, len(accounts[acc])):
                email = accounts[acc][j]
                emails.add(email)
                for nei in em2ac[email]:
                    dfs(emails,nei)
        for i in range(len(accounts)):
            # note need to recheck cases when account has no other ownership 
            if i in seen_acc:
                continue
            emails = set()
            name = accounts[i][0]
            dfs(emails,i)
            res.append([name]+sorted(emails))
        return res 

Optimization insead of buiding graph using email as edges and idx of accounts as vertices, using emails to connect common email in other account

class Solution:
    def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
        """
        DFS way
        1. build graph email: acnt, vitsit_acnt set to avoid cycle 
        2. dfs traverse email and its owner's other accnt, record path emails and  skip visited acnt
        3.  each time one acnt have done dfs, add sorted 
        emails into res 
        """
        email_name = {}
        res = []
        em2ac = collections.defaultdict(set) # graph
        #seen_acc = set()
        seen_email = set()
        # build graph 
        for i, acc in enumerate(accounts):
            name = acc[0]
            for j in range(1,len(acc)):
                em = acc[j]
                email_name[em] = name 
               # em2ac[em].add(i)
                em2ac[em].add(acc[1])
                em2ac[acc[1]].add(em)
        # print(em2ac)
        # print(email_name)
        # @emails: set(), for temporary cache emails belong to same user 
        def dfs(emails, em):
            if em in seen_email:
                return 
            seen_email.add(em)
            emails.add(em)
            for nei in em2ac[em]:
                if nei not in seen_email:
                    dfs(emails,nei)
                        
        for email in email_name:
            # note need to recheck cases when account has no other ownership 
            if email in seen_email:
                continue
            emails = set()
            name = email_name[email]
            dfs(emails,email)
            res.append([name]+sorted(emails))
        return res 

UF

class UF:
    def __init__(self,n):
        self.p = list(range(n))
        self.ranks = [1] * n 
    def find(self,u):
        if u != self.p[u]:
            self.p[u] = self.find(self.p[u])
        return self.p[u]
    def union(self,u,v):
        pu,pv = self.find(u),self.find(v)
        if pu == pv :
            return False 
        elif pu > pv:
            self.p[pv] = pu
            self.ranks[pu] += self.ranks[pv]
        elif pu < pv:
            self.p[pu] = pv
            self.ranks[pv] += self.ranks[pu]
        return True 
    
class Solution:
    def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
        """
        UF
        1 traverse all emails, if current email not seen, put in
        """
        n = len(accounts)
        uf = UF(n)
        mail2idx = {}
        res = []
        for i in range(n):
            for idx , email in enumerate(accounts[i]):
                if idx == 0 :
                    continue 
                if email not in mail2idx:
                    mail2idx[email] = i 
                else:
                    pre_idx = mail2idx[email]
                    uf.union(pre_idx,i)
        # @sets:   parent_idx:  emails 
        sets = collections.defaultdict(set)
        for i in range(n):
            parent_idx = uf.find(i)
            for j in range(1, len(accounts[i])):
                email = accounts[i][j]
                sets[parent_idx].add(email)
        for i, emails in sets.items():
            name = accounts[i][0]
            res.append([name]+sorted(list(emails)))
        return res