Facebook Pixel

2519. Count the Number of K-Big Indices 🔒

Problem Description

You are given a 0-indexed integer array nums and a positive integer k.

An index i is called k-big if it satisfies both of these conditions:

  1. There are at least k different indices before position i (indices less than i) where the values at those indices are smaller than nums[i]
  2. There are at least k different indices after position i (indices greater than i) where the values at those indices are smaller than nums[i]

In other words, for an index to be k-big, the element at that index must have at least k smaller elements on its left side AND at least k smaller elements on its right side in the array.

Your task is to count how many indices in the array are k-big and return this count.

For example, if nums = [2, 3, 6, 5, 2, 3] and k = 2:

  • Index 2 (value 6): Has 2 smaller values on the left (2, 3) and 2 smaller values on the right (5, 2, 3 where 2 and 3 are smaller). This is k-big.
  • Index 3 (value 5): Has 2 smaller values on the left (2, 3) and 2 smaller values on the right (2, 3). This is k-big.
  • Other indices don't satisfy both conditions with at least k=2 smaller elements on each side.

The solution uses two Binary Indexed Trees (Fenwick Trees) to efficiently count the number of elements smaller than the current element on both the left and right sides. One tree tracks elements seen so far (left side), while the other initially contains all elements and removes them as we traverse (right side).

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

Intuition

The key insight is that for each index, we need to count how many smaller elements exist on its left and right sides. A naive approach would check every pair of indices, resulting in O(n²) time complexity for each position, which is inefficient.

We can think of this problem as maintaining two dynamic sets of numbers - one growing from the left as we traverse, and one shrinking from the right. For each position, we need to quickly query: "How many elements in each set are smaller than the current element?"

This naturally leads us to consider data structures that support:

  1. Efficient insertion/deletion of elements
  2. Efficient range queries (counting elements less than a given value)

A Binary Indexed Tree (Fenwick Tree) is perfect for this scenario because it can:

  • Update values in O(log n) time
  • Query prefix sums (which can count elements) in O(log n) time

The clever approach is to use two trees:

  • Tree1: Starts empty and gradually accumulates elements as we move from left to right. At position i, it contains all elements from indices 0 to i-1, allowing us to count smaller elements on the left.
  • Tree2: Initially contains ALL elements, then removes them one by one as we traverse. At position i, it contains all elements from indices i+1 to n-1, allowing us to count smaller elements on the right.

By traversing the array once and maintaining these two trees, we can determine for each position whether it's k-big by querying both trees for the count of elements smaller than nums[i]. The query tree.query(v - 1) gives us the count of all elements strictly less than v.

This transforms an O(n²) problem into an O(n log n) solution, as we perform O(log n) operations for each of the n elements.

Learn more about Segment Tree, Binary Search, Divide and Conquer and Merge Sort patterns.

Solution Approach

The solution implements two Binary Indexed Trees (Fenwick Trees) to efficiently track elements smaller than the current value on both sides.

Binary Indexed Tree Implementation: The BinaryIndexedTree class provides two key operations:

  • update(x, delta): Adds delta to position x in O(log n) time
  • query(x): Returns the sum of elements from position 1 to x in O(log n) time

Main Algorithm Steps:

  1. Initialize Two Trees:

    • tree1 = BinaryIndexedTree(n) - Initially empty, will track elements on the left
    • tree2 = BinaryIndexedTree(n) - Will track elements on the right
  2. Populate tree2 with All Elements:

    for v in nums:
        tree2.update(v, 1)

    This adds a count of 1 for each value in the array, so tree2 initially represents all elements.

  3. Traverse and Check Each Position:

    for v in nums:
        tree2.update(v, -1)  # Remove current element from right side
        ans += tree1.query(v - 1) >= k and tree2.query(v - 1) >= k
        tree1.update(v, 1)   # Add current element to left side

    For each element v at position i:

    • Remove from Right Side: tree2.update(v, -1) decrements the count, effectively removing this element from the "right side" set
    • Check k-big Condition:
      • tree1.query(v - 1) counts elements less than v on the left
      • tree2.query(v - 1) counts elements less than v on the right
      • If both counts are at least k, increment the answer
    • Add to Left Side: tree1.update(v, 1) adds this element to the "left side" set for future positions

