9. Cluster Near-Duplicate Support Queries (Union-Find)
Problem
An upstream embedding-similarity check has already flagged certain pairs of incoming support queries as similar enough to be near-duplicates of each other. Given those pairs, group all queries into clusters of mutually-connected duplicates so that only one representative per cluster needs a fresh answer, and the rest can reuse it.
Similarity is transitive across the given pairs: if q1~q2 and q2~q3 are both flagged, q1, q2, and q3 all belong to the same cluster even though q1~q3 was never directly flagged.
Source: src/9_cluster_similar_queries.py
def cluster_queries(query_ids: list[str], similar_pairs: list[tuple[str, str]]) -> list[list[str]]
>>> cluster_queries(["q1", "q2", "q3", "q4"], [("q1", "q2"), ("q3", "q4")])
[['q1', 'q2'], ['q3', 'q4']]
>>> cluster_queries(["q1", "q2", "q3", "q4"], [("q1", "q2"), ("q2", "q3")])
[['q1', 'q2', 'q3'], ['q4']]
Step-by-Step Approach
- Recognize that "group items connected by pairwise edges, including transitively" is exactly the Union-Find (Disjoint Set Union) problem — every query starts in its own singleton set.
- Initialize a
parentmap where every query id points to itself, and arankmap for union-by-rank, all starting at 0. - Write a
findfunction that walks up parent pointers to the root of a query's set, applying path compression (point each visited node directly at its grandparent) to keep future lookups fast. - Write a
unionfunction that finds the roots of both queries and, if they differ, attaches the lower-rank root under the higher-rank root (bumping rank on a tie) — this keeps the resulting trees shallow. - Process every pair in
similar_pairswithunion, merging all transitively-connected queries into one set regardless of pair order. - After all unions, do a final pass over every query id, calling
findto get its root, and bucket ids by root into a dict of lists. - Sort each cluster's members and sort the clusters themselves (e.g. by first member) to get a deterministic output order.
The key insight is that Union-Find turns "is item A transitively connected to item B through a chain of pairwise edges?" into two near-constant-time operations, avoiding an explicit graph traversal (BFS/DFS) for each connectivity query.
Reference solution
def cluster_queries(
query_ids: list[str], similar_pairs: list[tuple[str, str]]
) -> list[list[str]]:
# Union-Find with path compression + union by rank: ~O(n + m * alpha(n)) total.
# every element starts as its own root
parent = {qid: qid for qid in query_ids}
rank = {qid: 0 for qid in query_ids}
def find(x: str) -> str:
while parent[x] != x:
# path halving: point at grandparent
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(a: str, b: str) -> None:
root_a, root_b = find(a), find(b)
if root_a == root_b:
return
if rank[root_a] < rank[root_b]:
# swap so root_a is higher-or-equal rank
root_a, root_b = root_b, root_a
# attach shorter tree under taller one
parent[root_b] = root_a
if rank[root_a] == rank[root_b]:
rank[root_a] += 1
for a, b in similar_pairs:
union(a, b)
groups: dict[str, list[str]] = {}
for qid in query_ids:
# get-or-create bucket by root
groups.setdefault(find(qid), []).append(qid)
# deterministic member order
clusters = [sorted(members) for members in groups.values()]
# deterministic cluster order
clusters.sort(key=lambda members: members[0])
return clusters
Key Functions & Tricks
- Union-Find (Disjoint Set Union) — partition into disjoint sets via a
parentforest - Dict comprehensions
{qid: qid for qid in ...}/{qid: 0 for qid in ...}— init parent/rank maps - Path compression (path halving) —
parent[x] = parent[parent[x]]flattens tree, near-O(1) amortized - Union by rank — attach shorter tree under taller root, keeps trees shallow
- Tuple swap
a, b = b, a— swaps variables in one line, no temp needed dict.setdefault(key, [])— get-or-create bucket list, one-line idiom- List comprehension
[sorted(m) for m in groups.values()]— deterministic member order list.sort(key=lambda m: m[0])— deterministic cluster order by first member
How to Recognize This Pattern
Reach for Union-Find whenever a problem gives you a set of items plus a list of pairwise "these are connected/equivalent/same-group" relationships and asks you to determine connected components, detect a cycle in an undirected graph, or check if two items end up in the same group — especially when the graph is never explicitly needed as an adjacency structure, only the grouping. Common variations include counting the number of clusters, detecting whether a specific pair ends up connected (a single find comparison), or Union-Find with weights/sizes attached to each set. A common pitfall is forgetting path compression or union-by-rank, which degrades find from near-O(1) to O(n) in the worst case (a long chain), and can matter on large inputs even though it doesn't affect correctness.