Coding questions · OpenAI
Implement follow graph with snapshots and recommendations
This question evaluates design and implementation skills for mutable graph data structures with point-in-time snapshotting and recommendation ranking, testing knowledge of data structures, versioning, graph traversal, and algorithmic time/space complexity analysis.
Design and implement an in-memory “social network” component that supports following/unfollowing, point-in-time (snapshot) queries, and a simple recommendation API.
You are given users identified by integers 1..N (assume N can be large). Implement the following operations:
follow(u, v): userustarts following userv.unfollow(u, v): userustops following userv.snap() -> sid: creates a snapshot of the current follow graph and returns a monotonically increasing snapshot idsid.isFollowing(u, v, sid) -> bool: returns whetheruwas followingvat the time snapshotsidwas taken.recommend(u, k) -> list[v]: recommend up tokusers thatudoes not currently follow (and excludingu). Rank candidates by the number of users thatucurrently follows who also follow the candidate (i.e., “friends-of-friends” count). Break ties by smaller user id.
Clarifications/assumptions you should state and handle:
- Repeated
follow(u, v)when already following should be a no-op; similarlyunfollowwhen not following. - Snapshots are immutable;
isFollowingmust reflect state at that snapshot. - Analyze time and space complexity of your approach, especially for large numbers of operations and snapshots.
Part 1: Basic Follow/Unfollow Graph
Implement a simple directed follow graph for a small social network and answer membership queries against it.
There are n users labeled 0 to n - 1. You are given a list of operations, each a tuple (kind, a, b) where a and b are user ids. Process the operations in order, maintaining for every user the set of users they currently follow (an edge a → b means "a follows b").
Function
def solution(n, operations):
...
n— the number of users.operations— a list of tuples; each is one of the three kinds below.- Return — a list of booleans, one for each
checkoperation, in the order thosecheckoperations appear.
Operations
('follow', a, b)— userastarts following userb(add the edgea → b).('unfollow', a, b)— userastops following userb(remove the edgea → b).('check', a, b)— appendTrueif userais currently following userb, otherwiseFalse.
Rules
- No self-follow. A user cannot follow themself. A
followwitha == bis ignored (no edge is created). Consequently, acheckwitha == balways returnsFalse. - Idempotent follow. Repeating
followon a relation that already exists does nothing. - No-op unfollow.
unfollowon a relation that does not exist does nothing.
Output
Return the answers to all check operations, in the order they were processed. If there are no check operations, return an empty list.
Constraints
- 0 <= n <= 10^5
- 0 <= len(operations) <= 2 * 10^5
- All user ids in operations are integers in the range [0, n - 1]
- Average O(1) set operations are acceptable
Examples
Example 1
Input: (4, [('follow', 0, 1), ('check', 0, 1), ('unfollow', 0, 1), ('check', 0, 1), ('follow', 0, 0), ('check', 0, 0)])
Output: [True, False, False]
Explanation: User 0 first follows 1, then unfollows 1. Self-follow is ignored, so checking (0, 0) is False.
Example 2
Input: (3, [('follow', 1, 2), ('follow', 1, 2), ('check', 1, 2), ('unfollow', 1, 2), ('unfollow', 1, 2), ('check', 1, 2)])
Output: [True, False]
Explanation: Repeated follow and unfollow operations should not break the state.
Hints
- A hash set for each user is enough to represent who they currently follow.
- Make
followandunfollowidempotent so repeated operations do not cause errors.
Approach
Approach: adjacency sets. The follow graph is a directed graph where an edge a → b means "user a follows user b." We store it as follows, a list of n Python sets, one per user. follows[a] holds exactly the set of users that a currently follows. Sets give us average O(1) insert, delete, and membership test — perfect for a stream of follow/unfollow/check operations.
Processing each operation:
('follow', a, b)— addbtofollows[a], but only ifa != b(self-follow is ignored per the rules). Adding to a set is idempotent, so repeatingfollowon an existing edge is automatically a no-op.('unfollow', a, b)— usefollows[a].discard(b).discard(unlikeremove) does not raise ifbis absent, so repeatingunfollowon a missing edge is a silent no-op, exactly as required. Thea != bguard is harmless here since a self-edge can never exist.('check', a, b)— appenda != b and b in follows[a]. Thea != bshort-circuit guarantees a user is never reported as following themself, even defensively, before doing the set lookup.
Why it's correct: the set membership of follows[a] is the single source of truth for whether edge a → b exists at any moment. Each rule (no self-follow, idempotent follow, no-op unfollow) maps directly onto a set property — uniqueness, add idempotency, and discard tolerance — so no extra bookkeeping is needed. check answers are appended in operation order, matching the required output.
- Time complexity: O(n + m) average, where n is the user count and m is the number of operations. Building n empty sets is O(n); each operation does O(1) average-case set work.
- Space complexity: O(n + E), where E is the number of currently active follow edges. The list holds n sets, and the sets collectively store E edges; the output list adds O(c) for the c check operations.
Part 2: Follow Graph with Snapshots and Historical Queries
Maintain a directed follow graph that supports immutable snapshots and historical queries against those snapshots.
There are n users labeled from 0 to n - 1. You are given a list of operations to process in order. Each operation is a tuple (or list) whose first element is a string naming the operation:
Operations
('follow', a, b)— userastarts following userb. The relationship is directed:afollowingbdoes not meanbfollowsa.('unfollow', a, b)— userastops following userb.('snapshot',)— save the current state of the follow graph and return its snapshot id.('check', snap_id, a, b)— ask whether userawas following userbin the saved state identified bysnap_id. Returns a boolean.
Rules
- Snapshot ids start at
0and increase by1each timesnapshotis called (the first snapshot gets id0, the second1, and so on). - A
snapshotcaptures everyfollow/unfollowapplied before it is taken. Snapshots are immutable: anyfollow/unfollowperformed after a snapshot leaves that older snapshot's state unchanged. - No self-follows. A
followwherea == bis ignored (no state change). Likewise, acheckwherea == balways returnsFalse. - Idempotency. Repeated
followoperations on a pair that is already following, or repeatedunfollowoperations on a pair that is not following, have no effect. Following → unfollowing → following again leaves the pair in the following state. - Every
checkis guaranteed to use asnap_idthat was previously returned by asnapshotcall.
Return value
Return a list containing the result of every snapshot and every check, in the order the operations appear:
- For each
snapshot, append its snapshot id (an integer). - For each
check, append a boolean (Trueifawas followingbin that snapshot, otherwiseFalse).
follow and unfollow operations produce no output.
Function signature
def solution(n, operations):
...
Examples
- Operations
[('follow', 0, 1), ('snapshot',), ('unfollow', 0, 1), ('snapshot',), ('check', 0, 0, 1), ('check', 1, 0, 1)]withn = 3return[0, 1, True, False]. Snapshot0is taken while0follows1(→True); snapshot1is taken after the unfollow (→False). - Operations
[('follow', 0, 1), ('unfollow', 0, 1), ('follow', 0, 1), ('snapshot',), ('check', 0, 0, 1)]withn = 2return[0, True], since the final state before the snapshot has0following1. - Operations
[('follow', 0, 0), ('snapshot',), ('check', 0, 0, 0)]withn = 2return[0, False]— the self-follow is ignored and the selfcheckisFalse.
Constraints
0 <= n <= 10^50 <= len(operations) <= 2 * 10^5- All user ids are in
[0, n - 1]. - Each
checkuses a valid snapshot id that has already been created.
Hints
- You do not need to copy the whole graph at every snapshot. Store only when a specific follow relation changes.
- For each pair
(a, b), keep a sorted history of(snapshot_id, state)and binary-search it during historical queries.
Approach
The solution treats each ordered pair (a, b) as an independent boolean ("a follows b") that changes over time, and answers historical check queries with a per-pair binary search.
State kept
current[(a, b)]— the live follow flag, used to enforce idempotency.history[(a, b)]— a list of(snap_version, flag)change-points, ordered by version.current_snap— the id the nextsnapshotwill return; it also tags every change with the snapshot version it belongs to.
Per operation
follow/unfollow: self-follows (a == b) are ignored. A change is recorded only when the flag actually flips (so repeats are no-ops). The new state is appended as(current_snap, flag); if the last history entry already has the samecurrent_snap, it is overwritten so multiple flips between two snapshots collapse to the net state at that version.snapshot: appendscurrent_snapto the answers, then increments it. No copying of the graph happens — snapshots are implicit, identified purely by version number.check(snap_id, a, b): self-pairs returnFalse. Otherwise it binary-searches the pair's history withbisect_right(hist, (snap_id, 1)) - 1to find the last change-point whose version is<= snap_id. That entry's flag is the answer; if none exists, the answer isFalse.
Why correct: because change-points are tagged with the version active when they happened, the most recent change at version <= snap_id is exactly the pair's state as it existed when snapshot snap_id was taken, and later mutations (higher versions) never affect it — giving immutable historical reads without ever materializing a full graph copy.
- Time complexity: O(len(operations) log len(operations))
- Space complexity: O(len(operations))
Part 3: Recommend Top-K Users by Two-Hop Follow Count
Build a friend-recommendation engine on a directed follow graph, processing a stream of operations and returning a recommendation list for each query.
There are n users labeled 0 to n - 1. The follow graph is directed: "user a follows user b" does not imply b follows a.
Function
Implement solution(n, operations):
n— the number of users.operations— a list of tuples, each one of three kinds, applied in order.
Return a list of lists: one recommendation list for every ('recommend', ...) operation, in the order those operations appear. follow and unfollow operations produce no output.
Operations
('follow', a, b)— userastarts following userb. Following someone you already follow has no effect.('unfollow', a, b)— userastops following userb. Unfollowing someone you don't follow has no effect.('recommend', u, k)— produce up tokrecommended users for useru(see below).
Recommendation rule
For a recommend(u, k) query, score every candidate user c by counting two-hop follow paths u → x → c:
- A candidate
cearns 1 point for each userxsuch that:ucurrently followsx, andxcurrently followsc.
- Equivalently,
c's score is the number of usersualready follows who in turn followc.
Apply these filters to the candidates:
- Exclude
u— never recommend a user to themselves. - Exclude users
ualready follows — only recommend new connections. - Keep only positive scores — a candidate with score
0(no qualifying two-hop path) is not recommended.
Ranking
Sort the surviving candidates and return at most k of them:
- Higher score first.
- Ties broken by smaller user id first.
Return the first k ids in this order. If fewer than k candidates qualify, return all of them.
Edge cases
- Self-follow is ignored. A
('follow', a, a)(and likewise('unfollow', a, a)) is a no-op and contributes no edge to the graph. k <= 0yields an empty recommendation list[]for that query.- If no candidate has a positive score, the recommendation list is
[].
Constraints
0 <= n <= 10^50 <= len(operations) <= 2 * 10^5- All user ids are in
[0, n - 1]
Examples
Example 1
Input: (5, [('follow', 0, 1), ('follow', 0, 2), ('follow', 1, 3), ('follow', 2, 3), ('follow', 2, 4), ('recommend', 0, 2)])
Output: [[3, 4]]
Explanation: User 3 gets score 2 because both followed users 1 and 2 follow 3. User 4 gets score 1.
Example 2
Input: (6, [('follow', 0, 1), ('follow', 0, 2), ('follow', 1, 4), ('follow', 2, 3), ('recommend', 0, 2)])
Output: [[3, 4]]
Explanation: Users 3 and 4 both have score 1, so the smaller id 3 comes first.
Hints
- For a recommendation query on user
u, iterate through everyoneufollows, then through each of their follow sets, and count candidate frequencies. - After counting, sort candidates by
(-score, user_id)and take the firstk.
Approach
The algorithm maintains an adjacency list of sets: follows[a] holds every user that a currently follows. Using sets makes follow, unfollow, and membership tests all O(1) average, and naturally deduplicates repeated follows.
Mutations. follow/unfollow simply add/discard on follows[a], but only when a != b, which silently ignores self-follows as required.
Recommendation (the core). For ('recommend', u, k):
- If
k <= 0, append[]and skip — nothing can be recommended. - Build
blocked = set(follows[u]) ∪ {u}. This enforces two rules at once: never recommend a userualready follows, and never recommenduitself. - Compute the two-hop score. For each
xinfollows[u](the usersufollows), iterate overfollows[x](whomxfollows). Each such candidatecnot inblockedgets +1 viascores[c] = scores.get(c, 0) + 1. So a candidate's score is exactly how many ofu's followees also followc— precisely the problem's definition. Candidates with zero qualifying paths never enterscores, so only positive scores remain. - Rank with
sorted(scores.items(), key=lambda item: (-item[1], item[0])): descending score, then ascending id to break ties. Take the firstkids.
Why correct: scores are accumulated only over genuine u → x → c paths, the blocked set removes self/already-followed users, and the sort key matches the ranking rule exactly. Test 5 confirms the self-follow ('follow', 1, 1) is ignored, so user 1 contributes no two-hop edge.
- Time complexity: O(1) average per follow/unfollow. Each recommend(u, k) is O(T + C log C), where T is the total number of outgoing edges across all users that u follows (the work to accumulate scores) and C is the number of distinct positive-score candidates (the sort).
- Space complexity: O(n + E) for the adjacency-list-of-sets (n sets plus E total follow edges), plus O(C) per recommend query for the transient scores map and blocked set, where C is the number of candidate users for that query.