Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Each query (a, b) makes a and b friends; friendship is transitive, forming circles. After each query, report the size of the largest circle so far. Use union-find with a size per component and union by size: when two distinct circles merge, the new size is the sum; keep a running max and append it after every query. Because person IDs can be up to 10⁹, back the disjoint set with hash maps, not arrays. O(q · α) ≈ O(q).
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you model incremental friend-merging as union-find, augment it with per-component size, and maintain the largest size as a running max — using hash maps because IDs are large.
Where candidates slip
Recomputing component sizes by BFS after each query (O(q·n)), updating the max on no-op unions, forgetting union by size / path compression, or assuming small array-indexable IDs.
How to stand out
Note that the max only changes on a real merge, use union by size + path compression for the inverse-Ackermann bound, and explain why a hash-map DSU is required for a 10⁹ id space.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewOnline merging + largest-group query?Edges removed ever?YesNoExplore the full treeCollapse the tree
How to read it:Query (2,3) fuses two size-2 circles into one size-4 circle — the moment the running max jumps to 4.
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Explicit-Set Merge (Baseline)
1Hint 1Explore hintHide hint
Model a circle as a real set of members, and keep a map from each person to the set they belong to.
2Hint 2Explore hintHide hint
On query (a, b): if they're in different sets, pour the members of one set into the other and update each moved member's pointer; track the new size and the global max.
3Hint 3Explore hintHide hint
Merging without choosing the smaller side can move many members repeatedly → O(n) per merge in the worst case. Correct, but it motivates union-find.
A · Core idea
The most direct model treats each friend circle as a concrete collection of people.
The structure. A hash map circleOf maps each person to the set object representing their circle. New people start in a fresh singleton set.
Per query. For (a, b), fetch both sets. If they're the same object, it's a no-op. Otherwise merge: iterate over the members of one set, add each to the other, and update its circleOf pointer to the surviving set. The surviving set's size is the sum; update the global best and append it.
Why it's correct. Sets exactly represent connected components: merging two sets on an edge models transitive friendship precisely, and the size of the largest set is the answer.
Why we don't ship it. Pouring all members of one set into another costs time proportional to that set's size. If you don't consistently move the smaller set, a chain of merges can re-move the same elements over and over — O(q · n). Union-find replaces whole-set copying with a single pointer update, dropping each operation to near-O(1).
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
queries = [[1,2],[3,4],[2,3]]Array state
(1,2)
{1,2}
[2, 2, 4]C · Code with teaching comments
Reference implementation previewpublic int[] maxCircle(int[][] queries) { Map<Integer, Set<Integer>> circleOf = new HashMap<>(); int[] result = new int[queries.length]; int best = 0; for (int qi = 0; qi < queries.length; qi++) {Explore the full codeHide the full code
D · Why it works and complexityO(q · n) worst case timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
Explicit-set merging makes the model obvious, and even reveals the key optimization: always move the smaller set (small-to-large) to bound the work. Union-find generalizes that intuition with union by size plus path compression, avoiding member copying entirely.
ComplexityO(q · n) worst case time, O(n) space — Each merge copies the members of one circle into another and repoints them. Without always moving the smaller side, a single person can be moved up to n times across q queries, giving O(q · n) worst case. Lookups are O(1) via the person→set map. Correct and intuitive, but quadratic in adversarial inputs.
E · Edge cases and pitfalls3Merging without small-to-large — re-moves members and degrades to O(q·n).Explore edge casesHide edge cases
Pitfalls
- Merging without small-to-large — re-moves members and degrades to O(q·n).
- Updating best on a no-op (same-set) query.
- Assuming small IDs and indexing an array instead of a map.
Approach 2 of 2 · Optimal
Union-Find with Size (Optimal)
1Hint 1Explore hintHide hint
Instead of copying members, can each circle be a tree of parent pointers so a merge is one pointer update?
2Hint 2Explore hintHide hint
Union-find: find(x) returns the root (circle id) with path compression; union(a, b) links the smaller-size root under the larger and adds the sizes.
3Hint 3Explore hintHide hint
Keep size[root] for each circle and a running best. On a real merge, best = max(best, new size). Append best after each query. Back parent/size with hash maps for the 10⁹ id space.
A · Core idea
Union-find turns whole-set merging into a single pointer change, keeping circle sizes on the roots.
The structure. Two hash maps: parent[x] (the parent pointer; a root points to itself) and size[r] (members in the circle rooted at r). The first time a person is seen, find creates them as a singleton — parent[x] = x, size[x] = 1.
find with path compression. find(x) walks parent pointers to the root and compresses the path so future lookups are near-constant.
union by size. For query (a, b), compute roots ra, rb. If equal, the two are already in one circle — a no-op. Otherwise attach the smaller-size root under the larger and add the sizes. Union by size keeps the trees shallow, which (with path compression) gives the inverse-Ackermann bound.
The running max. The largest circle can only change when two different circles merge, and the only candidate for a new maximum is the just-merged circle's size. So after a real union, best = max(best, size[newRoot]); on a no-op, best is unchanged. Append best after every query — including no-ops — to produce exactly q outputs.
Why hash maps. IDs reach 10⁹ and are sparse, so an array of that size is infeasible. Hash maps store only the people actually involved, with lazy singleton creation on first contact.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
queries = [[1,2],[3,4],[2,3]]Array state
(1,2)
size 2, best 2
[2, 2, 4]C · Code with teaching comments
D · Why it works and complexityO(q · α(n)) ≈ O(q) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
Augmenting union-find's roots with a size is the lever that answers aggregate questions ('largest circle', 'is this group big enough?') in O(1) per merge. Combined with the observation that the global max only moves on a real merge, the whole problem collapses to one running maximum updated alongside the unions.
ComplexityO(q · α(n)) ≈ O(q) time, O(n) space — Each query does two finds and at most one union. With path compression and union by size, those are O(α(n)) amortized (inverse Ackermann ≤ 4 in practice). So all q queries cost O(q · α(n)) ≈ O(q). Hash-map operations are O(1) average. Space is O(n) for the parent and size maps over the distinct people seen.
E · Edge cases and pitfalls7duplicate query (1,2) then (1,2) — Second union finds equal roots → no-op; best unchanged but still appended.Explore edge casesHide edge cases
Edge cases handled
duplicate query (1,2) then (1,2)Second union finds equal roots → no-op; best unchanged but still appended.query (5, 5) (same person)find(5) == find(5) → no merge; size stays 1, best unaffected.all distinct pairs, no overlapsEvery circle is size 2; best is 2 after the first query and stays 2.
Pitfalls
- Updating best on a no-op union (same root).
- Skipping union by size and/or path compression — finds degrade toward O(n).
- Using arrays for parent/size when IDs reach 10⁹.
- Re-deriving the max by scanning all components each query — that's O(q·n).
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewprivate Map<Integer, Integer> parent = new HashMap<>(); private Map<Integer, Integer> size = new HashMap<>(); private int find(int x) { parent.putIfAbsent(x, x);Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Max updated on a no-op union
If a and b are already in the same circle, nothing merges. Touching best (or re-adding size) here can overcount, and re-summing sizes corrupts the component size.
Explore the code comparisonHide the code comparison
Max updated on a no-op union
If a and b are already in the same circle, nothing merges. Touching best (or re-adding size) here can overcount, and re-summing sizes corrupts the component size.
Don't write this
ra, rb = find(a), find(b)
parent[rb] = ra # blindly unions even if ra == rb
size[ra] += size[rb]
best = max(best, size[ra])Write this instead
ra, rb = find(a), find(b)
if ra != rb: # only on a real merge
parent[rb] = ra
size[ra] += size[rb]
best = max(best, size[ra])2Array DSU with huge IDs
IDs reach 10⁹, so new int[10⁹] is infeasible (memory) — and even compressing requires a map. A hash-map DSU stores only the people actually seen.
Explore the code comparisonHide the code comparison
Array DSU with huge IDs
IDs reach 10⁹, so new int[10⁹] is infeasible (memory) — and even compressing requires a map. A hash-map DSU stores only the people actually seen.
Don't write this
int[] parent = new int[1_000_000_001]; // out of memoryWrite this instead
Map<Integer, Integer> parent = new HashMap<>(); // lazy, sparse3Rescanning for the max
Deriving the largest circle by iterating all components after each query is O(n) per query → O(q·n). Maintaining a running max updated on each real merge is O(1).
Explore the code comparisonHide the code comparison
Rescanning for the max
Deriving the largest circle by iterating all components after each query is O(n) per query → O(q·n). Maintaining a running max updated on each real merge is O(1).
Don't write this
result.append(max(size[find(p)] for p in all_people)) # O(n) per queryWrite this instead
best = max(best, size[ra]) # O(1) update on merge
result.append(best)Revise
Carry the pattern into the interview
Pattern to remember
Union-find with component sizes + running max
Code pattern to memorize
parent = {}, size = {}, best = 0
for (a, b) in queries:
ra, rb = find(a), find(b) # find auto-creates new singletons (size 1)
if ra != rb:
if size[ra] < size[rb]: swap(ra, rb)
parent[rb] = ra
size[ra] += size[rb]
best = max(best, size[ra])
append best
return resultsRules to remember
- 1Track a size per root and merge smaller into larger (union by size) to keep trees shallow.
- 2Maintain a running max circle size; update it only when a union actually merges two different circles.
- 3Person IDs are huge (up to 10⁹) → back parent/size with hash maps, lazily creating a singleton (size 1) on first sight.
- 4A query joining two people already in the same circle changes nothing —
beststays the same; still append it. - 5Append the current
bestafter every query, including no-op unions.
Use this pattern when
- Incremental **merging** of groups with a 'largest group so far' question
- Dynamic connectivity: edges arrive online, queries interleave
- Sparse / large id space where arrays won't fit → hash-map DSU
- You only need group *sizes/connectivity*, not paths or distances
Choose another pattern when
- Edges are *removed* over time → union-find can't undo; use other structures (or offline + reverse)
- You need shortest paths or weighted distances → Dijkstra/BFS, not DSU
- A static graph asked once → a single BFS/DFS pass suffices
- You must enumerate each group's members repeatedly → maintain explicit lists, not just sizes
Interview takeaway
Union-find is the canonical tool for online merging plus connectivity. Augmenting each root with a size turns 'how big is the largest group?' into an O(1) running maximum updated on each real merge — the standard pattern behind friend circles, islands that grow, and accounts-merge problems.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternCanonical Union-Find with Augmented Size. Recognize it on online merging plus an aggregate query ('largest group', 'still connected?'). Store size on roots, union by size with path compression, and keep a running max updated only on real merges. Use a hash-map-backed DSU whenever the id space is too large or sparse for arrays.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Number of Connected ComponentsSame union-find; track a component counter that decrements on each real merge instead of a max size.
- Accounts MergeComing soonUnion-find over emails to merge accounts sharing any address, then group members by root.
- Number of Islands IIComing soonOnline land additions on a grid — union neighbors and report the island count after each addition.
- Kruskal (MST): Really Special SubtreeUnion-find as the cycle-detection engine while greedily adding the cheapest edges.