2936. Number of Equal Numbers Blocks 🔒
Problem Description
You are given a very large array nums
with a special property: all occurrences of the same value appear consecutively. This means if a value appears multiple times in the array, all those occurrences must be adjacent to each other with no other values in between.
Since the array is extremely large, you cannot access it directly. Instead, you're provided with a BigArray
class that has two methods:
at(index)
: Returns the value at the given indexsize()
: Returns the total length of the array
Your task is to count how many distinct blocks of equal values exist in the array. A block is a maximal contiguous segment where all elements have the same value.
For example:
- If the array is
[1, 1, 1, 2, 2, 3, 3, 3, 3]
, there are 3 blocks:[1,1,1]
,[2,2]
, and[3,3,3,3]
- If the array is
[5, 5, 4, 4, 4, 7]
, there are 3 blocks:[5,5]
,[4,4,4]
, and[7]
The solution uses binary search to efficiently find the boundaries of each block. Starting from index 0, for each new block encountered, it uses binary search to find where that block ends (the first index where the value changes), then moves to the next block. This approach is efficient for very large arrays since it doesn't need to check every single element when dealing with large blocks of the same value.
The time complexity is O(b * log(n))
where b
is the number of blocks and n
is the array size, which is much better than O(n)
when there are long sequences of repeated values.
Intuition
The key insight comes from recognizing that we're dealing with a potentially massive array where consecutive equal values form blocks. If we naively check every single element, we'd need O(n)
operations, which could be inefficient for very large arrays.
Think about it this way: if we encounter a block of one million consecutive 5
s, do we really need to check all million elements? No! We just need to find where this block ends.
This leads us to the binary search approach. When we're at the start of a block with value x
, we know that:
- Elements equal to
x
will continue for some distance - At some point, we'll hit an element that's not equal to
x
(marking the end of the block)
We can use binary search to find this boundary efficiently. The search space is from our current position to the end of the array. We're looking for the first index where the value changes from x
.
The binary search works because of the problem's guarantee: all occurrences of a value are adjacent. This means once we find a value different from x
, we know we've passed the entire block of x
s, and there won't be any more x
s later in the array.
For each block, instead of checking potentially millions of elements one by one (O(block_size)
), we can find the block's end in just O(log(n))
operations using binary search. This is especially powerful when blocks are large.
The bisect_left
function with a custom key lambda j: nums.at(j) != x
cleverly searches for the first position where the condition "value is not equal to x" becomes True
, which is exactly where our current block ends.
Learn more about Binary Search patterns.
Solution Approach
The solution uses binary search to efficiently find the boundaries of each block of equal values. Here's how the implementation works:
Main Algorithm Flow:
-
Initialize variables:
i = 0
: current position in the arrayn = nums.size()
: total array lengthans = 0
: counter for number of blocks
-
While
i < n
, process each block:- Increment the block counter:
ans += 1
- Get the current value:
x = nums.at(i)
- Check if we can quickly move to the next block or need binary search
- Increment the block counter:
Key Decision Point:
The code makes a smart optimization by checking nums.at(i + 1)
:
- If
nums.at(i + 1) != x
: We know the current block has only one element, so we can simply move to the next position (i += 1
) - Otherwise: The block has multiple elements, so we use binary search to find its end
Binary Search Implementation:
When a block has multiple elements, we use bisect_left
to find the block's boundary:
i += bisect_left(range(i, n), True, key=lambda j: nums.at(j) != x)
This binary search:
- Searches in the range
[i, n)
- Uses a key function
lambda j: nums.at(j) != x
that returnsTrue
when we find a value different fromx
bisect_left
finds the leftmost position where the key function returnsTrue
- This gives us the offset from position
i
to the first position with a different value - We add this offset to
i
to jump directly to the start of the next block
Why This Works:
The algorithm leverages the problem's guarantee that all occurrences of a value are consecutive. Once we find where a block ends, we know:
- All elements before that position in the current search range have value
x
- The element at the found position (if it exists) has a different value
- We can skip all the intermediate elements and jump directly to the next block
Time Complexity:
- For each block, we either do a constant-time check (for single-element blocks) or a binary search taking
O(log n)
time - Total complexity:
O(b * log n)
whereb
is the number of blocks - This is much better than
O(n)
when blocks are large
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 the algorithm with the array [2, 2, 2, 7, 7, 3, 3, 3, 3, 3]
which has length 10.
Initial State:
i = 0
,n = 10
,ans = 0
First Block (value = 2):
- Increment counter:
ans = 1
- Current value:
x = nums.at(0) = 2
- Check next element:
nums.at(1) = 2
(same as x) - Since the block continues, use binary search to find where it ends
- Binary search in range [0, 10) looking for first index where value ≠2:
- Mid = 5:
nums.at(5) = 3
(≠2) → search left half - Mid = 2:
nums.at(2) = 2
(= 2) → search right half - Mid = 3:
nums.at(3) = 7
(≠2) → search left half - Found: index 3 is the first position where value ≠2
- Mid = 5:
- Jump to index 3:
i = 0 + 3 = 3
Second Block (value = 7):
- Increment counter:
ans = 2
- Current value:
x = nums.at(3) = 7
- Check next element:
nums.at(4) = 7
(same as x) - Use binary search in range [3, 10) looking for first index where value ≠7:
- Mid = 6:
nums.at(6) = 3
(≠7) → search left half - Mid = 4:
nums.at(4) = 7
(= 7) → search right half - Mid = 5:
nums.at(5) = 3
(≠7) → found
- Mid = 6:
- Jump to index 5:
i = 3 + (5-3) = 5
Third Block (value = 3):
- Increment counter:
ans = 3
- Current value:
x = nums.at(5) = 3
- Check next element:
nums.at(6) = 3
(same as x) - Use binary search in range [5, 10) looking for first index where value ≠3:
- Mid = 7:
nums.at(7) = 3
(= 3) → search right half - Mid = 9:
nums.at(9) = 3
(= 3) → search right half - Search reaches end of array at index 10
- Mid = 7:
- Jump to index 10:
i = 5 + (10-5) = 10
Termination:
i = 10
which equalsn
, so we exit the loop- Return
ans = 3
The algorithm correctly identified 3 blocks: [2,2,2]
, [7,7]
, and [3,3,3,3,3]
, making only 11 array accesses instead of checking all 10 elements sequentially.
Solution Implementation
1from bisect import bisect_left
2from typing import Optional
3
4# Definition for BigArray.
5# class BigArray:
6# def at(self, index: int) -> int:
7# pass
8# def size(self) -> int:
9# pass
10
11class Solution:
12 def countBlocks(self, nums: Optional["BigArray"]) -> int:
13 """
14 Count the number of contiguous blocks of equal elements in a BigArray.
15
16 A block is defined as a maximal contiguous subarray where all elements are equal.
17
18 Args:
19 nums: A BigArray object with at() and size() methods
20
21 Returns:
22 The number of distinct blocks in the array
23 """
24 # Initialize index pointer and get total size
25 current_index = 0
26 total_size = nums.size()
27 block_count = 0
28
29 # Iterate through the array
30 while current_index < total_size:
31 # Each iteration represents a new block
32 block_count += 1
33
34 # Get the value at current position
35 current_value = nums.at(current_index)
36
37 # Check if next element is different (small optimization for single elements)
38 if current_index + 1 < total_size and nums.at(current_index + 1) != current_value:
39 # If next element is different, this block has only one element
40 current_index += 1
41 else:
42 # Use binary search to find the end of the current block
43 # bisect_left finds the first position where the condition becomes True
44 # The condition checks if element at position j is different from current_value
45 block_length = bisect_left(
46 range(current_index, total_size),
47 True,
48 key=lambda j: nums.at(j) != current_value
49 )
50
51 # Move index to the start of the next block
52 current_index += block_length
53
54 return block_count
55
1/**
2 * Definition for BigArray.
3 * class BigArray {
4 * public BigArray(int[] elements);
5 * public int at(long index);
6 * public long size();
7 * }
8 */
9class Solution {
10 /**
11 * Counts the number of contiguous blocks in the array where each block
12 * contains identical consecutive elements.
13 *
14 * @param nums The BigArray to analyze
15 * @return The total number of distinct blocks
16 */
17 public int countBlocks(BigArray nums) {
18 int blockCount = 0;
19 long currentIndex = 0;
20 long arraySize = nums.size();
21
22 // Iterate through the array, jumping to the end of each block
23 while (currentIndex < arraySize) {
24 // Find the end of the current block of identical elements
25 currentIndex = findBlockEnd(nums, currentIndex, arraySize);
26 blockCount++;
27 }
28
29 return blockCount;
30 }
31
32 /**
33 * Uses binary search to find the index of the first element that differs
34 * from the element at the starting position.
35 *
36 * @param nums The BigArray to search
37 * @param left The starting index of the current block
38 * @param arraySize The total size of the array
39 * @return The index of the first different element, or arraySize if all remaining elements are the same
40 */
41 private long findBlockEnd(BigArray nums, long left, long arraySize) {
42 long right = arraySize;
43 int targetValue = nums.at(left);
44
45 // Binary search to find the boundary of the current block
46 while (left < right) {
47 long mid = (left + right) >> 1; // Equivalent to (left + right) / 2
48
49 if (nums.at(mid) != targetValue) {
50 // Different value found, search in the left half
51 right = mid;
52 } else {
53 // Same value, continue searching in the right half
54 left = mid + 1;
55 }
56 }
57
58 return left;
59 }
60}
61
1/**
2 * Definition for BigArray.
3 * class BigArray {
4 * public:
5 * BigArray(vector<int> elements);
6 * int at(long long index);
7 * long long size();
8 * };
9 */
10class Solution {
11public:
12 int countBlocks(BigArray* nums) {
13 // Type alias for long long to improve readability
14 using ll = long long;
15
16 // Get the total size of the array
17 ll arraySize = nums->size();
18
19 // Counter for the number of blocks
20 int blockCount = 0;
21
22 // Lambda function to find the end index of a block starting at 'startIdx'
23 // Uses binary search to find the first index where the value changes
24 auto findBlockEnd = [&](ll startIdx) -> ll {
25 ll left = startIdx;
26 ll right = arraySize;
27 int targetValue = nums->at(startIdx);
28
29 // Binary search to find the first position where value differs from targetValue
30 while (left < right) {
31 ll mid = (left + right) >> 1; // Equivalent to (left + right) / 2
32
33 if (nums->at(mid) != targetValue) {
34 // Value changed, search in the left half
35 right = mid;
36 } else {
37 // Value is still the same, search in the right half
38 left = mid + 1;
39 }
40 }
41
42 // Return the index of the first different element (or arraySize if all remaining are same)
43 return left;
44 };
45
46 // Iterate through the array, jumping from one block to the next
47 for (ll currentIdx = 0; currentIdx < arraySize; ++blockCount) {
48 // Find where the current block ends and jump to the next block
49 currentIdx = findBlockEnd(currentIdx);
50 }
51
52 return blockCount;
53 }
54};
55
1/**
2 * Counts the number of contiguous blocks of identical elements in a BigArray.
3 * A block is defined as a contiguous sequence of identical elements.
4 *
5 * @param nums - The BigArray to analyze (can be null)
6 * @returns The number of contiguous blocks in the array
7 */
8function countBlocks(nums: BigArray | null): number {
9 // Handle null case
10 if (!nums) {
11 return 0;
12 }
13
14 // Get the total size of the array
15 const arraySize: number = nums.size();
16
17 /**
18 * Binary search to find the end index (exclusive) of a block starting at leftIndex.
19 * Returns the index of the first element that differs from the element at leftIndex.
20 *
21 * @param leftIndex - The starting index of the current block
22 * @returns The index right after the last element of the current block
23 */
24 const findBlockEnd = (leftIndex: number): number => {
25 let rightIndex: number = arraySize;
26 const targetValue: number = nums.at(leftIndex);
27
28 // Binary search to find the boundary where the value changes
29 while (leftIndex < rightIndex) {
30 const midIndex: number = leftIndex + Math.floor((rightIndex - leftIndex) / 2);
31
32 if (nums.at(midIndex) !== targetValue) {
33 // Value changed, search in the left half
34 rightIndex = midIndex;
35 } else {
36 // Same value, search in the right half
37 leftIndex = midIndex + 1;
38 }
39 }
40
41 return leftIndex;
42 };
43
44 // Count the number of blocks by jumping from one block to the next
45 let blockCount: number = 0;
46 let currentIndex: number = 0;
47
48 while (currentIndex < arraySize) {
49 // Find the end of the current block and move to the next block
50 currentIndex = findBlockEnd(currentIndex);
51 blockCount++;
52 }
53
54 return blockCount;
55}
56
Time and Space Complexity
Time Complexity: O(m × log n)
, where m
is the number of distinct blocks (consecutive elements with the same value) in the array, and n
is the length of the array.
The algorithm iterates through the array and for each distinct block:
- When encountering a new value that differs from the next element (the
if
branch), it moves forward by 1 position inO(1)
time - When encountering a value that continues in a block (the
else
branch), it uses binary search viabisect_left
to find the end of the current block, which takesO(log n)
time
Since we process each of the m
blocks exactly once, and in the worst case each block requires a binary search operation, the total time complexity is O(m × log n)
.
Space Complexity: O(1)
The algorithm only uses a constant amount of extra space for variables i
, n
, ans
, and x
, regardless of the input size. The bisect_left
operation with the range
object and lambda function doesn't create additional space proportional to the input size, as the range object is a lazy iterator and the lambda function is evaluated on-the-fly.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Binary Search Range
A critical pitfall in this solution is the incorrect use of bisect_left
with range(current_index, total_size)
. The bisect_left
function returns the index within the provided sequence where the element should be inserted. When using range(current_index, total_size)
, this creates a sequence [current_index, current_index+1, ..., total_size-1]
, and bisect_left
returns an index relative to this range (0-based), not the actual array index.
Problem Example:
If current_index = 5
and the block ends at index 8, bisect_left
returns 3 (since 8 is at position 3 in the range starting from 5), but the code adds this directly to current_index
, resulting in current_index = 8
instead of the correct current_index = 9
.
Solution:
# Incorrect:
block_length = bisect_left(
range(current_index, total_size),
True,
key=lambda j: nums.at(j) != current_value
)
current_index += block_length
# Correct:
next_block_index = bisect_left(
range(current_index, total_size),
True,
key=lambda j: nums.at(j) != current_value
)
current_index = current_index + next_block_index
2. Edge Case: Last Block Extends to End of Array
When the last block extends to the end of the array, bisect_left
will return the length of the search range (since no different value is found). This could cause issues if not handled properly.
Solution:
The current implementation actually handles this correctly because when bisect_left
returns the full range length, adding it to current_index
will make it equal to or exceed total_size
, properly terminating the loop.
3. Optimization Check Boundary Issue
The optimization check nums.at(current_index + 1)
could throw an out-of-bounds error if not properly bounded.
Correct Implementation:
# Always check bounds before accessing if current_index + 1 < total_size and nums.at(current_index + 1) != current_value: current_index += 1
4. Binary Search for Single-Element Blocks
Using binary search for single-element blocks is inefficient. The optimization check helps, but if implemented incorrectly (e.g., forgetting the bounds check), it could lead to unnecessary binary searches.
Better Approach: Consider using a two-pointer approach or always checking the immediate next element first before resorting to binary search, which the current solution does correctly.
Which technique can we use to find the middle of a linked list?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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!