3160. Find the Number of Distinct Colors Among the Balls
Problem Description
You have limit + 1
balls labeled from 0
to limit
. Initially, all balls are uncolored.
You're given a 2D array queries
where each query is of the form [x, y]
. For each query:
- Ball
x
gets marked with colory
- If ball
x
was already colored, its old color is replaced with the new colory
After processing each query, you need to count how many distinct colors are currently being used across all balls.
Your task is to return an array result
where result[i]
represents the number of distinct colors in use after processing the first i+1
queries.
Key Points:
- Balls can be recolored (their color gets replaced, not added)
- Uncolored balls don't count as having a color
- You need to track the number of unique colors after each query, not the number of colored balls
Example walkthrough:
If you have queries [[0, 1], [1, 2], [0, 2]]
:
- After query 1: Ball 0 has color 1 → 1 distinct color
- After query 2: Ball 0 has color 1, Ball 1 has color 2 → 2 distinct colors
- After query 3: Ball 0 changes to color 2, Ball 1 has color 2 → 1 distinct color
The result would be [1, 2, 1]
.
Intuition
The key insight is that we need to track two things simultaneously: which color each ball currently has, and how many balls have each color.
Think about what happens when we color a ball:
- If the ball is being colored for the first time, we're adding a new ball to that color group
- If the ball already had a color, we're removing it from its old color group and adding it to the new color group
This suggests we need two data structures:
- A mapping from ball → color (to know what color each ball currently has)
- A mapping from color → count (to know how many balls have each color)
The critical observation is that when a ball changes color, the count for its old color decreases by 1. If that count reaches 0, that color is no longer in use and should be removed from our tracking.
For example, if ball 0 has color 1 and we recolor it to color 2:
- Color 1's count decreases by 1
- Color 2's count increases by 1
- If color 1's count becomes 0, color 1 is no longer active
The number of distinct colors at any point is simply the number of colors that have a count greater than 0, which equals the size of our color-count mapping after removing colors with zero count.
This approach efficiently handles both adding new colors and removing colors that are no longer in use, giving us the exact count of distinct colors after each query in O(1)
time per query.
Solution Approach
We implement the solution using two hash tables as identified in our intuition:
- Hash table
g
: Maps each ball to its current color (ball → color) - Hash table
cnt
: Tracks the count of balls for each color (color → count)
Step-by-step implementation:
For each query [x, y]
in queries
:
-
Increment the count for the new color:
cnt[y] += 1
- This increases the count for colory
by 1
-
Handle the ball's previous color (if any):
- Check if ball
x
was already colored:if x in g
- If yes, decrease the count of its old color:
cnt[g[x]] -= 1
- If the old color's count drops to 0, remove it from
cnt
:if cnt[g[x]] == 0: cnt.pop(g[x])
- This ensures we only track colors that are actively in use
- Check if ball
-
Update the ball's color:
g[x] = y
- Record that ballx
now has colory
-
Record the number of distinct colors:
ans.append(len(cnt))
- The size ofcnt
gives us the number of distinct colors currently in use
Why this works:
- The
cnt
hash table only contains colors with positive counts - When a color's count reaches 0, we immediately remove it
- Therefore,
len(cnt)
always gives us the exact number of distinct colors
Time Complexity: O(n)
where n is the number of queries, as each query is processed in O(1)
time
Space Complexity: O(min(n, limit))
for storing the ball-color mappings and color counts
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 trace through a small example with limit = 3
and queries [[1, 3], [2, 3], [1, 4], [2, 4]]
.
Initial State:
g = {}
(tracks which color each ball has)cnt = {}
(tracks how many balls have each color)ans = []
(stores results)
Query 1: [1, 3] - Color ball 1 with color 3
- Increment count for color 3:
cnt[3] = 1
→cnt = {3: 1}
- Ball 1 has no previous color (not in
g
), so skip the removal step - Update ball 1's color:
g[1] = 3
→g = {1: 3}
- Number of distinct colors =
len(cnt) = 1
ans = [1]
Query 2: [2, 3] - Color ball 2 with color 3
- Increment count for color 3:
cnt[3] = 2
→cnt = {3: 2}
- Ball 2 has no previous color (not in
g
), so skip the removal step - Update ball 2's color:
g[2] = 3
→g = {1: 3, 2: 3}
- Number of distinct colors =
len(cnt) = 1
ans = [1, 1]
Query 3: [1, 4] - Recolor ball 1 from color 3 to color 4
- Increment count for color 4:
cnt[4] = 1
→cnt = {3: 2, 4: 1}
- Ball 1 has previous color 3:
- Decrease count:
cnt[3] = 1
→cnt = {3: 1, 4: 1}
- Count is not 0, so keep color 3 in
cnt
- Decrease count:
- Update ball 1's color:
g[1] = 4
→g = {1: 4, 2: 3}
- Number of distinct colors =
len(cnt) = 2
ans = [1, 1, 2]
Query 4: [2, 4] - Recolor ball 2 from color 3 to color 4
- Increment count for color 4:
cnt[4] = 2
→cnt = {3: 1, 4: 2}
- Ball 2 has previous color 3:
- Decrease count:
cnt[3] = 0
→cnt = {3: 0, 4: 2}
- Count is 0, so remove color 3:
cnt = {4: 2}
- Decrease count:
- Update ball 2's color:
g[2] = 4
→g = {1: 4, 2: 4}
- Number of distinct colors =
len(cnt) = 1
ans = [1, 1, 2, 1]
Final Result: [1, 1, 2, 1]
The key insight demonstrated here is how color 3 gets removed from tracking when its count drops to 0 (Query 4), ensuring we accurately count only the colors that are actively in use.
Solution Implementation
1from typing import List
2from collections import Counter
3
4class Solution:
5 def queryResults(self, limit: int, queries: List[List[int]]) -> List[int]:
6 # Dictionary to store ball -> color mapping
7 ball_to_color = {}
8
9 # Counter to track how many balls have each color
10 color_count = Counter()
11
12 # Result list to store distinct color counts after each query
13 result = []
14
15 # Process each query [ball_number, new_color]
16 for ball_number, new_color in queries:
17 # Increment count for the new color
18 color_count[new_color] += 1
19
20 # If ball already had a color, update the color counts
21 if ball_number in ball_to_color:
22 old_color = ball_to_color[ball_number]
23 color_count[old_color] -= 1
24
25 # Remove color from counter if no balls have it anymore
26 if color_count[old_color] == 0:
27 color_count.pop(old_color)
28
29 # Update ball's color to the new color
30 ball_to_color[ball_number] = new_color
31
32 # Add count of distinct colors to result
33 result.append(len(color_count))
34
35 return result
36
1class Solution {
2 public int[] queryResults(int limit, int[][] queries) {
3 // Map to store ball number -> color mapping
4 Map<Integer, Integer> ballToColor = new HashMap<>();
5
6 // Map to store color -> count of balls with that color
7 Map<Integer, Integer> colorCount = new HashMap<>();
8
9 int numberOfQueries = queries.length;
10 int[] result = new int[numberOfQueries];
11
12 // Process each query
13 for (int i = 0; i < numberOfQueries; i++) {
14 int ballNumber = queries[i][0];
15 int newColor = queries[i][1];
16
17 // Increment count for the new color
18 colorCount.merge(newColor, 1, Integer::sum);
19
20 // If ball already had a color, handle the old color
21 if (ballToColor.containsKey(ballNumber)) {
22 int oldColor = ballToColor.get(ballNumber);
23
24 // Decrement count for the old color
25 int updatedCount = colorCount.merge(oldColor, -1, Integer::sum);
26
27 // Remove color from map if no balls have this color anymore
28 if (updatedCount == 0) {
29 colorCount.remove(oldColor);
30 }
31 }
32
33 // Assign new color to the ball
34 ballToColor.put(ballNumber, newColor);
35
36 // Store number of distinct colors after this query
37 result[i] = colorCount.size();
38 }
39
40 return result;
41 }
42}
43
1class Solution {
2public:
3 vector<int> queryResults(int limit, vector<vector<int>>& queries) {
4 // Map to store ball number -> color mapping
5 unordered_map<int, int> ballToColor;
6
7 // Map to count how many balls have each color
8 unordered_map<int, int> colorCount;
9
10 // Result vector to store the number of distinct colors after each query
11 vector<int> result;
12
13 // Process each query
14 for (auto& query : queries) {
15 int ballNumber = query[0];
16 int newColor = query[1];
17
18 // Increment count for the new color
19 colorCount[newColor]++;
20
21 // If the ball already had a color, handle the old color
22 if (ballToColor.contains(ballNumber)) {
23 int oldColor = ballToColor[ballNumber];
24
25 // Decrement count for the old color
26 colorCount[oldColor]--;
27
28 // If no balls have the old color anymore, remove it from the map
29 if (colorCount[oldColor] == 0) {
30 colorCount.erase(oldColor);
31 }
32 }
33
34 // Update the ball's color to the new color
35 ballToColor[ballNumber] = newColor;
36
37 // Add the current number of distinct colors to the result
38 result.push_back(colorCount.size());
39 }
40
41 return result;
42 }
43};
44
1/**
2 * Processes queries to track ball movements between boxes and returns the count of non-empty boxes after each query
3 * @param limit - The maximum number of boxes (currently unused in the logic)
4 * @param queries - Array of [ballId, boxId] pairs representing ball placement operations
5 * @returns Array containing the count of non-empty boxes after each query
6 */
7function queryResults(limit: number, queries: number[][]): number[] {
8 // Map to track which box each ball is currently in (ballId -> boxId)
9 const ballToBoxMap = new Map<number, number>();
10
11 // Map to track the count of balls in each box (boxId -> count)
12 const boxBallCount = new Map<number, number>();
13
14 // Array to store the result after each query
15 const results: number[] = [];
16
17 // Process each query
18 for (const [ballId, targetBoxId] of queries) {
19 // Increment the ball count for the target box
20 boxBallCount.set(targetBoxId, (boxBallCount.get(targetBoxId) ?? 0) + 1);
21
22 // Check if the ball was previously in another box
23 if (ballToBoxMap.has(ballId)) {
24 const previousBoxId = ballToBoxMap.get(ballId)!;
25
26 // Decrement the ball count from the previous box
27 boxBallCount.set(previousBoxId, boxBallCount.get(previousBoxId)! - 1);
28
29 // Remove the box from the count map if it becomes empty
30 if (boxBallCount.get(previousBoxId) === 0) {
31 boxBallCount.delete(previousBoxId);
32 }
33 }
34
35 // Update the ball's current location
36 ballToBoxMap.set(ballId, targetBoxId);
37
38 // Add the count of non-empty boxes to the results
39 results.push(boxBallCount.size);
40 }
41
42 return results;
43}
44
Time and Space Complexity
The time complexity is O(m)
, where m
is the length of the array queries
. Each query involves:
- Dictionary operations (
g[x]
lookup, assignment) which areO(1)
on average - Counter operations (
cnt[y] += 1
,cnt[g[x]] -= 1
,cnt.pop()
) which areO(1)
on average - Getting the length of the Counter with
len(cnt)
which isO(1)
- Appending to the answer list which is
O(1)
Since we perform constant time operations for each of the m
queries, the overall time complexity is O(m)
.
The space complexity is O(m)
, where m
is the length of the array queries
. This is because:
- Dictionary
g
can store at mostm
key-value pairs (one for each unique ball number in the queries) - Counter
cnt
can store at mostm
entries (one for each unique color in the queries) - List
ans
stores exactlym
results (one for each query)
In the worst case, all queries could have unique ball numbers and unique colors, leading to space usage proportional to m
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Forgetting to Handle Color Replacement
Issue: A common mistake is treating each query as simply adding a new color to a ball, without properly handling the case where the ball already had a different color. This leads to overcounting distinct colors.
Incorrect approach:
def queryResults(self, limit: int, queries: List[List[int]]) -> List[int]:
ball_to_color = {}
color_count = Counter()
result = []
for ball_number, new_color in queries:
# Wrong: Just adding the new color without removing the old one
ball_to_color[ball_number] = new_color
color_count[new_color] += 1
result.append(len(color_count))
return result
Why it fails: If ball 0 changes from color 1 to color 2, this approach would count both colors as being in use, even though color 1 is no longer present on any ball.
Solution: Always check if the ball had a previous color and properly decrement its count:
if ball_number in ball_to_color: old_color = ball_to_color[ball_number] color_count[old_color] -= 1 if color_count[old_color] == 0: color_count.pop(old_color)
Pitfall 2: Not Removing Colors with Zero Count
Issue: Keeping colors with zero count in the counter leads to incorrect distinct color counts.
Incorrect approach:
if ball_number in ball_to_color: old_color = ball_to_color[ball_number] color_count[old_color] -= 1 # Forgot to remove the color when count reaches 0!
Why it fails: len(color_count)
would include colors that aren't actually on any balls, giving inflated results.
Solution: Always remove colors from the counter when their count reaches zero:
if color_count[old_color] == 0: color_count.pop(old_color)
Pitfall 3: Order of Operations Error
Issue: Updating the ball's color mapping before handling the old color removal.
Incorrect approach:
for ball_number, new_color in queries: # Wrong: Updating ball color first old_color = ball_to_color.get(ball_number) ball_to_color[ball_number] = new_color # This overwrites the old color! if old_color is not None: color_count[old_color] -= 1 # ...rest of logic
Why it fails: While this particular version might work if you save the old color first, it's error-prone and can lead to confusion or bugs if the code is modified later.
Solution: Follow a clear sequence:
- First, increment the new color count
- Then, handle the old color (if exists)
- Finally, update the ball's color mapping
This ensures the logic flow is clear and maintainable.
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
Recommended Readings
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
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!