[Hard] 315. Count of Smaller Numbers After Self
You are given an integer array nums and you have to return a new array counts. The array counts has the property where counts[i] is the number of smaller elements to the right of nums[i].
Examples
Example 1:
Input: nums = [5,2,6,1]
Output: [2,1,1,0]
Explanation:
To the right of 5 there are 2 smaller elements (2 and 1).
To the right of 2 there is 1 smaller element (1).
To the right of 6 there is 1 smaller element (1).
To the right of 1 there is 0 smaller elements.
Example 2:
Input: nums = [-1]
Output: [0]
Example 3:
Input: nums = [-1,-1]
Output: [0,0]
Constraints
1 <= nums.length <= 10^5-10^4 <= nums[i] <= 10^4
Thinking Process
- Coordinate Compression: Essential for handling negative numbers and large ranges
- Use
unordered_mapfor O(1) rank lookup after sorting - Maps distinct values to consecutive ranks [1, k]
- Use
- The search space must shrink monotonically each step.
- Decide which half still satisfies the predicate, discard the other.
- Use
mid = left + (right - left) / 2to avoid overflow.
Common Approaches
Typical techniques for this pattern:
| Approach | Time | Space | Notes |
|---|---|---|---|
| Prefix sum | O(n) | O(n) | Range queries, subarray sum |
| Sort + scan | O(n log n) | O(1) | Intervals, meeting rooms |
| Kadane’s algorithm | O(n) | O(1) | Maximum subarray |
| Hash map counting (this problem) | O(n) | O(n) | Frequency, two-sum variants |
Solution
Solution: Fenwick Tree (Binary Indexed Tree) with Coordinate Compression
class FrenwickTree{
public:
FrenwickTree(int n): sums_(n + 1, 0){}
void update(int i, int delta) {
while(i < sums_.size()) {
sums_[i] += delta;
i += lowbit(i);
}
}
int query(int i) {
int sum = 0;
while(i > 0) {
sum += sums_[i];
i -= lowbit(i);
}
return sum;
}
private:
vector<int> sums_;
int lowbit(int x) {
return x & (-x);
}
};
class Solution {
public:
vector<int> countSmaller(vector<int>& nums) {
// Get rank order
vector<int> sorted(nums);
sort(sorted.begin(), sorted.end());
sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());
unordered_map<int, int> ranks;
int rank = 1;
for(const int num: sorted) {
ranks[num] = rank++;
}
vector<int> rtn;
// Update pre-fix sum while iterate, add ranks by 1 when encouter
FrenwickTree tree(ranks.size());
for(int i = nums.size() - 1; i >= 0; i--) {
rtn.push_back(tree.query(ranks[nums[i]] - 1));
tree.update(ranks[nums[i]], 1);
}
reverse(rtn.begin(), rtn.end());
return rtn;
}
};
Solution Explanation
Approach: Hash map counting (this problem)
Key idea: 1. Coordinate Compression: Essential for handling negative numbers and large ranges
How the code works:
- Coordinate Compression: Essential for handling negative numbers and large ranges
- Use
unordered_mapfor O(1) rank lookup after sorting - Maps distinct values to consecutive ranks [1, k]
- The search space must shrink monotonically each step.
- Decide which half still satisfies the predicate, discard the other.
- Use
mid = left + (right - left) / 2to avoid overflow.
- Use
Walkthrough — input nums = [5,2,6,1], expected output [2,1,1,0]:
To the right of 5 there are 2 smaller elements (2 and 1). To the right of 2 there is 1 smaller element (1). To the right of 6 there is 1 smaller element (1). To the right of 1 there is 0 smaller elements.
Algorithm Explanation:
FrenwickTree Class:
- Constructor: Initialize Fenwick Tree with size
n(1-indexed arraysums_) - lowbit(x): Extract lowest set bit using
x & (-x) - update(i, delta): Add
deltato positioni(1-indexed) and all ancestors- Traverse upward:
i += lowbit(i) - Stops when
i >= sums_.size()
- Traverse upward:
- query(i): Get prefix sum from 1 to
i(1-indexed)- Traverse downward:
i -= lowbit(i) - Stops when
i <= 0
- Traverse downward:
Solution Class:
- Coordinate Compression:
- Create sorted, unique array of all values
- Build
unordered_mapmapping each value to its rank [1, k] - Handles negative numbers and large ranges efficiently
- Example:
[5, 2, 6, 1]→ sorted[1, 2, 5, 6]→ ranks:{1:1, 2:2, 5:3, 6:4}
- Right-to-Left Processing:
- Process from
nums.size()-1down to0 - For each element:
- Get rank:
ranks[nums[i]] - Query count of elements < current:
tree.query(ranks[nums[i]] - 1) - Push result to
rtnvector - Update tree: mark current element as seen with
tree.update(ranks[nums[i]], 1)
- Get rank:
- Reverse result array to get correct order
- Process from
How It Works:
- Coordinate Compression:
[5, 2, 6, 1]→ sorted[1, 2, 5, 6]→ ranks{1:1, 2:2, 5:3, 6:4} - Right-to-Left: Ensures we only count elements to the right
- Query Before Update: Query counts elements already processed (to the right)
- Update: Marks current element for future queries
- Reverse Result: Results are collected in reverse order, then reversed at the end
Example Walkthrough:
Input: nums = [5, 2, 6, 1]
Step 1: Coordinate Compression
sorted = [1, 2, 5, 6]
ranks = {1:1, 2:2, 5:3, 6:4}
Step 2: Process from right to left
i=3: nums[3] = 1, rank = 1
query(0) = 0 → rtn.push_back(0)
update(1, 1) → sums_[1] = 1
rtn = [0]
i=2: nums[2] = 6, rank = 4
query(3) = sums_[3] + sums_[2] = 0 + 1 = 1 → rtn.push_back(1)
update(4, 1) → sums_[4] = 1
rtn = [0, 1]
i=1: nums[1] = 2, rank = 2
query(1) = sums_[1] = 1 → rtn.push_back(1)
update(2, 1) → sums_[2] = 2
rtn = [0, 1, 1]
i=0: nums[0] = 5, rank = 3
query(2) = sums_[2] = 2 → rtn.push_back(2)
update(3, 1) → sums_[3] = 1
rtn = [0, 1, 1, 2]
Step 3: Reverse result
reverse(rtn) → [2, 1, 1, 0] ✓
Complexity Analysis:
- Time Complexity: O(n log n)
- Coordinate compression: O(n log n) for sorting
- Binary search for each element: O(n log n)
- Fenwick Tree operations: O(n log n) for n updates + n queries
- Overall: O(n log n)
- Space Complexity: O(n)
- Result array: O(n)
- Sorted array: O(n)
- Fenwick Tree: O(n)
- Overall: O(n)
Common Mistakes
- Single element:
nums = [5]→ return[0] - All same:
nums = [1, 1, 1]→ return[0, 0, 0] - Negative numbers:
nums = [-1, -2]→ coordinate compression handles it - Descending order:
nums = [5, 4, 3, 2, 1]→ all counts are 0 -
Ascending order:
nums = [1, 2, 3, 4, 5]→ counts increase - Left-to-right processing: Would count elements to the left instead
- Forgetting coordinate compression: BIT requires positive indices
- Wrong query index: Using
query(x)instead ofquery(x-1)for strictly smaller - Update before query: Should query first, then update
- Not handling duplicates: Coordinate compression must preserve uniqueness
Related Problems
- LC 327: Count of Range Sum - Similar inversion counting
- LC 493: Reverse Pairs - Count inversions with condition
- LC 1649: Create Sorted Array through Instructions - Fenwick Tree for cost calculation
- LC 307: Range Sum Query - Mutable - Fenwick Tree basics
Key Takeaways
- Coordinate Compression: Essential for handling negative numbers and large ranges
- Use
unordered_mapfor O(1) rank lookup after sorting - Maps distinct values to consecutive ranks [1, k]
- Use
- Right-to-Left Processing: Ensures we only count elements to the right
- Fenwick Tree Efficiency: O(log n) per operation, better than naive O(n)
- Query Before Update: Query counts already-seen elements, then mark current
- Result Collection: Use
push_backandreversefor cleaner code when processing backwards - Rank Mapping:
unordered_mapprovides O(1) lookup vs O(log n) binary search
References
- LC 315: Count of Smaller Numbers After Self on LeetCode
- LeetCode Discuss — LC 315: Count of Smaller Numbers After Self
- LeetCode Editorial (may require premium)