Key Insight: The algorithm cleverly maintains the invariant that at each position i:

  • tree1 contains exactly the elements from indices 0 to i-1
  • tree2 contains exactly the elements from indices i+1 to n-1

This allows us to query both trees to get the exact counts needed to determine if position i is k-big.

Time Complexity: O(n log n) - We perform O(log n) operations for each of the n elements Space Complexity: O(n) - Two trees each storing up to n elements

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with nums = [2, 3, 6, 5, 2, 3] and k = 2.

Initial Setup:

  • Create two Binary Indexed Trees of size 6
  • tree1 starts empty (represents left side)
  • tree2 initially contains all elements: {2:2, 3:2, 5:1, 6:1} (value:count)

Step-by-step traversal:

Position 0 (value = 2):

  • Remove 2 from tree2: {2:1, 3:2, 5:1, 6:1}
  • Check left: tree1.query(1) = 0 elements < 2 (not enough, need k=2)
  • Check right: tree2.query(1) = 1 element < 2 (not enough, need k=2)
  • Not k-big ❌
  • Add 2 to tree1: {2:1}

Position 1 (value = 3):

  • Remove 3 from tree2: {2:1, 3:1, 5:1, 6:1}
  • Check left: tree1.query(2) = 1 element < 3 (not enough, need k=2)
  • Check right: tree2.query(2) = 2 elements < 3 (enough!)
  • Not k-big ❌ (left side insufficient)
  • Add 3 to tree1: {2:1, 3:1}

Position 2 (value = 6):

  • Remove 6 from tree2: {2:1, 3:1, 5:1}
  • Check left: tree1.query(5) = 2 elements < 6 (enough!)
  • Check right: tree2.query(5) = 3 elements < 6 (enough!)
  • Is k-big ✓
  • Add 6 to tree1: {2:1, 3:1, 6:1}

Position 3 (value = 5):

  • Remove 5 from tree2: {2:1, 3:1}
  • Check left: tree1.query(4) = 2 elements < 5 (enough!)
  • Check right: tree2.query(4) = 2 elements < 5 (enough!)
  • Is k-big ✓
  • Add 5 to tree1: {2:1, 3:1, 5:1, 6:1}

Position 4 (value = 2):

  • Remove 2 from tree2: {3:1}
  • Check left: tree1.query(1) = 1 element < 2 (not enough, need k=2)
  • Check right: tree2.query(1) = 0 elements < 2 (not enough, need k=2)
  • Not k-big ❌
  • Add 2 to tree1: {2:2, 3:1, 5:1, 6:1}

Position 5 (value = 3):

  • Remove 3 from tree2: {} (empty)
  • Check left: tree1.query(2) = 2 elements < 3 (enough!)
  • Check right: tree2.query(2) = 0 elements < 3 (not enough, need k=2)
  • Not k-big ❌ (right side insufficient)
  • Add 3 to tree1: {2:2, 3:2, 5:1, 6:1}

Result: 2 indices are k-big (positions 2 and 3)

The key observation is how the trees dynamically maintain the left and right sides: tree1 grows as we add processed elements, while tree2 shrinks as we remove the current element, ensuring accurate counts for each position.

Solution Implementation

