2782. Number of Unique Categories 🔒
Problem Description
You have n
elements numbered from 0
to n - 1
, where each element belongs to a certain category. Your goal is to determine how many unique categories exist among these elements.
To help you solve this problem, you're provided with a CategoryHandler
object that has one method:
haveSameCategory(a, b)
: This method takes two integersa
andb
and returnstrue
if both elements belong to the same category, orfalse
if they belong to different categories. The method also returnsfalse
if eithera
orb
is invalid (less than 0 or greater than or equal ton
).
The solution uses a Union-Find (Disjoint Set Union) data structure to group elements that belong to the same category. Here's how it works:
-
Initialize parent array: Create an array
p
where each element initially points to itself as its parent (p[i] = i
), meaning each element starts in its own category. -
Find function with path compression: The
find(x)
function returns the root representative of elementx
's category. It uses path compression (p[x] = find(p[x])
) to optimize future lookups. -
Group elements by category: For every pair of elements
(a, b)
wherea < b
, check if they belong to the same category usinghaveSameCategory(a, b)
. If they do, union their sets by making the root ofa
point to the root ofb
. -
Count unique categories: After processing all pairs, count how many elements are their own root (
i == p[i]
). Each root represents a unique category.
The time complexity is O(n² × α(n))
where α(n)
is the inverse Ackermann function (practically constant), since we check all pairs of elements. The space complexity is O(n)
for the parent array.
Intuition
The key insight is recognizing that this is fundamentally a grouping problem. We need to identify which elements belong together in the same category, even though we don't know what the categories actually are - we can only check if two elements share the same category.
Think of it like this: imagine you have a collection of colored balls, but you're blindfolded. You can't see the colors, but you have a helper who can tell you whether any two balls you pick have the same color. How would you figure out how many different colors there are?
The natural approach is to compare elements pairwise and group them together when they match. If element a
has the same category as element b
, and element b
has the same category as element c
, then all three must be in the same category. This transitivity property suggests using a Union-Find data structure.
Why Union-Find? Because:
- We start with each element potentially being in its own category (worst case:
n
categories if all elements are different) - As we discover that two elements share a category, we need to merge their groups
- We need to efficiently track which elements belong to the same group even after multiple merges
- At the end, we need to count the number of distinct groups
The Union-Find structure elegantly handles all these requirements. By checking all possible pairs (a, b)
and unioning them when they share a category, we progressively build up the complete picture of which elements belong together. The final count of root elements (elements that are their own parent) gives us the number of unique categories, since each group has exactly one root representing that entire category.
Learn more about Union Find patterns.
Solution Approach
The solution implements a Union-Find (Disjoint Set Union) data structure with path compression to efficiently group elements by their categories.
Step 1: Initialize the parent array
p = list(range(n))
Create a parent array where p[i] = i
for all elements from 0
to n-1
. Initially, each element is its own parent, representing that it's in its own separate category.
Step 2: Define the find function with path compression
def find(x: int) -> int:
if p[x] != x:
p[x] = find(p[x])
return p[x]
This recursive function finds the root representative of element x
's category. The path compression optimization (p[x] = find(p[x])
) directly connects x
to its root, flattening the tree structure for faster future lookups.
Step 3: Check all pairs and union categories
for a in range(n):
for b in range(a + 1, n):
if categoryHandler.haveSameCategory(a, b):
p[find(a)] = find(b)
Iterate through all unique pairs (a, b)
where a < b
. For each pair:
- Call
haveSameCategory(a, b)
to check if they belong to the same category - If they do, perform a union operation:
p[find(a)] = find(b)
- This makes the root of
a
's group point to the root ofb
's group, effectively merging the two groups
Step 4: Count unique categories
return sum(i == x for i, x in enumerate(p))
After processing all pairs, count how many elements satisfy p[i] == i
. These are the root elements, each representing a unique category. The total count gives us the number of distinct categories.
The algorithm ensures that all elements belonging to the same category will share the same root in the Union-Find structure, making the final count accurate and efficient.
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 small example with n = 5
elements, where the actual categories are:
- Elements 0, 2, 4 belong to category A
- Elements 1, 3 belong to category B
Initial Setup:
Parent array p = [0, 1, 2, 3, 4]
Each element points to itself
Step 1: Check pairs and union same categories
Checking pair (0, 1): haveSameCategory(0, 1)
returns false
(different categories)
- No union needed
p = [0, 1, 2, 3, 4]
Checking pair (0, 2): haveSameCategory(0, 2)
returns true
(both in category A)
- Union:
p[find(0)] = find(2)
→p[0] = 2
p = [2, 1, 2, 3, 4]
Checking pair (0, 3): haveSameCategory(0, 3)
returns false
- No union needed
p = [2, 1, 2, 3, 4]
Checking pair (0, 4): haveSameCategory(0, 4)
returns true
(both in category A)
find(0)
returns 2 (follows 0→2)- Union:
p[find(0)] = find(4)
→p[2] = 4
p = [2, 1, 4, 3, 4]
Checking pair (1, 2): haveSameCategory(1, 2)
returns false
- No union needed
Checking pair (1, 3): haveSameCategory(1, 3)
returns true
(both in category B)
- Union:
p[find(1)] = find(3)
→p[1] = 3
p = [2, 3, 4, 3, 4]
Checking pair (1, 4): haveSameCategory(1, 4)
returns false
- No union needed
Checking pair (2, 3): haveSameCategory(2, 3)
returns false
- No union needed
Checking pair (2, 4): haveSameCategory(2, 4)
returns true
(both in category A)
find(2)
returns 4 (follows 2→4)find(4)
returns 4- Union:
p[4] = 4
(already connected)
Checking pair (3, 4): haveSameCategory(3, 4)
returns false
- No union needed
Step 2: Count unique categories
Final parent array with path compression applied:
find(0)
= 4 (follows 0→2→4)find(1)
= 3 (follows 1→3)find(2)
= 4 (follows 2→4)find(3)
= 3 (itself)find(4)
= 4 (itself)
After path compression: p = [4, 3, 4, 3, 4]
Count elements where i == p[i]
:
- i=0: 0 ≠4 ✗
- i=1: 1 ≠3 ✗
- i=2: 2 ≠4 ✗
- i=3: 3 = 3 ✓
- i=4: 4 = 4 ✓
Result: 2 unique categories (elements 3 and 4 are roots representing categories B and A respectively)
Solution Implementation
1# Definition for a category handler.
2# class CategoryHandler:
3# def haveSameCategory(self, a: int, b: int) -> bool:
4# pass
5
6from typing import Optional
7
8class Solution:
9 def numberOfCategories(
10 self, n: int, categoryHandler: Optional['CategoryHandler']
11 ) -> int:
12 """
13 Counts the number of distinct categories among n items.
14
15 Args:
16 n: The total number of items (indexed from 0 to n-1)
17 categoryHandler: An object that can determine if two items belong to the same category
18
19 Returns:
20 The number of distinct categories
21 """
22
23 def find_root(node: int) -> int:
24 """
25 Finds the root of the set containing the given node.
26 Implements path compression for optimization.
27
28 Args:
29 node: The node whose root we want to find
30
31 Returns:
32 The root node of the set containing the input node
33 """
34 if parent[node] != node:
35 # Path compression: directly connect node to its root
36 parent[node] = find_root(parent[node])
37 return parent[node]
38
39 # Initialize parent array where each node is its own parent (self-loop)
40 # This represents n disjoint sets initially
41 parent = list(range(n))
42
43 # Check all pairs of items to determine if they belong to the same category
44 for item_a in range(n):
45 for item_b in range(item_a + 1, n):
46 # If two items have the same category, union their sets
47 if categoryHandler.haveSameCategory(item_a, item_b):
48 # Union operation: connect the roots of both sets
49 root_a = find_root(item_a)
50 root_b = find_root(item_b)
51 parent[root_a] = root_b
52
53 # Count the number of distinct sets (categories)
54 # A node is a root if its parent is itself
55 num_categories = sum(1 for node, parent_node in enumerate(parent) if node == parent_node)
56
57 return num_categories
58
1/**
2 * Definition for a category handler.
3 * class CategoryHandler {
4 * public CategoryHandler(int[] categories);
5 * public boolean haveSameCategory(int a, int b);
6 * };
7 */
8class Solution {
9 // Parent array for Union-Find data structure
10 private int[] parent;
11
12 /**
13 * Finds the number of distinct categories among n items
14 * using Union-Find algorithm to group items in the same category
15 *
16 * @param n The total number of items
17 * @param categoryHandler Handler to check if two items belong to the same category
18 * @return The number of distinct categories
19 */
20 public int numberOfCategories(int n, CategoryHandler categoryHandler) {
21 // Initialize parent array where each item is its own parent initially
22 parent = new int[n];
23 for (int i = 0; i < n; ++i) {
24 parent[i] = i;
25 }
26
27 // Check all pairs of items and union them if they belong to the same category
28 for (int itemA = 0; itemA < n; ++itemA) {
29 for (int itemB = itemA + 1; itemB < n; ++itemB) {
30 if (categoryHandler.haveSameCategory(itemA, itemB)) {
31 // Union the two items by connecting their root parents
32 parent[find(itemA)] = find(itemB);
33 }
34 }
35 }
36
37 // Count the number of distinct categories (root parents)
38 int categoryCount = 0;
39 for (int i = 0; i < n; ++i) {
40 // An item is a root if it's its own parent
41 if (i == parent[i]) {
42 ++categoryCount;
43 }
44 }
45
46 return categoryCount;
47 }
48
49 /**
50 * Finds the root parent of an item using path compression
51 *
52 * @param x The item whose root parent needs to be found
53 * @return The root parent of the item
54 */
55 private int find(int x) {
56 // Path compression: directly connect x to its root parent
57 if (parent[x] != x) {
58 parent[x] = find(parent[x]);
59 }
60 return parent[x];
61 }
62}
63
1/**
2 * Definition for a category handler.
3 * class CategoryHandler {
4 * public:
5 * CategoryHandler(vector<int> categories);
6 * bool haveSameCategory(int a, int b);
7 * };
8 */
9class Solution {
10public:
11 int numberOfCategories(int n, CategoryHandler* categoryHandler) {
12 // Initialize parent array for Union-Find data structure
13 // Each element initially points to itself as parent
14 vector<int> parent(n);
15 iota(parent.begin(), parent.end(), 0);
16
17 // Find function with path compression
18 // Returns the root parent of element x
19 function<int(int)> findRoot = [&](int x) {
20 if (parent[x] != x) {
21 // Path compression: make x point directly to root
22 parent[x] = findRoot(parent[x]);
23 }
24 return parent[x];
25 };
26
27 // Check all pairs of elements and union them if they belong to the same category
28 for (int elementA = 0; elementA < n; ++elementA) {
29 for (int elementB = elementA + 1; elementB < n; ++elementB) {
30 // If two elements have the same category, union their sets
31 if (categoryHandler->haveSameCategory(elementA, elementB)) {
32 // Union operation: make root of elementA point to root of elementB
33 parent[findRoot(elementA)] = findRoot(elementB);
34 }
35 }
36 }
37
38 // Count the number of distinct categories (connected components)
39 // An element is a root if it points to itself
40 int categoryCount = 0;
41 for (int i = 0; i < n; ++i) {
42 if (i == parent[i]) {
43 categoryCount++;
44 }
45 }
46
47 return categoryCount;
48 }
49};
50
1/**
2 * Definition for a category handler.
3 * class CategoryHandler {
4 * constructor(categories: number[]);
5 * public haveSameCategory(a: number, b: number): boolean;
6 * }
7 */
8
9/**
10 * Counts the number of distinct categories using Union-Find algorithm
11 * @param n - Total number of items to categorize
12 * @param categoryHandler - Handler to check if two items belong to the same category
13 * @returns The number of distinct categories
14 */
15function numberOfCategories(n: number, categoryHandler: CategoryHandler): number {
16 // Initialize parent array where each element is its own parent (self-representative)
17 const parent: number[] = Array.from({ length: n }, (_, index) => index);
18
19 /**
20 * Finds the root representative of a given element using path compression
21 * @param element - The element whose root we want to find
22 * @returns The root representative of the element
23 */
24 const findRoot = (element: number): number => {
25 // Path compression: directly connect element to its root
26 if (parent[element] !== element) {
27 parent[element] = findRoot(parent[element]);
28 }
29 return parent[element];
30 };
31
32 // Check all pairs of items and union them if they belong to the same category
33 for (let itemA = 0; itemA < n; itemA++) {
34 for (let itemB = itemA + 1; itemB < n; itemB++) {
35 // If two items share the same category, merge their sets
36 if (categoryHandler.haveSameCategory(itemA, itemB)) {
37 const rootA = findRoot(itemA);
38 const rootB = findRoot(itemB);
39 // Union operation: make one root point to the other
40 parent[rootA] = rootB;
41 }
42 }
43 }
44
45 // Count the number of distinct sets (categories)
46 let categoryCount = 0;
47 for (let i = 0; i < n; i++) {
48 // An element is a root representative if it points to itself
49 if (i === parent[i]) {
50 categoryCount++;
51 }
52 }
53
54 return categoryCount;
55}
56
Time and Space Complexity
Time Complexity: O(n² × α(n))
The algorithm uses a Union-Find (Disjoint Set Union) data structure with path compression. Breaking down the time complexity:
- The outer loop runs
n
times - The inner loop runs approximately
n/2
times on average (froma+1
ton
) - Total number of iterations:
n × (n-1) / 2 = O(n²)
- For each iteration, we call
haveSameCategory()
which takesO(1)
time - When categories match, we perform two
find()
operations and one union operation - With path compression,
find()
operations have an amortized time complexity ofO(α(n))
, whereα
is the inverse Ackermann function (practically constant, but technically grows very slowly) - The final counting loop takes
O(n)
time
Overall: O(n²)
iterations × O(α(n))
per iteration = O(n² × α(n))
For practical purposes, this can be considered O(n²)
since α(n)
is effectively constant for all reasonable values of n
.
Space Complexity: O(n)
The space usage consists of:
- The parent array
p
which storesn
integers:O(n)
- The recursive call stack for
find()
function: worst caseO(n)
if the tree becomes a chain before path compression occurs - No additional data structures are used
Total space complexity: O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Finding Roots Before Union Operation
A critical mistake is directly connecting nodes without finding their roots first. This creates incorrect connections and fails to properly merge entire groups.
Incorrect Implementation:
# WRONG: Directly connecting nodes instead of roots if categoryHandler.haveSameCategory(item_a, item_b): parent[item_a] = item_b # This is incorrect!
Why it fails: If item_a
is already part of a larger group with root r1
, and item_b
is part of another group with root r2
, this code only connects item_a
to item_b
, leaving the rest of item_a
's group disconnected from item_b
's group.
Correct Implementation:
if categoryHandler.haveSameCategory(item_a, item_b): root_a = find_root(item_a) root_b = find_root(item_b) parent[root_a] = root_b # Connect the roots, not the nodes
2. Incorrect Final Count Due to Not Updating Parent Array
After all union operations, the parent array might not have all nodes directly pointing to their ultimate root due to path compression only happening during find
operations.
Problem: Counting parent[i] == i
without ensuring all paths are compressed can give incorrect results.
Solution: Call find_root
on all nodes before counting to ensure complete path compression:
# Ensure all paths are compressed
for i in range(n):
find_root(i)
# Now count the roots
num_categories = sum(1 for i in range(n) if parent[i] == i)
3. Redundant Union Operations
Performing union operations when two elements are already in the same category wastes computation.
Optimization:
if categoryHandler.haveSameCategory(item_a, item_b): root_a = find_root(item_a) root_b = find_root(item_b) if root_a != root_b: # Only union if they're in different sets parent[root_a] = root_b
This check prevents unnecessary writes to the parent array when elements are already grouped together, which can happen frequently as the algorithm progresses.
How many ways can you arrange the three letters A, B and C?
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
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!