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 person b, then person b is also friends with person a
  • Transitive through acquaintance: Person a is considered acquainted with person b if:
    • a is directly friends with b, OR
    • a is friends with someone who is acquainted with b (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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Tracks which connected component each person belongs to (using the find operation)
  2. Merges two components when a new friendship forms (using the union operation)
  3. 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 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

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 and y are already in the same component using find(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

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) where m 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 Evaluator

Example 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.

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 of num_groups = n
  • Checking if num_groups == 0 instead of if 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
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

You are given an array of intervals where intervals[i] = [start_i, end_i] represent the start and end of the ith interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More