1from typing import List
2
3
4class BinaryIndexedTree:
5    """
6    Binary Indexed Tree (Fenwick Tree) for efficient range sum queries
7    and point updates in O(log n) time.
8    """
9  
10    def __init__(self, n: int) -> None:
11        """
12        Initialize a Binary Indexed Tree with size n.
13      
14        Args:
15            n: The maximum value that can be stored (1-indexed)
16        """
17        self.size = n
18        # Tree array with size n+1 (1-indexed for easier bit manipulation)
19        self.tree = [0] * (n + 1)
20  
21    def update(self, index: int, delta: int) -> None:
22        """
23        Add delta to the value at position index.
24      
25        Args:
26            index: The position to update (1-indexed)
27            delta: The value to add to the position
28        """
29        # Traverse all affected nodes in the tree
30        while index <= self.size:
31            self.tree[index] += delta
32            # Move to the next node that this index affects
33            # index & -index gives the lowest set bit
34            index += index & -index
35  
36    def query(self, index: int) -> int:
37        """
38        Get the prefix sum from 1 to index (inclusive).
39      
40        Args:
41            index: The end position of the range (1-indexed)
42          
43        Returns:
44            The sum of elements from position 1 to index
45        """
46        result = 0
47        # Traverse and accumulate values from the tree
48        while index > 0:
49            result += self.tree[index]
50            # Move to the parent node by removing the lowest set bit
51            index -= index & -index
52        return result
53
54
55class Solution:
56    def kBigIndices(self, nums: List[int], k: int) -> int:
57        """
58        Find the number of k-big indices in the array.
59        A k-big index i is an index where there are at least k elements
60        smaller than nums[i] before position i, and at least k elements
61        smaller than nums[i] after position i.
62      
63        Args:
64            nums: The input array of integers
65            k: The threshold for k-big indices
66          
67        Returns:
68            The count of k-big indices in the array
69        """
70        n = len(nums)
71      
72        # Tree for tracking elements to the left of current position
73        left_tree = BinaryIndexedTree(n)
74      
75        # Tree for tracking elements to the right of current position
76        right_tree = BinaryIndexedTree(n)
77      
78        # Initially, all elements are to the right of position 0
79        for value in nums:
80            right_tree.update(value, 1)
81      
82        k_big_count = 0
83      
84        # Process each element in the array
85        for value in nums:
86            # Remove current element from the right tree
87            # (as we're now at this position)
88            right_tree.update(value, -1)
89          
90            # Check if current index is k-big:
91            # - left_tree.query(value - 1): count of elements < value on the left
92            # - right_tree.query(value - 1): count of elements < value on the right
93            if left_tree.query(value - 1) >= k and right_tree.query(value - 1) >= k:
94                k_big_count += 1
95          
96            # Add current element to the left tree
97            # (as we're moving past this position)
98            left_tree.update(value, 1)
99      
100        return k_big_count
101
1/**
2 * Binary Indexed Tree (Fenwick Tree) implementation for efficient range sum queries
3 * and point updates in O(log n) time.
4 */
5class BinaryIndexedTree {
6    private int size;
7    private int[] tree;
8
9    /**
10     * Constructor to initialize the Binary Indexed Tree
11     * @param n The maximum value that can be stored (1-indexed)
12     */
13    public BinaryIndexedTree(int n) {
14        this.size = n;
15        // Array is 1-indexed for easier BIT operations
16        this.tree = new int[n + 1];
17    }
18
19    /**
20     * Updates the value at index x by adding delta
21     * @param x The index to update (1-indexed)
22     * @param delta The value to add at index x
23     */
24    public void update(int x, int delta) {
25        // Traverse all affected nodes in the tree
26        while (x <= size) {
27            tree[x] += delta;
28            // Move to next node affected by this update
29            // x & -x gives the rightmost set bit
30            x += x & -x;
31        }
32    }
33
34    /**
35     * Queries the prefix sum from index 1 to x
36     * @param x The ending index for the range sum (1-indexed)
37     * @return The sum of elements from index 1 to x
38     */
39    public int query(int x) {
40        int sum = 0;
41        // Traverse the tree to calculate prefix sum
42        while (x > 0) {
43            sum += tree[x];
44            // Move to parent node by removing rightmost set bit
45            x -= x & -x;
46        }
47        return sum;
48    }
49}
50
51/**
52 * Solution class to find k-big indices in an array
53 * A k-big index i satisfies:
54 * - There are at least k elements before i that are smaller than nums[i]
55 * - There are at least k elements after i that are smaller than nums[i]
56 */
57class Solution {
58    /**
59     * Finds the count of k-big indices in the given array
60     * @param nums The input array
61     * @param k The threshold value for k-big indices
62     * @return The number of k-big indices in the array
63     */
64    public int kBigIndices(int[] nums, int k) {
65        int arrayLength = nums.length;
66      
67        // Tree to track elements seen so far (left side)
68        BinaryIndexedTree leftTree = new BinaryIndexedTree(arrayLength);
69      
70        // Tree to track elements not yet processed (right side)
71        BinaryIndexedTree rightTree = new BinaryIndexedTree(arrayLength);
72      
73        // Initially, all elements are on the right side
74        for (int value : nums) {
75            rightTree.update(value, 1);
76        }
77      
78        int kBigCount = 0;
79      
80        // Process each element in the array
81        for (int currentValue : nums) {
82            // Remove current element from right tree (as we're processing it)
83            rightTree.update(currentValue, -1);
84          
85            // Check if current index is a k-big index:
86            // - leftTree.query(currentValue - 1): count of elements < currentValue on the left
87            // - rightTree.query(currentValue - 1): count of elements < currentValue on the right
88            if (leftTree.query(currentValue - 1) >= k && 
89                rightTree.query(currentValue - 1) >= k) {
90                kBigCount++;
91            }
92          
93            // Add current element to left tree (as it's now processed)
94            leftTree.update(currentValue, 1);
95        }
96      
97        return kBigCount;
98    }
99}
100
1class BinaryIndexedTree {
2public:
3    // Constructor: Initialize BIT with size n
4    BinaryIndexedTree(int n) : size_(n), tree_(n + 1, 0) {}
5
6    // Update: Add delta to position x (1-indexed)
7    void update(int x, int delta) {
8        // Propagate the update upwards through the tree
9        while (x <= size_) {
10            tree_[x] += delta;
11            x += x & -x;  // Move to next node by adding the lowest set bit
12        }
13    }
14
15    // Query: Get prefix sum from 1 to x (inclusive)
16    int query(int x) {
17        int sum = 0;
18        // Traverse downwards accumulating values
19        while (x > 0) {
20            sum += tree_[x];
21            x -= x & -x;  // Move to parent by removing the lowest set bit
22        }
23        return sum;
24    }
25
26private:
27    int size_;           // Size of the array
28    vector<int> tree_;   // BIT storage (1-indexed)
29};
30
31class Solution {
32public:
33    // Find count of indices where there are at least k smaller elements
34    // both to the left and to the right
35    int kBigIndices(vector<int>& nums, int k) {
36        int n = nums.size();
37      
38        // Tree to track elements seen so far (left side)
39        BinaryIndexedTree* leftTree = new BinaryIndexedTree(n);
40      
41        // Tree to track elements not yet processed (right side)
42        BinaryIndexedTree* rightTree = new BinaryIndexedTree(n);
43      
44        // Initially, all elements are on the right side
45        for (int& value : nums) {
46            rightTree->update(value, 1);
47        }
48      
49        int result = 0;
50      
51        // Process each element
52        for (int& value : nums) {
53            // Remove current element from right side
54            rightTree->update(value, -1);
55          
56            // Check if current position satisfies k-big condition:
57            // - At least k elements smaller than value on the left
58            // - At least k elements smaller than value on the right
59            bool hasKSmallerOnLeft = leftTree->query(value - 1) >= k;
60            bool hasKSmallerOnRight = rightTree->query(value - 1) >= k;
61          
62            if (hasKSmallerOnLeft && hasKSmallerOnRight) {
63                result++;
64            }
65          
66            // Add current element to left side for next iteration
67            leftTree->update(value, 1);
68        }
69      
70        return result;
71    }
72};
73
1// Binary Indexed Tree (Fenwick Tree) for efficient range sum queries
2// Array to store the tree values (1-indexed)
3let treeSize: number;
4let treeArray: number[];
5
6/**
7 * Initialize the Binary Indexed Tree with given size
8 * @param n - The size of the tree
9 */
10function initTree(n: number): void {
11    treeSize = n;
12    treeArray = new Array(n + 1).fill(0);
13}
14
15/**
16 * Update the tree by adding delta to position x
17 * Uses the lowbit operation to traverse parent nodes
18 * @param x - The position to update (1-indexed)
19 * @param delta - The value to add
20 */
21function update(x: number, delta: number): void {
22    while (x <= treeSize) {
23        treeArray[x] += delta;
24        // Move to next position by adding lowbit
25        x += x & -x;
26    }
27}
28
29/**
30 * Query the prefix sum from index 1 to x
31 * @param x - The end position of the range (1-indexed)
32 * @returns The sum of elements from 1 to x
33 */
34function query(x: number): number {
35    let sum = 0;
36    while (x > 0) {
37        sum += treeArray[x];
38        // Move to parent by removing lowbit
39        x -= x & -x;
40    }
41    return sum;
42}
43
44/**
45 * Find the count of k-big indices in the array
46 * A k-big index i means there are at least k elements smaller than nums[i] 
47 * both before and after position i
48 * @param nums - The input array
49 * @param k - The threshold value
50 * @returns The count of k-big indices
51 */
52function kBigIndices(nums: number[], k: number): number {
53    // Find the maximum value to determine tree size
54    const maxValue = Math.max(...nums);
55  
56    // Tree for counting elements before current position
57    let leftTreeSize = maxValue;
58    let leftTree = new Array(maxValue + 1).fill(0);
59  
60    // Tree for counting elements after current position
61    let rightTreeSize = maxValue;
62    let rightTree = new Array(maxValue + 1).fill(0);
63  
64    // Helper function for updating tree
65    const updateTree = (tree: number[], size: number, x: number, delta: number): void => {
66        while (x <= size) {
67            tree[x] += delta;
68            x += x & -x;
69        }
70    };
71  
72    // Helper function for querying tree
73    const queryTree = (tree: number[], x: number): number => {
74        let sum = 0;
75        while (x > 0) {
76            sum += tree[x];
77            x -= x & -x;
78        }
79        return sum;
80    };
81  
82    // Initially, all elements are on the right side
83    for (const value of nums) {
84        updateTree(rightTree, rightTreeSize, value, 1);
85    }
86  
87    let result = 0;
88  
89    // Process each element
90    for (const value of nums) {
91        // Remove current element from right side
92        updateTree(rightTree, rightTreeSize, value, -1);
93      
94        // Check if current position is a k-big index
95        // Count elements smaller than current value on both sides
96        const leftSmallerCount = queryTree(leftTree, value - 1);
97        const rightSmallerCount = queryTree(rightTree, value - 1);
98      
99        if (leftSmallerCount >= k && rightSmallerCount >= k) {
100            result++;
101        }
102      
103        // Add current element to left side
104        updateTree(leftTree, leftTreeSize, value, 1);
105    }
106  
107    return result;
108}
109

