1101. The Earliest Moment When Everyone Become Friends
Problem Description
You have n
people in a social group, labeled from 0
to n - 1
. You're given an array logs
where each entry logs[i] = [timestamp_i, x_i, y_i]
indicates that person x_i
and person y_i
become friends at time timestamp_i
.
The friendship relationship has two important properties:
- Symmetric: If person
a
is friends with personb
, then personb
is also friends with persona
- Transitive through acquaintance: Person
a
is considered acquainted with personb
if:a
is directly friends withb
, ORa
is friends with someone who is acquainted withb
(friendship chains)
Your task is to find the earliest timestamp when every person becomes acquainted with every other person in the group. In other words, find the minimum time when all n
people form a single connected network through their friendship relationships.
If it's impossible for everyone to become acquainted (meaning the friendships never connect all people into one group), return -1
.
For example, if we have 4 people and the following logs:
- At time 1: person 0 and 1 become friends
- At time 2: person 1 and 2 become friends
- At time 3: person 2 and 3 become friends
Then at time 3, everyone is acquainted with everyone else through the chain of friendships, so the answer would be 3.
Intuition
The key insight is that we need to find when all people form a single connected component. This is a classic Union-Find (Disjoint Set Union) problem.
Think of the friendship network as a graph where each person is a node and each friendship is an edge. Initially, we have n
isolated nodes (each person is their own component). As friendships form over time, these separate components start merging together. We want to find the exact moment when all these components merge into one.
Since we want the earliest time this happens, we should process the friendships in chronological order. By sorting the logs by timestamp, we can simulate the friendship formation process as it happens over time.
The Union-Find data structure is perfect for this because it efficiently:
- Tracks which connected component each person belongs to (using the
find
operation) - Merges two components when a new friendship forms (using the
union
operation) - Allows us to count how many separate components remain
Starting with n
separate components, each time we successfully merge two different components (when two people from different groups become friends), we decrease our component count by 1. When we reach exactly 1 component, everyone is connected - that's our answer!
The beauty of this approach is that we don't need to check all pairs of people to see if they're connected. We just need to track when the number of separate friend groups reduces to 1. If we process all friendships and still have more than 1 component, it means not everyone can be acquainted, so we return -1
.
Learn more about Union Find and Sorting patterns.
Solution Approach
The solution implements the Union-Find algorithm with path compression to efficiently track and merge friend groups.
Step 1: Initialize the Union-Find Structure
p = list(range(n))
We create a parent array p
where initially each person is their own parent (representing n
separate components). p[i] = i
means person i
is the root of their own component.
Step 2: Sort the Logs by Timestamp
for t, x, y in sorted(logs):
We sort the logs in ascending order by timestamp to process friendships chronologically. This ensures we find the earliest possible time when everyone becomes connected.
Step 3: Find Operation with Path Compression
def find(x):
if p[x] != x:
p[x] = find(p[x])
return p[x]
The find
function returns the root representative of a person's component. Path compression (p[x] = find(p[x])
) optimizes future lookups by directly pointing each node to the root.
Step 4: Process Each Friendship
For each log entry [t, x, y]
:
- First check if
x
andy
are already in the same component usingfind(x) == find(y)
- If they're already connected, skip this friendship (continue to next)
- If they're in different components:
- Merge the components:
p[find(x)] = find(y)
(make one root point to the other) - Decrease the component count:
n -= 1
- Check if we've reached a single component:
if n == 1: return t
- Merge the components:
Step 5: Handle Disconnected Case
If we process all logs and n > 1
(multiple components remain), return -1
as not everyone can be acquainted.
The algorithm's efficiency comes from:
- Sorting:
O(m log m)
wherem
is the number of logs - Union-Find operations: Nearly
O(1)
amortized time per operation with path compression - Overall complexity:
O(m log m)
dominated by the sorting step
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with 6 people (labeled 0-5) and the following friendship logs:
logs = [[3, 0, 1], [7, 0, 2], [4, 3, 4], [6, 3, 5], [9, 2, 5]]
Initial State (before any friendships):
- Parent array:
p = [0, 1, 2, 3, 4, 5]
- Each person is their own parent (6 separate components)
- Component count:
n = 6
Step 1: Sort logs by timestamp
After sorting: [[3, 0, 1], [4, 3, 4], [6, 3, 5], [7, 0, 2], [9, 2, 5]]
Step 2: Process timestamp 3 - [3, 0, 1]
- Find(0) = 0, Find(1) = 1 (different components)
- Union: Set
p[0] = 1
(0's parent becomes 1) - Components:
{0,1}, {2}, {3}, {4}, {5}
- Count:
n = 5
Step 3: Process timestamp 4 - [4, 3, 4]
- Find(3) = 3, Find(4) = 4 (different components)
- Union: Set
p[3] = 4
(3's parent becomes 4) - Components:
{0,1}, {2}, {3,4}, {5}
- Count:
n = 4
Step 4: Process timestamp 6 - [6, 3, 5]
- Find(3) = 4 (trace: 3→4), Find(5) = 5 (different components)
- Union: Set
p[4] = 5
(4's parent becomes 5) - Components:
{0,1}, {2}, {3,4,5}
- Count:
n = 3
Step 5: Process timestamp 7 - [7, 0, 2]
- Find(0) = 1 (trace: 0→1), Find(2) = 2 (different components)
- Union: Set
p[1] = 2
(1's parent becomes 2) - Components:
{0,1,2}, {3,4,5}
- Count:
n = 2
Step 6: Process timestamp 9 - [9, 2, 5]
- Find(2) = 2, Find(5) = 5 (different components)
- Union: Set
p[2] = 5
(2's parent becomes 5) - Components:
{0,1,2,3,4,5}
(all connected!) - Count:
n = 1
- Return 9 (earliest time when everyone is acquainted)
The key insight: We tracked when separate friend groups merged. At timestamp 9, the two remaining groups {0,1,2}
and {3,4,5}
finally connected through the friendship between persons 2 and 5, creating one unified network where everyone is acquainted with everyone else.
Solution Implementation
1class Solution:
2 def earliestAcq(self, logs: List[List[int]], n: int) -> int:
3 """
4 Find the earliest time when all people become acquainted using Union-Find.
5
6 Args:
7 logs: List of [timestamp, person_x, person_y] representing friendships
8 n: Total number of people (0 to n-1)
9
10 Returns:
11 Earliest timestamp when all people are connected, or -1 if not possible
12 """
13
14 def find(person: int) -> int:
15 """
16 Find the root parent of a person with path compression.
17
18 Args:
19 person: The person whose root parent to find
20
21 Returns:
22 The root parent of the person
23 """
24 if parent[person] != person:
25 # Path compression: directly connect to root parent
26 parent[person] = find(parent[person])
27 return parent[person]
28
29 # Initialize parent array where each person is their own parent
30 parent = list(range(n))
31
32 # Number of separate groups (initially each person is a separate group)
33 num_groups = n
34
35 # Process logs in chronological order
36 for timestamp, person_x, person_y in sorted(logs):
37 # Find root parents of both people
38 root_x = find(person_x)
39 root_y = find(person_y)
40
41 # If already in the same group, skip
42 if root_x == root_y:
43 continue
44
45 # Union: merge the two groups by connecting their roots
46 parent[root_x] = root_y
47
48 # Decrease the number of separate groups
49 num_groups -= 1
50
51 # If all people are in one group, return the timestamp
52 if num_groups == 1:
53 return timestamp
54
55 # Not all people became acquainted
56 return -1
57
1class Solution {
2 // Parent array for Union-Find (Disjoint Set Union) data structure
3 private int[] parent;
4
5 /**
6 * Finds the earliest time when all people become acquainted.
7 *
8 * @param logs 2D array where each element is [timestamp, person1, person2]
9 * @param n Total number of people (labeled from 0 to n-1)
10 * @return The earliest timestamp when all people know each other, or -1 if impossible
11 */
12 public int earliestAcq(int[][] logs, int n) {
13 // Sort logs by timestamp in ascending order
14 Arrays.sort(logs, (a, b) -> a[0] - b[0]);
15
16 // Initialize parent array where each person is their own parent initially
17 parent = new int[n];
18 for (int i = 0; i < n; i++) {
19 parent[i] = i;
20 }
21
22 // Number of connected components (initially n separate groups)
23 int numComponents = n;
24
25 // Process each friendship log in chronological order
26 for (int[] log : logs) {
27 int timestamp = log[0];
28 int person1 = log[1];
29 int person2 = log[2];
30
31 // Find the root parents of both people
32 int root1 = find(person1);
33 int root2 = find(person2);
34
35 // If they already belong to the same group, skip
36 if (root1 == root2) {
37 continue;
38 }
39
40 // Union: merge the two groups by connecting their roots
41 parent[root1] = root2;
42
43 // Decrease the number of separate components
44 numComponents--;
45
46 // If all people are in one group, return current timestamp
47 if (numComponents == 1) {
48 return timestamp;
49 }
50 }
51
52 // Not all people became acquainted
53 return -1;
54 }
55
56 /**
57 * Find operation with path compression optimization.
58 * Finds the root parent of a given person.
59 *
60 * @param x The person whose root parent we want to find
61 * @return The root parent of person x
62 */
63 private int find(int x) {
64 // Path compression: make every node point directly to the root
65 if (parent[x] != x) {
66 parent[x] = find(parent[x]);
67 }
68 return parent[x];
69 }
70}
71
1class Solution {
2public:
3 int earliestAcq(vector<vector<int>>& logs, int n) {
4 // Sort logs by timestamp (first element of each log)
5 sort(logs.begin(), logs.end());
6
7 // Initialize parent array for Union-Find
8 // Each person is initially their own parent (separate groups)
9 vector<int> parent(n);
10 iota(parent.begin(), parent.end(), 0); // Fill with 0, 1, 2, ..., n-1
11
12 // Find function with path compression
13 // Returns the root parent of a given person
14 function<int(int)> find = [&](int person) {
15 if (parent[person] == person) {
16 return person;
17 }
18 // Path compression: directly connect to root parent
19 return parent[person] = find(parent[person]);
20 };
21
22 // Process each log in chronological order
23 for (auto& log : logs) {
24 int timestamp = log[0];
25 int person1 = log[1];
26 int person2 = log[2];
27
28 // Find root parents of both persons
29 int root1 = find(person1);
30 int root2 = find(person2);
31
32 // If they have different roots, they belong to different groups
33 if (root1 != root2) {
34 // Union: merge the two groups by connecting one root to another
35 parent[root1] = root2;
36 // Decrease the number of separate groups
37 --n;
38 }
39
40 // If only one group remains, everyone knows each other
41 if (n == 1) {
42 return timestamp;
43 }
44 }
45
46 // If we never reached a single group, return -1
47 return -1;
48 }
49};
50
1/**
2 * Finds the earliest time when all people become acquainted
3 * @param logs - Array of [timestamp, personX, personY] representing when two people become friends
4 * @param n - Total number of people (indexed from 0 to n-1)
5 * @returns The earliest timestamp when all people are connected, or -1 if not possible
6 */
7function earliestAcq(logs: number[][], n: number): number {
8 // Initialize parent array for Union-Find (Disjoint Set Union)
9 // Each person is initially their own parent (separate group)
10 const parent: number[] = Array(n)
11 .fill(0)
12 .map((_, index) => index);
13
14 /**
15 * Find operation with path compression
16 * Finds the root parent of a given person
17 * @param x - Person to find the root parent for
18 * @returns The root parent of person x
19 */
20 const find = (x: number): number => {
21 // Path compression: directly connect x to its root parent
22 if (parent[x] !== x) {
23 parent[x] = find(parent[x]);
24 }
25 return parent[x];
26 };
27
28 // Sort logs by timestamp in ascending order
29 logs.sort((a, b) => a[0] - b[0]);
30
31 // Process each friendship log in chronological order
32 for (const [timestamp, personX, personY] of logs) {
33 // Find root parents of both people
34 const rootX = find(personX);
35 const rootY = find(personY);
36
37 // If they have different roots, they belong to different groups
38 if (rootX !== rootY) {
39 // Union operation: merge the two groups
40 parent[rootX] = rootY;
41
42 // Decrease the number of separate groups
43 // When n becomes 1, all people are in one group (all acquainted)
44 if (--n === 1) {
45 return timestamp;
46 }
47 }
48 }
49
50 // If we've processed all logs and still have multiple groups, return -1
51 return -1;
52}
53
Time and Space Complexity
The time complexity is O(m × log m + m × α(n))
, where m
is the number of logs and n
is the number of people. The O(m × log m)
comes from sorting the logs array, and O(m × α(n))
comes from performing at most m
union-find operations, where α(n)
is the inverse Ackermann function (practically constant). Since α(n)
is effectively constant for all practical values, this simplifies to O(m × log m)
.
The space complexity is O(n)
for storing the parent array p
of size n
, which represents the disjoint set union (DSU) data structure. The sorting operation uses O(log m)
space for the recursion stack in typical implementations, but since n ≤ m
in most cases, the dominant space usage is O(n)
.
Note: The reference answer uses n
to denote the number of logs, which creates confusion with the parameter n
in the code that represents the number of people. In the code, n
is the number of people, and the logs array has length m
. The reference answer's notation would make the time complexity O(n × log n)
and space complexity O(n)
where this n
refers to the length of the logs array.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Sort the Logs
One of the most common mistakes is processing the logs in the given order instead of sorting them by timestamp first. This would lead to incorrect results since we need to find the earliest time when everyone becomes connected.
Wrong approach:
# Processing logs without sorting for timestamp, person_x, person_y in logs: # Wrong! # Union-find operations...
Correct approach:
# Sort logs by timestamp first
for timestamp, person_x, person_y in sorted(logs):
# Union-find operations...
2. Incorrect Union Operation
Another frequent error is performing the union operation on the persons directly instead of their root parents. This breaks the Union-Find structure and leads to incorrect component tracking.
Wrong approach:
# Directly connecting persons instead of roots if find(person_x) != find(person_y): parent[person_x] = person_y # Wrong! num_groups -= 1
Correct approach:
# Connect the root parents root_x = find(person_x) root_y = find(person_y) if root_x != root_y: parent[root_x] = root_y # Correct! num_groups -= 1
3. Not Implementing Path Compression
While the solution would still be correct without path compression, omitting it can lead to performance issues with large inputs. The find operation could degrade to O(n) in the worst case, making the overall complexity O(m*n) instead of the optimal O(m log m).
Inefficient approach:
def find(person):
while parent[person] != person:
person = parent[person]
return person
Optimized approach with path compression:
def find(person):
if parent[person] != person:
parent[person] = find(parent[person]) # Path compression
return parent[person]
4. Off-by-One Error in Component Counting
Some implementations might incorrectly initialize or update the component count, leading to premature or delayed return of the result.
Common mistakes:
- Starting with
num_groups = 0
instead ofnum_groups = n
- Checking
if num_groups == 0
instead ofif num_groups == 1
- Decrementing the counter even when the persons are already connected
Correct implementation:
num_groups = n # Start with n separate groups
for timestamp, person_x, person_y in sorted(logs):
root_x = find(person_x)
root_y = find(person_y)
if root_x != root_y: # Only decrement when actually merging
parent[root_x] = root_y
num_groups -= 1
if num_groups == 1: # Check for exactly 1 group
return timestamp
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
Recommended Readings
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!