🧠
Union-Find with Path Compression and Optimized Grouping
💡 This approach improves the previous union-find by using path compression and sorting emails only once, optimizing runtime and memory usage.
Intuition
Use union-find with path compression to flatten trees, then group emails by root. Sorting is done once per group for output.
Algorithm
- Initialize union-find with path compression.
- Map emails to names and union emails in the same account.
- Find root parent for each email and group emails by root.
- Sort each group of emails once.
- Return merged accounts with name and sorted emails.
💡 Path compression drastically reduces find operation time, making union-find near constant time.
from typing import List
class UnionFind:
def __init__(self):
self.parent = {}
self.rank = {}
def find(self, x):
if x != self.parent.setdefault(x, x):
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
rootX = self.find(x)
rootY = self.find(y)
if rootX != rootY:
rankX = self.rank.setdefault(rootX, 0)
rankY = self.rank.setdefault(rootY, 0)
if rankX < rankY:
self.parent[rootX] = rootY
elif rankX > rankY:
self.parent[rootY] = rootX
else:
self.parent[rootY] = rootX
self.rank[rootX] += 1
def accountsMerge(accounts: List[List[str]]) -> List[List[str]]:
uf = UnionFind()
email_to_name = {}
for account in accounts:
name = account[0]
first_email = account[1]
for email in account[1:]:
email_to_name[email] = name
uf.union(first_email, email)
unions = {}
for email in email_to_name:
root = uf.find(email)
unions.setdefault(root, []).append(email)
merged = []
for root, emails in unions.items():
merged.append([email_to_name[root]] + sorted(emails))
return merged
# Driver code
if __name__ == '__main__':
input_accounts = [["John", "johnsmith@mail.com", "john00@mail.com"],
["John", "johnnybravo@mail.com"],
["John", "johnsmith@mail.com", "john_newyork@mail.com"],
["Mary", "mary@mail.com"]]
print(accountsMerge(input_accounts))
Line Notes
self.rank = {}Track tree depth to optimize union by rank and keep trees shallow
rankX = self.rank.setdefault(rootX, 0)Get rank or initialize to zero for rootX
if rankX < rankY:Attach smaller tree under larger to keep tree shallow and optimize find
unions.setdefault(root, []).append(email)Group emails by root parent efficiently for merging
import java.util.*;
class UnionFind {
private Map<String, String> parent = new HashMap<>();
private Map<String, Integer> rank = new HashMap<>();
public String find(String x) {
parent.putIfAbsent(x, x);
if (!parent.get(x).equals(x)) {
parent.put(x, find(parent.get(x)));
}
return parent.get(x);
}
public void union(String x, String y) {
String rootX = find(x);
String rootY = find(y);
if (!rootX.equals(rootY)) {
int rankX = rank.getOrDefault(rootX, 0);
int rankY = rank.getOrDefault(rootY, 0);
if (rankX < rankY) {
parent.put(rootX, rootY);
} else if (rankX > rankY) {
parent.put(rootY, rootX);
} else {
parent.put(rootY, rootX);
rank.put(rootX, rankX + 1);
}
}
}
}
public class AccountsMergeUFOptimized {
public static List<List<String>> accountsMerge(List<List<String>> accounts) {
UnionFind uf = new UnionFind();
Map<String, String> emailToName = new HashMap<>();
for (List<String> account : accounts) {
String name = account.get(0);
String firstEmail = account.get(1);
for (int i = 1; i < account.size(); i++) {
String email = account.get(i);
emailToName.put(email, name);
uf.union(firstEmail, email);
}
}
Map<String, List<String>> unions = new HashMap<>();
for (String email : emailToName.keySet()) {
String root = uf.find(email);
unions.computeIfAbsent(root, k -> new ArrayList<>()).add(email);
}
List<List<String>> merged = new ArrayList<>();
for (Map.Entry<String, List<String>> entry : unions.entrySet()) {
List<String> account = entry.getValue();
Collections.sort(account);
account.add(0, emailToName.get(entry.getKey()));
merged.add(account);
}
return merged;
}
public static void main(String[] args) {
List<List<String>> accounts = Arrays.asList(
Arrays.asList("John", "johnsmith@mail.com", "john00@mail.com"),
Arrays.asList("John", "johnnybravo@mail.com"),
Arrays.asList("John", "johnsmith@mail.com", "john_newyork@mail.com"),
Arrays.asList("Mary", "mary@mail.com")
);
System.out.println(accountsMerge(accounts));
}
}
Line Notes
private Map<String, Integer> rank = new HashMap<>();Store rank to optimize union operations and keep trees balanced
if (!rootX.equals(rootY)) {Only union if roots differ to avoid cycles and redundant unions
unions.computeIfAbsent(root, k -> new ArrayList<>()).add(email);Group emails by root efficiently for merging
Collections.sort(account);Sort emails before adding name at front for consistent output
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <map>
#include <algorithm>
using namespace std;
class UnionFind {
unordered_map<string, string> parent;
unordered_map<string, int> rank;
public:
string find(const string& x) {
if (parent.find(x) == parent.end()) parent[x] = x;
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unite(const string& x, const string& y) {
string rootX = find(x);
string rootY = find(y);
if (rootX != rootY) {
int rankX = rank[rootX];
int rankY = rank[rootY];
if (rankX < rankY) {
parent[rootX] = rootY;
} else if (rankX > rankY) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
};
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
UnionFind uf;
unordered_map<string, string> emailToName;
for (auto& account : accounts) {
string name = account[0];
string firstEmail = account[1];
for (int i = 1; i < (int)account.size(); i++) {
emailToName[account[i]] = name;
uf.unite(firstEmail, account[i]);
}
}
map<string, vector<string>> unions;
for (auto& [email, name] : emailToName) {
string root = uf.find(email);
unions[root].push_back(email);
}
vector<vector<string>> merged;
for (auto& [root, emails] : unions) {
sort(emails.begin(), emails.end());
emails.insert(emails.begin(), emailToName[root]);
merged.push_back(emails);
}
return merged;
}
int main() {
vector<vector<string>> accounts = {
{"John", "johnsmith@mail.com", "john00@mail.com"},
{"John", "johnnybravo@mail.com"},
{"John", "johnsmith@mail.com", "john_newyork@mail.com"},
{"Mary", "mary@mail.com"}
};
vector<vector<string>> result = accountsMerge(accounts);
for (auto& acc : result) {
for (auto& s : acc) cout << s << " ";
cout << endl;
}
return 0;
}
Line Notes
unordered_map<string, int> rank;Track rank to optimize union and keep trees balanced
if (rootX != rootY) {Only union different sets to avoid cycles and redundant unions
map<string, vector<string>> unions;Use map to group emails by root and keep sorted order
sort(emails.begin(), emails.end());Sort emails before adding name for consistent output
class UnionFind {
constructor() {
this.parent = new Map();
this.rank = new Map();
}
find(x) {
if (!this.parent.has(x)) this.parent.set(x, x);
if (this.parent.get(x) !== x) {
this.parent.set(x, this.find(this.parent.get(x)));
}
return this.parent.get(x);
}
union(x, y) {
let rootX = this.find(x);
let rootY = this.find(y);
if (rootX !== rootY) {
let rankX = this.rank.get(rootX) || 0;
let rankY = this.rank.get(rootY) || 0;
if (rankX < rankY) {
this.parent.set(rootX, rootY);
} else if (rankX > rankY) {
this.parent.set(rootY, rootX);
} else {
this.parent.set(rootY, rootX);
this.rank.set(rootX, rankX + 1);
}
}
}
}
function accountsMerge(accounts) {
const uf = new UnionFind();
const emailToName = new Map();
for (const account of accounts) {
const name = account[0];
const firstEmail = account[1];
for (const email of account.slice(1)) {
emailToName.set(email, name);
uf.union(firstEmail, email);
}
}
const unions = new Map();
for (const email of emailToName.keys()) {
const root = uf.find(email);
if (!unions.has(root)) unions.set(root, []);
unions.get(root).push(email);
}
const merged = [];
for (const [root, emails] of unions.entries()) {
emails.sort();
emails.unshift(emailToName.get(root));
merged.push(emails);
}
return merged;
}
// Driver code
const inputAccounts = [
["John", "johnsmith@mail.com", "john00@mail.com"],
["John", "johnnybravo@mail.com"],
["John", "johnsmith@mail.com", "john_newyork@mail.com"],
["Mary", "mary@mail.com"]
];
console.log(accountsMerge(inputAccounts));
Line Notes
this.rank = new Map();Store rank to optimize union and keep trees balanced
if (rootX !== rootY) {Only union different sets to avoid cycles and redundant unions
unions.get(root).push(email);Group emails by root parent for merging
emails.sort();Sort emails before adding name for consistent output
TimeO(N * M * α(N*M))
SpaceO(N * M)
Path compression and union by rank optimize union-find to near constant time per operation.
💡 This approach is the most efficient and practical for large datasets, making it ideal for interviews.
Interview Verdict: Accepted
This is the optimal union-find solution expected in interviews for this problem.