Facebook Pixel

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 index
  • size(): 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 5s, 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 xs, and there won't be any more xs 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:

  1. Initialize variables:

    • i = 0: current position in the array
    • n = nums.size(): total array length
    • ans = 0: counter for number of blocks
  2. 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

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 returns True when we find a value different from x
  • bisect_left finds the leftmost position where the key function returns True
  • 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:

  1. All elements before that position in the current search range have value x
  2. The element at the found position (if it exists) has a different value
  3. 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) where b 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 Evaluator

Example 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
  • 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
  • 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
  • Jump to index 10: i = 5 + (10-5) = 10

Termination:

  • i = 10 which equals n, 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 in O(1) time
  • When encountering a value that continues in a block (the else branch), it uses binary search via bisect_left to find the end of the current block, which takes O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More