327. Count of Range Sum
327. Count of Range Sum
Problem Statement
Given an integer array nums and two integers lower and upper, return the number of range sums that lie in [lower, upper] inclusive.
Range sum S(i, j) is defined as the sum of the elements in nums between indices i and j inclusive, where i <= j.
Examples
Example 1:
Input: nums = [-2,5,-1], lower = -2, upper = 2
Output: 3
Explanation: The three ranges are: [0,0], [2,2], and [0,2] and their respective sums are: -2, -1, 2.
Example 2:
Input: nums = [0], lower = 0, upper = 0
Output: 1
Constraints
1 <= nums.length <= 10^5-2^31 <= nums[i] <= 2^31 - 1-10^5 <= lower <= upper <= 10^5
Clarification Questions
Before diving into the solution, here are 5 important clarifications and assumptions to discuss during an interview:
-
Range inclusivity: Is the range
[lower, upper]inclusive on both ends? (Assumption: Yes - subarray sum should satisfylower <= sum <= upper) -
Subarray definition: Does a subarray need to be contiguous? (Assumption: Yes - subarray is contiguous by definition)
-
Empty subarray: Can an empty subarray be considered? (Assumption: Need to clarify - typically empty subarray has sum 0, but should confirm if it counts)
-
Integer overflow: Can prefix sums overflow? (Assumption: Yes - need to use
long longfor prefix sums to handle large numbers) -
Negative numbers: Can the array contain negative numbers? (Assumption: Yes - this makes prefix sums non-monotonic, requiring advanced techniques like merge sort or segment tree)
Interview Deduction Process (30 minutes)
Step 1: Brute-Force Approach (8 minutes)
Initial Thought: “I need to count range sums. Let me check all possible subarrays.”
Naive Solution: Check all possible subarrays, compute sum, count those in range [lower, upper].
Complexity: O(n²) time, O(1) space
Issues:
- O(n²) time - inefficient
- Repeats sum computation for overlapping subarrays
- Doesn’t leverage prefix sum
- Can be optimized
Step 2: Semi-Optimized Approach (10 minutes)
Insight: “I can use prefix sum to compute subarray sums efficiently, but need to count pairs.”
Improved Solution: Build prefix sum array. For each j, count i < j where prefix[j] - prefix[i] is in [lower, upper]. This requires counting prefix[i] in range [prefix[j]-upper, prefix[j]-lower].
Complexity: O(n²) time with prefix sum, O(n) space
Improvements:
- Prefix sum enables O(1) subarray sum queries
- Still O(n²) for checking all pairs
- Better structure than brute-force
- Can optimize further
Step 3: Optimized Solution (12 minutes)
Final Optimization: “I can use merge sort or segment tree to count pairs efficiently.”
Best Solution: Use merge sort on prefix sums. During merge, count pairs where prefix[i] is in range [prefix[j]-upper, prefix[j]-lower]. Alternative: Segment Tree with coordinate compression.
Complexity: O(n log n) time, O(n) space
Key Realizations:
- Prefix sum transformation is key insight
- Merge sort enables efficient pair counting
- O(n log n) time is optimal
- Segment Tree is alternative approach
Solution Approach
This problem requires counting the number of subarray sums that fall within a given range. The key insight is to use prefix sums and then count pairs of prefix sums that satisfy the condition.
Key Insights:
- Prefix Sum:
S(i, j) = prefix[j+1] - prefix[i] - Range Condition:
lower <= prefix[j+1] - prefix[i] <= upper - Rearranged:
prefix[j+1] - upper <= prefix[i] <= prefix[j+1] - lower - Problem Transformation: Count pairs
(i, j)wherei < jandprefix[i]is in range[prefix[j] - upper, prefix[j] - lower]
Solution 1: Divide and Conquer (Merge Sort)
class Solution {
public:
int countRangeSum(vector<int>& nums, int lower, int upper) {
int n = nums.size();
vector<long long> prefix(n + 1, 0);
// prefix sum with 0 included
for (int i = 0; i < n; i++)
prefix[i + 1] = prefix[i] + nums[i];
vector<long long> temp(n + 1);
return divide(prefix, 0, n, lower, upper, temp);
}
private:
int divide(vector<long long>& prefix, int left, int right,
int lower, int upper, vector<long long>& temp) {
if (left >= right) return 0;
int mid = (left + right) / 2;
int count = 0;
count += divide(prefix, left, mid, lower, upper, temp);
count += divide(prefix, mid + 1, right, lower, upper, temp);
count += countCross(prefix, left, mid, right, lower, upper);
merge(prefix, left, mid, right, temp);
return count;
}
int countCross(vector<long long>& prefix, int left, int mid, int right,
int lower, int upper) {
int count = 0;
int wl = left, wr = left;
for (int i = mid + 1; i <= right; i++) {
long long low = prefix[i] - upper;
long long high = prefix[i] - lower;
while (wl <= mid && prefix[wl] < low) wl++;
while (wr <= mid && prefix[wr] <= high) wr++;
count += wr - wl;
}
return count;
}
void merge(vector<long long>& prefix, int left, int mid, int right,
vector<long long>& temp) {
int i = left, j = mid + 1, k = left;
while (i <= mid && j <= right)
temp[k++] = (prefix[i] <= prefix[j]) ? prefix[i++] : prefix[j++];
while (i <= mid) temp[k++] = prefix[i++];
while (j <= right) temp[k++] = prefix[j++];
for (int i = left; i <= right; i++)
prefix[i] = temp[i];
}
};
Algorithm Explanation:
-
Prefix Sum Array: Build
prefix[i] = sum(nums[0..i-1])withprefix[0] = 0 - Divide and Conquer:
- Divide prefix array into left and right halves
- Recursively count valid pairs in left and right halves
- Count cross pairs (one from left, one from right)
- Merge the sorted halves
- Count Cross Pairs (
countCross):- For each
prefix[j]in right half, find validprefix[i]in left half - Condition:
prefix[j] - upper <= prefix[i] <= prefix[j] - lower - Use two pointers
wlandwrto maintain valid range wl: first index whereprefix[wl] >= prefix[j] - upperwr: first index whereprefix[wr] > prefix[j] - lower- Count:
wr - wl
- For each
- Merge: Standard merge sort merge operation
Why This Works?
- Sorted Property: After merge, left and right halves are sorted
- Two Pointers: For each right element, find valid left elements using sliding window
- Efficiency: O(n log n) time complexity
Complexity Analysis:
- Time Complexity: O(n log n)
- Divide: O(log n) levels
- Each level: O(n) for counting and merging
- Total: O(n log n)
- Space Complexity: O(n)
- Prefix array: O(n)
- Temp array: O(n)
- Recursion stack: O(log n)
Solution 2: Segment Tree (Dynamic Node Creation)
struct Node {
long long start, end;
int cnt;
Node *left, *right;
Node(long long start, long long end): start(start), end(end), cnt(0){
left = right = nullptr;
}
};
class Solution {
/*
Prefix sum pre
pre[j] - upper <= pre[i] <= pre[j] - lower
Fenwick Tree (Binary Indexed Tree)
Segment Tree with dynamic allocation - reduce allocations, no delete needed
*/
public:
int countRangeSum(vector<int>& nums, int lower, int upper) {
const int N = nums.size();
vector<long long> presum(N + 1, 0);
for(int i = 0; i < N; i++) {
presum[i + 1] = presum[i] + nums[i];
}
long long mn = LLONG_MAX, mx = LLONG_MIN;
for(auto x: presum) {
mn = min({mn, x, x - upper});
mx = max({mx, x, x - lower});
}
Node root(mn, mx);
int rtn = 0;
for(auto x: presum) {
rtn += query(&root, x - upper, x - lower);
add(&root, x);
}
return rtn;
}
private:
int query(Node* node, long long l, long long r) {
if(!node) return 0;
if(l > node->end || r < node->start) return 0;
if(l <= node->start && node->end <= r) return node->cnt;
return query(node->left, l, r) + query(node->right, l, r);
}
// node is not null
// add pos [start, end]
void add(Node* node, long long pos){
node->cnt++;
if(node->start == node->end) return;
long long mid = (node->start + node->end) >> 1;
if(pos <= mid) {
if(!node->left) {
node->left = new Node(node->start, mid);
}
add(node->left, pos);
} else {
if(!node->right) {
node->right = new Node(mid+1, node->end);
}
add(node->right, pos);
}
}
};
Algorithm Explanation:
-
Prefix Sum Array: Build prefix sums as in Solution 1
- Dynamic Segment Tree:
- Root covers range
[min_value, max_value]of all possible prefix sums - Dynamically create nodes only when needed (lazy initialization)
- Each node stores count of prefix sums in its range
- Root covers range
- Query and Update:
- For each prefix sum
x, query count of values in[x - upper, x - lower] - This counts how many previous prefix sums satisfy the condition
- Then add current prefix sum to the tree
- For each prefix sum
- Dynamic Node Creation:
- Only create child nodes when needed (when adding a value)
- Reduces memory usage compared to full segment tree
Why This Works?
- Range Query: Segment tree efficiently counts values in range
- Online Processing: Process prefix sums one by one
- Memory Efficient: Only create nodes for values that appear
Complexity Analysis:
- Time Complexity: O(n log U)
- n = number of elements
- U = range size (max - min)
- Each query/update: O(log U)
- Space Complexity: O(n log U)
- Segment tree nodes: O(n log U) in worst case
- Prefix array: O(n)
Key Insights
- Prefix Sum Transformation: Convert subarray sum problem to prefix sum difference problem
- Range Condition:
lower <= prefix[j] - prefix[i] <= upperbecomesprefix[j] - upper <= prefix[i] <= prefix[j] - lower - Two Approaches:
- Divide & Conquer: Sort prefix sums, count pairs using two pointers
- Segment Tree: Maintain count of prefix sums, query range for each new prefix
- Dynamic Node Creation: Reduces memory for sparse segment trees
Edge Cases
- Single element:
nums = [0],lower = 0,upper = 0→ return1 - All negative:
nums = [-2,-1],lower = -3,upper = -1→ count valid ranges - Large numbers: Use
long longto prevent overflow - Empty ranges: Handle cases where no valid ranges exist
- Overflow prevention: Prefix sums can exceed
intrange
Common Mistakes
- Integer overflow: Not using
long longfor prefix sums// WRONG: vector<int> prefix(n + 1, 0); // ❌ May overflow - Off-by-one errors: Incorrect prefix sum indexing
- Missing prefix[0]: Forgetting to include empty prefix (sum = 0)
- Wrong range: Confusing
prefix[j] - upperandprefix[j] - lower - Memory leaks: Not managing segment tree nodes properly (though solution doesn’t delete)
Comparison of Approaches
| Approach | Time | Space | Pros | Cons |
|---|---|---|---|---|
| Divide & Conquer | O(n log n) | O(n) | Simple, stable | Requires sorting |
| Segment Tree | O(n log U) | O(n log U) | Online processing | More complex, memory overhead |
Related Problems
- LC 327: Count of Range Sum - This problem
- LC 315: Count of Smaller Numbers After Self - Similar divide & conquer approach
- LC 493: Reverse Pairs - Count pairs with condition
- LC 307: Range Sum Query - Mutable - Segment tree for range sum
- LC 303: Range Sum Query - Immutable - Prefix sum basics
This problem demonstrates divide and conquer and segment tree techniques for counting pairs satisfying a range condition. The key transformation is converting subarray sums to prefix sum differences, making it easier to count valid pairs efficiently.