Time and Space Complexity

Time Complexity: O(n × log n)

The code uses two Binary Indexed Trees (Fenwick Trees) to solve the problem. Let's break down the time complexity:

  1. Initialization of BITs: Creating two BinaryIndexedTree objects takes O(n) time (just initializing arrays).

  2. Initial population of tree2: The first loop iterates through all n elements in nums and calls update() for each. The update() operation in a BIT takes O(log n) time because it updates at most log n nodes (following the path determined by x += x & -x). This gives us O(n × log n).

  3. Main processing loop: This loop iterates through all n elements and for each element:

    • Calls tree2.update(v, -1): O(log n)
    • Calls tree1.query(v - 1): O(log n) (the query traverses at most log n nodes via x -= x & -x)
    • Calls tree2.query(v - 1): O(log n)
    • Calls tree1.update(v, 1): O(log n)

    Total per iteration: 4 × O(log n) = O(log n) For n iterations: O(n × log n)

Overall time complexity: O(n × log n) + O(n × log n) = O(n × log n)

Space Complexity: O(n)

The space usage consists of:

  • Two BIT objects, each containing an array c of size n + 1: 2 × O(n) = O(n)
  • A few integer variables (ans, loop variables): O(1)

Total space complexity: O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Handling of Value Range

The Pitfall: The current implementation assumes that all values in nums are within the range [1, n] where n = len(nums). This is a critical assumption because the Binary Indexed Tree is initialized with size n, meaning it can only handle values from 1 to n. If the array contains:

  • Values larger than n
  • Zero or negative values
  • Duplicate values that when counted exceed the array size

