Design a Versioned Social Follow Graph with Friend Recommendations
Company: OpenAI
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: easy
Interview Round: Technical Screen
Constraints
- User ids are non-negative integers; timestamps are positive integers, strictly increasing across mutations (no two mutations share a timestamp).
- Up to about 10^5 total operations.
- get_followees / get_followers return ids in ascending order, reflecting the latest state.
- is_following considers only events with event-time <= the query timestamp, returns False when the pair has no such event, and does not advance the current time.
- recommend ranks qualifying candidates by number of distinct intermediaries descending, breaking ties by smaller id, and returns at most k ids (fewer if fewer qualify).
Examples
Input: (['SocialGraph', 'follow', 'follow', 'follow', 'follow', 'follow', 'is_following', 'unfollow', 'is_following', 'is_following', 'get_followees', 'recommend'], [[], [1, 10, 20], [2, 10, 30], [3, 20, 40], [4, 30, 40], [5, 30, 50], [10, 20, 1], [6, 10, 20], [10, 20, 6], [10, 20, 3], [10], [10, 5]])
Expected Output: [None, None, None, None, None, None, True, None, False, True, [30], [40, 50]]
Explanation: Problem's worked example: follow/unfollow, versioned is_following (True@t1, False@t6, True@t3), get_followees([30]), recommend([40,50]).
Input: (['SocialGraph', 'follow', 'follow', 'follow', 'follow', 'follow', 'follow', 'follow', 'follow', 'recommend'], [[], [1, 1, 2], [2, 1, 3], [3, 1, 4], [4, 2, 5], [5, 2, 6], [6, 3, 5], [7, 3, 7], [8, 4, 5], [1, 5]])
Expected Output: [None, None, None, None, None, None, None, None, None, [5, 6, 7]]
Explanation: recommend scoring: user 1 follows {2,3,4}; candidate 5 has three intermediaries (score 3); 6 and 7 tie at score 1, broken by smaller id -> [5,6,7].
Input: (['SocialGraph', 'follow', 'unfollow', 'follow', 'is_following', 'is_following', 'is_following', 'is_following'], [[], [1, 1, 2], [3, 1, 2], [5, 1, 2], [1, 2, 2], [1, 2, 4], [1, 2, 6], [1, 2, 1]])
Expected Output: [None, None, None, None, True, False, True, True]
Explanation: History with re-follow (follow@1, unfollow@3, follow@5): is_following True@t2, False@t4, True@t6, and True@t1 (equal-timestamp boundary).
Input: (['SocialGraph', 'is_following', 'follow', 'is_following'], [[], [1, 2, 5], [10, 1, 2], [1, 2, 3]])
Expected Output: [None, False, None, False]
Explanation: Edge case: is_following on a pair with no recorded event -> False; and a query timestamp (3) earlier than the only event (follow@10) -> False.
Input: (['SocialGraph', 'follow', 'follow', 'follow', 'follow', 'follow', 'follow', 'get_followees', 'get_followers', 'recommend'], [[], [1, 1, 5], [2, 1, 3], [3, 3, 5], [4, 3, 9], [5, 5, 3], [6, 5, 8], [1], [5], [1, 5]])
Expected Output: [None, None, None, None, None, None, None, [3, 5], [1, 3], [8, 9]]
Explanation: get_followees(1)=[3,5] and get_followers(5)=[1,3] are ascending; recommend(1) excludes already-followed 3 and 5, leaving 9 and 8 (score 1 each) -> [8,9].
Input: (['SocialGraph', 'follow', 'follow', 'follow', 'recommend'], [[], [1, 1, 2], [2, 2, 3], [3, 2, 4], [1, 1]])
Expected Output: [None, None, None, None, [3]]
Explanation: recommend returns at most k ids: via intermediary 2 both 3 and 4 qualify (score 1), but k=1 keeps only the smaller-id 3 -> [3].
Hints
- Keep the current state as two adjacency maps, followees[u] and followers[u] (sets), updated on every follow/unfollow. get_followees, get_followers, and recommend all read from these latest-state maps.
- For versioned is_following, store an append-only list of (timestamp, is_follow) events per ordered pair (follower, followee). Since mutation timestamps strictly increase, each list is already sorted, so binary-search (bisect_right - 1) for the rightmost event with time <= the query and return its flag.
- For recommend(user, k): for each intermediary m in followees[user], for each c in followees[m], increment score[c]; skip c == user and any c already in followees[user]. Sort candidates by (-score, id) and slice the first k.