Facebook Pixel

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

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 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

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

Load More