The code will either crash with an index out of bounds error or produce incorrect results.

Example of Failure:

nums = [10, 20, 30, 15, 5]  # Values exceed array length
k = 1
# This will cause index out of bounds when trying to update(10, 1) on a tree of size 5

Solution: Compress the values to the range [1, unique_count] using coordinate compression:

def kBigIndices(self, nums: List[int], k: int) -> int:
    n = len(nums)
  
    # Coordinate compression: map values to ranks
    sorted_unique = sorted(set(nums))
    value_to_rank = {v: i + 1 for i, v in enumerate(sorted_unique)}
  
    # Convert original values to their ranks
    compressed = [value_to_rank[v] for v in nums]
  
    # Initialize trees with the number of unique values
    max_rank = len(sorted_unique)
    left_tree = BinaryIndexedTree(max_rank)
    right_tree = BinaryIndexedTree(max_rank)
  
    # Populate right tree with compressed values
    for rank in compressed:
        right_tree.update(rank, 1)
  
    k_big_count = 0
  
    for rank in compressed:
        right_tree.update(rank, -1)
      
        if left_tree.query(rank - 1) >= k and right_tree.query(rank - 1) >= k:
            k_big_count += 1
      
        left_tree.update(rank, 1)
  
    return k_big_count

2. Handling Duplicate Values Incorrectly

