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:
- There are at least
k
different indices before positioni
(indices less thani
) where the values at those indices are smaller thannums[i]
- There are at least
k
different indices after positioni
(indices greater thani
) where the values at those indices are smaller thannums[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).
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:
- Efficient insertion/deletion of elements
- 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 indices0
toi-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 indicesi+1
ton-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)
: Addsdelta
to positionx
in O(log n) timequery(x)
: Returns the sum of elements from position 1 tox
in O(log n) time
Main Algorithm Steps:
-
Initialize Two Trees:
tree1 = BinaryIndexedTree(n)
- Initially empty, will track elements on the lefttree2 = BinaryIndexedTree(n)
- Will track elements on the right
-
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. -
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 positioni
:- 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 thanv
on the lefttree2.query(v - 1)
counts elements less thanv
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
- Remove from Right Side:
Key Insight: The algorithm cleverly maintains the invariant that at each position i
:
tree1
contains exactly the elements from indices0
toi-1
tree2
contains exactly the elements from indicesi+1
ton-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 EvaluatorExample 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:
-
Initialization of BITs: Creating two
BinaryIndexedTree
objects takesO(n)
time (just initializing arrays). -
Initial population of tree2: The first loop iterates through all
n
elements innums
and callsupdate()
for each. Theupdate()
operation in a BIT takesO(log n)
time because it updates at mostlog n
nodes (following the path determined byx += x & -x
). This gives usO(n × log n)
. -
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 mostlog n
nodes viax -= 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)
Forn
iterations:O(n × log n)
- Calls
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 sizen + 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.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
Segment Tree Faster Range Queries For this article we want to introduce the idea of a Segment Tree Segment Trees allow us to quickly perform range queries as well as range updates Suppose we have an array and we want to know the sum of a particular range of numbers as well
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
https assets algo monster cover_photos dnd svg Divide and Conquer The main idea of divide and conquer is to split the main problem into two smaller components assuming that each one of the components had already been solved recursively and then try to solve the bigger problem using the solutions
Want a Structured Path to Master System Design Too? Don’t Miss This!