2076. Process Restricted Friend Requests
Problem Description
You have a network of n
people labeled from 0
to n - 1
. Initially, no one is friends with each other.
You're given two lists:
-
Restrictions: A list of pairs
[xi, yi]
indicating that personxi
and personyi
cannot become friends, either directly or indirectly through other people. This means ifxi
becomes friends with someone who is friends withyi
(through any chain of friendships), it violates the restriction. -
Requests: A list of friend requests
[uj, vj]
that need to be processed in order.
For each friend request, you need to determine if it can be accepted:
- A request is successful if accepting it doesn't violate any restriction
- If two people are already friends (directly or through a chain), the request is still considered successful
- When a request is successful, the two people become direct friends
The key challenge is that when two people become friends, they join each other's friend groups. This means all friends of person u
become indirectly connected to all friends of person v
. You must ensure this merger doesn't cause any pair from the restrictions list to become connected.
For example, if person 0 and person 1 are restricted from being friends, and person 0 is already friends with person 2, then a friend request between person 1 and person 2 cannot be accepted because it would indirectly connect person 0 and person 1.
You need to return a boolean array where each element indicates whether the corresponding friend request was successful (true
) or not (false
).
Intuition
The core insight is that we need to track connected components of friends, as friendship is transitive - if A is friends with B, and B is friends with C, then A and C are in the same friend group.
This immediately suggests using a Union-Find (Disjoint Set Union) data structure, which efficiently manages connected components and can quickly tell us if two people are in the same group.
For each friend request between persons u
and v
, we face two scenarios:
-
They're already in the same friend group: The request is automatically successful since they're already connected through some chain of friendships.
-
They're in different friend groups: This is where we need to be careful. Accepting this request would merge two entire friend groups together. We must check if this merger would violate any restrictions.
The key realization is that when we merge groups containing u
and v
, every person in u
's group becomes connected to every person in v
's group. So for each restriction [x, y]
, we need to verify that we're not about to connect them.
How do we check this? For each restriction [x, y]
, we find which groups x
and y
belong to (using the find operation). If x
is in the same group as u
and y
is in the same group as v
(or vice versa), then accepting the friend request would connect x
and y
, violating the restriction.
If no restrictions are violated, we can safely merge the two groups by making one group's root point to the other's root (p[pu] = pv
), effectively combining all members of both groups into one larger friend group.
This approach works because Union-Find gives us constant-time group identification (with path compression) and efficient merging of groups, while we only need to check each restriction once per request.
Learn more about Union Find and Graph patterns.
Solution Approach
The solution uses the Union-Find data structure to efficiently manage friend groups and check restrictions.
Initialization:
- Create a parent array
p
wherep[i] = i
initially, meaning each person is their own representative (root of their group) - Initialize an empty answer list to store results for each request
Find Operation with Path Compression:
def find(x: int) -> int:
if p[x] != x:
p[x] = find(p[x]) # Path compression
return p[x]
This function finds the root representative of a person's group. Path compression optimizes future lookups by making nodes point directly to the root.
Processing Each Friend Request [u, v]
:
-
Find the roots of both people:
pu, pv = find(u), find(v)
-
Check if already friends:
if pu == pv: ans.append(True)
If they have the same root, they're already in the same friend group, so the request is automatically successful.
-
Check restrictions if they're in different groups:
- Initialize
ok = True
to track if the request can be accepted - For each restriction
[x, y]
:px, py = find(x), find(y) if (pu == px and pv == py) or (pu == py and pv == px): ok = False break
- This checks if accepting the request would connect restricted pairs:
pu == px and pv == py
: Personx
is inu
's group and persony
is inv
's grouppu == py and pv == px
: Persony
is inu
's group and personx
is inv
's group
- Either condition means merging the groups would violate the restriction
- Initialize
-
Union operation if no restrictions are violated:
if ok: p[pu] = pv
This merges the two groups by making one root point to the other.
-
Record the result:
ans.append(ok)
Time Complexity:
- For each request:
O(r)
wherer
is the number of restrictions (we check all restrictions in worst case) - With path compression, find operations are nearly
O(1)
amortized - Total:
O(m * r)
wherem
is the number of requests
Space Complexity: O(n)
for the parent array
The elegance of this solution lies in how Union-Find naturally handles the transitive nature of friendships while making it easy to check if a merger would violate restrictions.
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 to illustrate how the solution works.
Given:
n = 5
(people labeled 0 to 4)- Restrictions:
[[0, 1], [1, 2], [3, 4]]
- Requests:
[[0, 4], [1, 3], [3, 2]]
Initial State:
- Parent array:
p = [0, 1, 2, 3, 4]
(everyone is their own root) - Each person is in their own group
Processing Request 1: [0, 4]
- Find roots:
find(0) = 0
,find(4) = 4
(different groups) - Check restrictions:
[0, 1]
:find(0) = 0
,find(1) = 1
- Is
(0 == 0 and 4 == 1)
or(0 == 1 and 4 == 0)
? No ✓
- Is
[1, 2]
:find(1) = 1
,find(2) = 2
- Is
(0 == 1 and 4 == 2)
or(0 == 2 and 4 == 1)
? No ✓
- Is
[3, 4]
:find(3) = 3
,find(4) = 4
- Is
(0 == 3 and 4 == 4)
or(0 == 4 and 4 == 3)
? No ✓
- Is
- No violations found, so merge:
p[0] = 4
- Parent array:
p = [4, 1, 2, 3, 4]
- Groups: {0, 4}, {1}, {2}, {3}
- Result:
true
Processing Request 2: [1, 3]
- Find roots:
find(1) = 1
,find(3) = 3
(different groups) - Check restrictions:
[0, 1]
:find(0) = 4
,find(1) = 1
- Is
(1 == 4 and 3 == 1)
or(1 == 1 and 3 == 4)
? No ✓
- Is
[1, 2]
:find(1) = 1
,find(2) = 2
- Is
(1 == 1 and 3 == 2)
or(1 == 2 and 3 == 1)
? No ✓
- Is
[3, 4]
:find(3) = 3
,find(4) = 4
- Is
(1 == 3 and 3 == 4)
or(1 == 4 and 3 == 3)
? No ✓
- Is
- No violations found, so merge:
p[1] = 3
- Parent array:
p = [4, 3, 2, 3, 4]
- Groups: {0, 4}, {1, 3}, {2}
- Result:
true
Processing Request 3: [3, 2]
- Find roots:
find(3) = 3
,find(2) = 2
(different groups) - Check restrictions:
[0, 1]
:find(0) = 4
,find(1) = 3
- Is
(3 == 4 and 2 == 3)
or(3 == 3 and 2 == 4)
? No ✓
- Is
[1, 2]
:find(1) = 3
,find(2) = 2
- Is
(3 == 3 and 2 == 2)
or(3 == 2 and 2 == 3)
? - First condition is TRUE! (3 == 3 and 2 == 2) ✗
- Is
- Violation detected! Person 1 (in group with root 3) would connect to person 2
- Cannot merge these groups
- Result:
false
Final Answer: [true, true, false]
The key insight: When checking request [3, 2], we found that person 1 is already in person 3's group (from the previous request). Accepting this request would put person 2 in the same group, violating the restriction [1, 2] that says persons 1 and 2 cannot be friends directly or indirectly.
Solution Implementation
1class Solution:
2 def friendRequests(
3 self, n: int, restrictions: List[List[int]], requests: List[List[int]]
4 ) -> List[bool]:
5 """
6 Process friend requests with restrictions using Union-Find.
7
8 Args:
9 n: Number of people (0 to n-1)
10 restrictions: List of pairs that cannot be in the same friend group
11 requests: List of friend request pairs to process
12
13 Returns:
14 List of booleans indicating whether each request was approved
15 """
16
17 def find(node: int) -> int:
18 """
19 Find the root parent of a node with path compression.
20
21 Args:
22 node: The node to find the root for
23
24 Returns:
25 The root parent of the node
26 """
27 if parent[node] != node:
28 # Path compression: make node point directly to root
29 parent[node] = find(parent[node])
30 return parent[node]
31
32 # Initialize parent array where each person is their own parent initially
33 parent = list(range(n))
34
35 # Store results for each request
36 result = []
37
38 # Process each friend request
39 for person_u, person_v in requests:
40 # Find the root parents of both people
41 parent_u = find(person_u)
42 parent_v = find(person_v)
43
44 if parent_u == parent_v:
45 # Already in the same friend group, request automatically approved
46 result.append(True)
47 else:
48 # Check if merging these groups would violate any restriction
49 is_valid = True
50
51 for restricted_x, restricted_y in restrictions:
52 # Find parents of the restricted pair
53 parent_x = find(restricted_x)
54 parent_y = find(restricted_y)
55
56 # Check if merging would put restricted pair in same group
57 if (parent_u == parent_x and parent_v == parent_y) or \
58 (parent_u == parent_y and parent_v == parent_x):
59 is_valid = False
60 break
61
62 result.append(is_valid)
63
64 # If valid, perform the union operation
65 if is_valid:
66 parent[parent_u] = parent_v
67
68 return result
69
1class Solution {
2 // Parent array for Union-Find data structure
3 private int[] parent;
4
5 /**
6 * Process friend requests and determine which can be accepted based on restrictions.
7 * @param n The number of people (nodes 0 to n-1)
8 * @param restrictions Array of restricted pairs that cannot be in the same friend group
9 * @param requests Array of friend requests to process
10 * @return Boolean array indicating whether each request can be accepted
11 */
12 public boolean[] friendRequests(int n, int[][] restrictions, int[][] requests) {
13 // Initialize Union-Find structure with each person as their own parent
14 parent = new int[n];
15 for (int i = 0; i < n; i++) {
16 parent[i] = i;
17 }
18
19 int requestCount = requests.length;
20 boolean[] result = new boolean[requestCount];
21
22 // Process each friend request sequentially
23 for (int i = 0; i < requestCount; i++) {
24 int person1 = requests[i][0];
25 int person2 = requests[i][1];
26
27 // Find the root representatives of both people
28 int root1 = find(person1);
29 int root2 = find(person2);
30
31 // If already in the same friend group, request can be accepted
32 if (root1 == root2) {
33 result[i] = true;
34 } else {
35 // Check if merging these groups would violate any restriction
36 boolean canMerge = true;
37
38 for (int[] restriction : restrictions) {
39 int restrictedPerson1 = restriction[0];
40 int restrictedPerson2 = restriction[1];
41
42 // Find roots of the restricted pair
43 int restrictedRoot1 = find(restrictedPerson1);
44 int restrictedRoot2 = find(restrictedPerson2);
45
46 // Check if merging would connect the restricted pair
47 if ((root1 == restrictedRoot1 && root2 == restrictedRoot2) ||
48 (root1 == restrictedRoot2 && root2 == restrictedRoot1)) {
49 canMerge = false;
50 break;
51 }
52 }
53
54 // If no restrictions are violated, accept the request and merge groups
55 if (canMerge) {
56 result[i] = true;
57 parent[root1] = root2; // Union operation: merge the two groups
58 }
59 }
60 }
61
62 return result;
63 }
64
65 /**
66 * Find operation with path compression for Union-Find.
67 * @param x The node to find the root for
68 * @return The root representative of the node's group
69 */
70 private int find(int x) {
71 // Path compression: make all nodes point directly to root
72 if (parent[x] != x) {
73 parent[x] = find(parent[x]);
74 }
75 return parent[x];
76 }
77}
78
1class Solution {
2public:
3 vector<bool> friendRequests(int n, vector<vector<int>>& restrictions, vector<vector<int>>& requests) {
4 // Initialize parent array for Union-Find data structure
5 // Each person is initially their own parent (separate groups)
6 vector<int> parent(n);
7 iota(parent.begin(), parent.end(), 0);
8
9 // Find function with path compression
10 // Returns the root parent of a given node
11 function<int(int)> find = [&](int x) {
12 if (parent[x] != x) {
13 parent[x] = find(parent[x]); // Path compression
14 }
15 return parent[x];
16 };
17
18 // Result vector to store whether each request can be approved
19 vector<bool> result;
20
21 // Process each friend request
22 for (auto& request : requests) {
23 int person1 = request[0];
24 int person2 = request[1];
25
26 // Find the root parents of both persons
27 int root1 = find(person1);
28 int root2 = find(person2);
29
30 // If they're already in the same group, the request is automatically approved
31 if (root1 == root2) {
32 result.push_back(true);
33 } else {
34 // Check if merging these two groups would violate any restrictions
35 bool canMerge = true;
36
37 for (auto& restriction : restrictions) {
38 int restrictedRoot1 = find(restriction[0]);
39 int restrictedRoot2 = find(restriction[1]);
40
41 // Check if merging would connect restricted pairs
42 // This happens when one restricted person is in group1 and the other in group2
43 if ((root1 == restrictedRoot1 && root2 == restrictedRoot2) ||
44 (root1 == restrictedRoot2 && root2 == restrictedRoot1)) {
45 canMerge = false;
46 break;
47 }
48 }
49
50 result.push_back(canMerge);
51
52 // If the merge is allowed, unite the two groups
53 if (canMerge) {
54 parent[root1] = root2; // Union operation
55 }
56 }
57 }
58
59 return result;
60 }
61};
62
1/**
2 * Process friend requests with restrictions using Union-Find data structure
3 * @param n - Number of people (indexed from 0 to n-1)
4 * @param restrictions - Array of pairs [x, y] where x and y cannot be friends (directly or indirectly)
5 * @param requests - Array of friend requests [u, v] to process in order
6 * @returns Array of booleans indicating whether each request was successful
7 */
8function friendRequests(n: number, restrictions: number[][], requests: number[][]): boolean[] {
9 // Initialize parent array for Union-Find structure
10 // Each person initially belongs to their own group
11 const parent: number[] = Array.from({ length: n }, (_, index) => index);
12
13 /**
14 * Find the root parent of a node with path compression
15 * @param node - The node to find the root parent for
16 * @returns The root parent of the node
17 */
18 const findRoot = (node: number): number => {
19 if (parent[node] !== node) {
20 // Path compression: directly connect node to its root
21 parent[node] = findRoot(parent[node]);
22 }
23 return parent[node];
24 };
25
26 // Array to store the results for each request
27 const results: boolean[] = [];
28
29 // Process each friend request
30 for (const [user1, user2] of requests) {
31 // Find the root parents of both users
32 const root1 = findRoot(user1);
33 const root2 = findRoot(user2);
34
35 // If both users are already in the same group, the request is automatically successful
36 if (root1 === root2) {
37 results.push(true);
38 } else {
39 // Check if merging these two groups would violate any restrictions
40 let isValid = true;
41
42 for (const [restrictedUser1, restrictedUser2] of restrictions) {
43 // Find the root parents of the restricted users
44 const restrictedRoot1 = findRoot(restrictedUser1);
45 const restrictedRoot2 = findRoot(restrictedUser2);
46
47 // Check if merging would connect restricted users
48 // This happens when one restricted user's group matches user1's group
49 // and the other restricted user's group matches user2's group (or vice versa)
50 if ((restrictedRoot1 === root1 && restrictedRoot2 === root2) ||
51 (restrictedRoot1 === root2 && restrictedRoot2 === root1)) {
52 isValid = false;
53 break;
54 }
55 }
56
57 // Add the validation result to results array
58 results.push(isValid);
59
60 // If the request is valid, merge the two groups
61 if (isValid) {
62 parent[root1] = root2;
63 }
64 }
65 }
66
67 return results;
68}
69
Time and Space Complexity
Time Complexity: O(q × m × α(n))
where q
is the number of requests, m
is the number of restrictions, and α(n)
is the inverse Ackermann function.
The algorithm uses a Union-Find (Disjoint Set Union) data structure with path compression. For each of the q
requests:
- We perform two
find()
operations to getpu
andpv
, which takesO(α(n))
time each with path compression - We iterate through all
m
restrictions, and for each restriction, we perform twofind()
operations to getpx
andpy
, takingO(α(n))
time each - If the request is approved, we perform one union operation by setting
p[pu] = pv
, which takesO(1)
time
Therefore, for each request, the time complexity is O(1 × α(n) + m × α(n)) = O(m × α(n))
. With q
requests total, the overall time complexity is O(q × m × α(n))
.
Note: The reference answer states O(q × m × log(n))
, which would be accurate if using Union-Find without path compression. With path compression (as implemented in the code with p[x] = find(p[x])
), the amortized time complexity for find()
operations is O(α(n))
, where α(n)
is the inverse Ackermann function, which is effectively constant for all practical values of n
.
Space Complexity: O(n)
The space is used for:
- The parent array
p
of sizen
:O(n)
- The answer list
ans
which stores boolean values for each request:O(q)
- Recursive call stack for
find()
function in worst case (before path compression):O(n)
Since typically q ≤ n
in practical scenarios and we're looking at the dominant term, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Using Find() When Checking Restrictions
The Pitfall:
A critical mistake is directly using the parent array instead of the find()
function when checking restrictions. For example:
# WRONG - This will fail after path compression changes intermediate nodes for restricted_x, restricted_y in restrictions: if (parent[person_u] == parent[restricted_x] and parent[person_v] == parent[restricted_y]) or \ (parent[person_u] == parent[restricted_y] and parent[person_v] == parent[restricted_x]): is_valid = False break
Why It Fails: The parent array doesn't always directly point to the root. After path compression, intermediate nodes may point to ancestors but not necessarily the root. This leads to incorrect group membership checks.
The Solution:
Always use find()
to get the actual root representative:
# CORRECT - Always use find() to get the true root parent_x = find(restricted_x) parent_y = find(restricted_y) if (parent_u == parent_x and parent_v == parent_y) or \ (parent_u == parent_y and parent_v == parent_x): is_valid = False break
2. Performing Union Before Checking All Restrictions
The Pitfall: Merging groups immediately upon finding no direct restriction between the requested pair:
# WRONG - Merging too early parent_u = find(person_u) parent_v = find(person_v) if parent_u != parent_v: parent[parent_u] = parent_v # Union performed here # Then checking restrictions - but groups are already merged! for restricted_x, restricted_y in restrictions: # This check is meaningless now
Why It Fails: Once the union is performed, you can't undo it. If you later discover a restriction violation, the data structure is already corrupted.
The Solution: First check ALL restrictions, then perform union only if valid:
# CORRECT - Check all restrictions first is_valid = True for restricted_x, restricted_y in restrictions: # Check restrictions without modifying the structure parent_x = find(restricted_x) parent_y = find(restricted_y) if (parent_u == parent_x and parent_v == parent_y) or \ (parent_u == parent_y and parent_v == parent_x): is_valid = False break # Only merge if all checks pass if is_valid: parent[parent_u] = parent_v
3. Forgetting to Handle Already-Connected Case
The Pitfall: Not checking if two people are already in the same group before validating restrictions:
# WRONG - Missing the already-friends check parent_u = find(person_u) parent_v = find(person_v) # Immediately checking restrictions without considering they might already be friends is_valid = True for restricted_x, restricted_y in restrictions: # ... restriction checks
Why It Fails: If two people are already friends, the request should automatically succeed without checking restrictions (since no new connections are being made).
The Solution: Check for existing friendship first:
# CORRECT - Handle already-friends case separately parent_u = find(person_u) parent_v = find(person_v) if parent_u == parent_v: result.append(True) # Already friends, automatic success else: # Only check restrictions if they're in different groups is_valid = True for restricted_x, restricted_y in restrictions: # ... restriction checks
These pitfalls are particularly dangerous because they may work correctly for simple test cases but fail on more complex scenarios with multiple groups and chained friendships.
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
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
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
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!