Accounts Merge
Problem
Given a list of accounts where each account contains a name and a list of emails, merge accounts that share at least one email and return the merged accounts with unique emails sorted lexicographically.
- 1 ≤ accounts.length ≤ 1000
- 2 ≤ accounts[i].length ≤ 10
- 1 ≤ accounts[i][j].length ≤ 30
- accounts[i][0] consists of English letters.
- accounts[i][j] (for j > 0) is a valid email.
Example
accounts = [["John", "johnsmith@mail.com", "john00@mail.com"], ["John", "johnnybravo@mail.com"], ["John", "johnsmith@mail.com", "john_newyork@mail.com"], ["Mary", "mary@mail.com"]][["John", "john00@mail.com", "john_newyork@mail.com", "johnsmith@mail.com"], ["John", "johnnybravo@mail.com"], ["Mary", "mary@mail.com"]]The accounts with emails "johnsmith@mail.com" appear in two accounts, so these accounts are merged. The merged account contains all unique emails sorted lexicographically. The other accounts remain separate because they share no emails.
Approach
Straightforward Solution
A brute-force approach would compare every pair of accounts for shared emails, merging them repeatedly until no overlaps remain. This approach is inefficient (potentially O(n^2 * m^2)) and impractical for large inputs.
Core Observation
Accounts sharing emails form connected components in a graph where emails are nodes and edges connect emails appearing in the same account. Merging accounts is equivalent to finding connected components of this graph.
Path to Optimal
Recognizing the problem as graph connectivity allows building an undirected graph where each email is a node connected to other emails in the same account. Using DFS to find connected components groups all emails belonging to the same user. This approach efficiently merges accounts by traversing each connected component once.
Optimal Approach
Construct a graph mapping emails to their neighbors and a map from email to account name. Perform DFS on unvisited emails to collect connected components. For each component, sort the emails and prepend the associated name. Return the list of merged accounts. This approach runs in O(N * M log M) where N is number of accounts and M is max emails per account.
Time
O(N * M log M)
Building the graph takes O(N * M) where N is number of accounts and M is max emails per account. DFS visits each email once. Sorting each connected component's emails dominates with O(M log M) per component, leading to overall O(N * M log M).
Space
O(N * M)
The graph and email-to-name map store all emails and their connections, requiring space proportional to total emails.
Pattern Spotlight
DFS (Graph Connected Components)
When merging entities connected by shared attributes, model the problem as a graph and use DFS to find connected components, which represent merged groups.
Solution
| 1 | class Solution: |
| 2 | def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]: |
| 3 | graph = defaultdict(list) |
| 4 | email_to_name = {} |
| 5 | |
| 6 | for account in accounts: |
| 7 | name = account[0] |
| 8 | first_email = account[1] |
| 9 | |
| 10 | for email in account[1:]: |
| 11 | email_to_name[email] = name |
| 12 | graph[first_email].append(email) |
| 13 | graph[email].append(first_email) |
| 14 | |
| 15 | visited = set() |
| 16 | res = [] |
| 17 | |
| 18 | def dfs(email, component_emails): |
| 19 | visited.add(email) |
| 20 | component_emails.append(email) |
| 21 | |
| 22 | for neighbor in graph[email]: |
| 23 | if neighbor not in visited: |
| 24 | dfs(neighbor, component_emails) |
| 25 | |
| 26 | for email in graph: |
| 27 | if email not in visited: |
| 28 | component_emails = [] |
| 29 | dfs(email, component_emails) |
| 30 | |
| 31 | res.append([email_to_name[email]] + sorted(component_emails)) |
| 32 | |
| 33 | return res |
| 1 | import java.util.*; |
| 2 | |
| 3 | class Solution { |
| 4 | public List<List<String>> accountsMerge(List<List<String>> accounts) { |
| 5 | Map<String, List<String>> graph = new HashMap<>(); |
| 6 | Map<String, String> emailToName = new HashMap<>(); |
| 7 | |
| 8 | for (List<String> account : accounts) { |
| 9 | String name = account.get(0); |
| 10 | String firstEmail = account.get(1); |
| 11 | |
| 12 | for (int i = 1; i < account.size(); i++) { |
| 13 | String email = account.get(i); |
| 14 | emailToName.put(email, name); |
| 15 | graph.computeIfAbsent(firstEmail, k -> new ArrayList<>()).add(email); |
| 16 | graph.computeIfAbsent(email, k -> new ArrayList<>()).add(firstEmail); |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | Set<String> visited = new HashSet<>(); |
| 21 | List<List<String>> res = new ArrayList<>(); |
| 22 | |
| 23 | for (String email : graph.keySet()) { |
| 24 | if (!visited.contains(email)) { |
| 25 | List<String> componentEmails = new ArrayList<>(); |
| 26 | dfs(email, graph, visited, componentEmails); |
| 27 | Collections.sort(componentEmails); |
| 28 | List<String> account = new ArrayList<>(); |
| 29 | account.add(emailToName.get(email)); |
| 30 | account.addAll(componentEmails); |
| 31 | res.add(account); |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | return res; |
| 36 | } |
| 37 | |
| 38 | private void dfs(String email, Map<String, List<String>> graph, Set<String> visited, List<String> componentEmails) { |
| 39 | visited.add(email); |
| 40 | componentEmails.add(email); |
| 41 | |
| 42 | for (String neighbor : graph.get(email)) { |
| 43 | if (!visited.contains(neighbor)) { |
| 44 | dfs(neighbor, graph, visited, componentEmails); |
| 45 | } |
| 46 | } |
| 47 | } |
| 48 | } |
| 1 | #include <vector> |
| 2 | #include <string> |
| 3 | #include <unordered_map> |
| 4 | #include <unordered_set> |
| 5 | #include <map> |
| 6 | #include <algorithm> |
| 7 | |
| 8 | using namespace std; |
| 9 | |
| 10 | class Solution { |
| 11 | public: |
| 12 | vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) { |
| 13 | unordered_map<string, vector<string>> graph; |
| 14 | unordered_map<string, string> emailToName; |
| 15 | |
| 16 | for (const auto& account : accounts) { |
| 17 | const string& name = account[0]; |
| 18 | const string& firstEmail = account[1]; |
| 19 | |
| 20 | for (const string& email : account) { |
| 21 | if (&email == &account[0]) continue; // skip name |
| 22 | emailToName[email] = name; |
| 23 | graph[firstEmail].push_back(email); |
| 24 | graph[email].push_back(firstEmail); |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | unordered_set<string> visited; |
| 29 | vector<vector<string>> res; |
| 30 | |
| 31 | for (const auto& [email, _] : graph) { |
| 32 | if (visited.find(email) == visited.end()) { |
| 33 | vector<string> componentEmails; |
| 34 | dfs(email, graph, visited, componentEmails); |
| 35 | sort(componentEmails.begin(), componentEmails.end()); |
| 36 | vector<string> account; |
| 37 | account.push_back(emailToName[email]); |
| 38 | account.insert(account.end(), componentEmails.begin(), componentEmails.end()); |
| 39 | res.push_back(account); |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | return res; |
| 44 | } |
| 45 | |
| 46 | private: |
| 47 | void dfs(const string& email, unordered_map<string, vector<string>>& graph, unordered_set<string>& visited, vector<string>& componentEmails) { |
| 48 | visited.insert(email); |
| 49 | componentEmails.push_back(email); |
| 50 | |
| 51 | for (const string& neighbor : graph[email]) { |
| 52 | if (visited.find(neighbor) == visited.end()) { |
| 53 | dfs(neighbor, graph, visited, componentEmails); |
| 54 | } |
| 55 | } |
| 56 | } |
| 57 | }; |
| 1 | var accountsMerge = function(accounts) { |
| 2 | const graph = new Map(); |
| 3 | const emailToName = new Map(); |
| 4 | |
| 5 | for (const account of accounts) { |
| 6 | const name = account[0]; |
| 7 | const firstEmail = account[1]; |
| 8 | |
| 9 | for (const email of account.slice(1)) { |
| 10 | emailToName.set(email, name); |
| 11 | if (!graph.has(firstEmail)) graph.set(firstEmail, []); |
| 12 | if (!graph.has(email)) graph.set(email, []); |
| 13 | graph.get(firstEmail).push(email); |
| 14 | graph.get(email).push(firstEmail); |
| 15 | } |
| 16 | } |
| 17 | |
| 18 | const visited = new Set(); |
| 19 | const res = []; |
| 20 | |
| 21 | function dfs(email, componentEmails) { |
| 22 | visited.add(email); |
| 23 | componentEmails.push(email); |
| 24 | |
| 25 | for (const neighbor of graph.get(email)) { |
| 26 | if (!visited.has(neighbor)) { |
| 27 | dfs(neighbor, componentEmails); |
| 28 | } |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | for (const email of graph.keys()) { |
| 33 | if (!visited.has(email)) { |
| 34 | const componentEmails = []; |
| 35 | dfs(email, componentEmails); |
| 36 | componentEmails.sort(); |
| 37 | res.push([emailToName.get(email), ...componentEmails]); |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | return res; |
| 42 | }; |
Step-by-Step Solution
Build Email Graph and Map Emails to Names
| 3 | graph = defaultdict(list) |
| 4 | email_to_name = {} |
| 6 | for account in accounts: |
| 7 | name = account[0] |
| 8 | first_email = account[1] |
| 10 | for email in account[1:]: |
| 11 | email_to_name[email] = name |
| 12 | graph[first_email].append(email) |
| 13 | graph[email].append(first_email) |
Objective
To construct an undirected graph connecting emails that appear in the same account and map each email to its account name.
Key Insight
By connecting all emails in an account to the first email, the graph encodes all relationships between emails belonging to the same user. The email-to-name map allows retrieving the user's name after grouping emails. This graph representation transforms the merging problem into a connected components problem.
Interview Quick-Check
Core Logic
Connecting each email to the first email in the account creates edges that link all emails of the same user, enabling DFS to find connected components.
State & Boundaries
The email_to_name map stores the name for each email, ensuring the correct name is associated with merged emails.
Common Pitfalls & Bugs
Forgetting to add edges in both directions (undirected graph) can cause incomplete traversal during DFS.
Traverse Graph with DFS to Collect Connected Emails
| 15 | visited = set() |
| 16 | res = [] |
| 18 | def dfs(email, component_emails): |
| 19 | visited.add(email) |
| 20 | component_emails.append(email) |
| 22 | for neighbor in graph[email]: |
| 23 | if neighbor not in visited: |
| 24 | dfs(neighbor, component_emails) |
Objective
To perform DFS on each unvisited email to find all connected emails forming one merged account.
Key Insight
DFS explores all emails reachable from a starting email, collecting them into a component. This ensures all emails belonging to the same user are grouped together. Using a visited set prevents revisiting emails and infinite loops.
Interview Quick-Check
Core Logic
DFS recursively visits all neighbors of an email, collecting connected emails into a single component representing one merged account.
State & Boundaries
The visited set tracks emails already processed to avoid redundant work and cycles.
Common Pitfalls & Bugs
Not marking emails as visited before recursion can cause infinite recursion or duplicate processing.
Aggregate and Sort Emails for Each Merged Account
| 26 | for email in graph: |
| 27 | if email not in visited: |
| 28 | component_emails = [] |
| 29 | dfs(email, component_emails) |
| 31 | res.append([email_to_name[email]] + sorted(component_emails)) |
Objective
To collect the connected emails from DFS, sort them lexicographically, prepend the account name, and add to the result list.
Key Insight
Sorting emails ensures the output meets problem requirements. Prepending the name from the email_to_name map associates the merged emails with the correct user. Appending to the result list accumulates all merged accounts for final output.
Interview Quick-Check
Core Logic
Sorting the collected emails guarantees the output format is correct and consistent.
State & Boundaries
Using the email_to_name map with any email in the component retrieves the correct account name.
Common Pitfalls & Bugs
Appending the name before sorting or failing to sort emails can cause incorrect output order.
Return the List of Merged Accounts
| 33 | return res |
Objective
To return the final list of merged accounts after processing all connected components.
Key Insight
Returning the accumulated result completes the algorithm, providing the merged accounts as required by the problem.
Interview Quick-Check
Core Logic
Returning the result list after all DFS traversals ensures all accounts are merged and included.
Line Analysis
This solution has 5 Critical lines interviewers watch for.
graph[first_email].append(email)
Add an edge from the first email to the current email in the graph.
Connecting the first email to all others in the account creates edges that link emails belonging to the same user.
graph[email].append(first_email)
Add an edge from the current email back to the first email to maintain an undirected graph.
Adding edges in both directions ensures the graph is undirected, which is necessary for correct DFS traversal of connected components.
visited.add(email)
Mark the current email as visited.
Marking emails as visited before recursion prevents cycles and duplicate visits, ensuring correctness and efficiency.
dfs(neighbor, component_emails)
Recursively call DFS on the unvisited neighbor email.
Recursion explores the full connected component by visiting all reachable emails.
dfs(email, component_emails)
Perform DFS starting from the current unvisited email to collect connected emails.
This call discovers all emails connected to the starting email, representing one merged account.
graph = defaultdict(list)
Initialize a graph as a default dictionary of lists to store email connections.
This data structure efficiently represents the undirected graph where each email maps to its connected emails, enabling quick adjacency lookups.
visited = set()
Initialize a set to track visited emails during DFS.
The visited set prevents revisiting emails, avoiding infinite loops and redundant processing during DFS.
def dfs(email, component_emails):
Define a recursive DFS function to explore connected emails.
DFS is the core traversal method to find all emails connected to a given email, representing one merged account.
if neighbor not in visited:
Check if the neighbor email has not been visited.
Visiting only unvisited neighbors prevents infinite recursion and redundant work.
if email not in visited:
Check if the current email has not been visited.
Starting DFS only from unvisited emails avoids redundant traversals of already processed components.
res.append([email_to_name[email]] + sorted(component_emails))
Append the merged account to the result list with the name and sorted emails.
Sorting emails and prepending the name formats the merged account correctly for output, fulfilling problem requirements.
email_to_name = {}
Initialize a dictionary to map each email to its account name.
Mapping emails to names allows retrieval of the correct user name after grouping emails, which is essential for constructing the final merged accounts.
first_email = account[1]
Identify the first email in the account to serve as a connection hub.
Using the first email as a hub simplifies graph construction by connecting all other emails in the account to it, ensuring connectivity.
email_to_name[email] = name
Map the current email to the account name.
This ensures that every email can be traced back to the correct user name for final output.
component_emails.append(email)
Add the current email to the component's email list.
Collecting emails during DFS builds the full set of emails belonging to one merged account.
for neighbor in graph[email]:
Iterate over all neighbors of the current email in the graph.
Exploring neighbors is essential to traverse the entire connected component of emails.
for email in graph:
Iterate over all emails in the graph to find unvisited starting points for DFS.
This loop ensures all connected components are found by starting DFS from each unvisited email.
for email in account[1:]:
Iterate over all emails in the current account starting from the first email.
This loop processes each email to build graph edges and map emails to names.
component_emails = []
Initialize a list to collect emails in the current connected component.
This list temporarily stores emails found by DFS before sorting and adding to results.
for account in accounts:
Iterate over each account in the input list.
Processing each account is necessary to build the graph and email-to-name mapping for all emails.
name = account[0]
Extract the account name from the first element of the account list.
The name is associated with all emails in the account and is needed for the email-to-name map.
res = []
Initialize a list to store the merged account results.
This list accumulates all merged accounts to be returned at the end.
return res
Return the list of merged accounts.
Returning the accumulated results completes the algorithm and provides the final answer.
Test Your Understanding
Why is DFS on the email graph the correct method to merge accounts?
Because emails connected by edges represent belonging to the same user, DFS finds all emails reachable from a starting email, effectively grouping all emails of one user into a connected component.
Related Problems
DFS pattern
Don't just read it. Drill it.
Reconstruct Accounts Merge from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.
Unlock the Accounts Merge drill