The Pitfall: When multiple identical values exist in the array, the current approach correctly handles them because each occurrence is treated separately. However, developers might mistakenly think they need special handling for duplicates or might incorrectly try to "optimize" by grouping them.

Why It Works As-Is: Each element is processed individually in sequence, so duplicates naturally get counted correctly:

  • When we encounter a duplicate, previous occurrences are already in the left tree
  • Future occurrences are still in the right tree
  • The query correctly counts all instances smaller than the current value

What NOT to Do: Don't try to batch-process duplicates or skip them thinking they're already handled. Each position needs individual evaluation for the k-big condition.

3. Off-by-One Errors in Queries

The Pitfall: Using tree.query(v) instead of tree.query(v - 1) when counting elements strictly less than v. The query function returns the count of elements from 1 to the given index (inclusive), so to count elements strictly less than v, we need to query up to v - 1.

Incorrect:

# This counts elements <= v, not < v
left_count = left_tree.query(value)  

Correct:

# This counts elements < v
left_count = left_tree.query(value - 1)

4. Tree Initialization Size Error

The Pitfall: If implementing coordinate compression, initializing the trees with n (array length) instead of the number of unique values can waste memory or cause issues if the compressed ranks exceed expectations.

Best Practice: Always size the Binary Indexed Tree based on the maximum value it needs to handle after any compression or transformation is applied.

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

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

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

Load More