Pattern family: Array / Subarray / Range Query
Difficulty range: Easy β Hard
Language: Java
- How to identify this pattern β start here
- What is this pattern?
- Core rules
- 2-Question framework
- Variants table
- Universal Java template
- Quick reference cheatsheet
- Common mistakes
- Complexity summary
| If you see this... | It means |
|---|---|
| "subarray sum equals k" | prefix sum + HashMap lookup |
| "number of subarrays with sum / count" | prefix sum + frequency map |
| "find pivot index / equilibrium index" | left sum == right sum |
| "subarray sum divisible by k" | prefix sum + modulo + HashMap |
| "contiguous subarray with equal 0s and 1s" | remap 0β-1, prefix sum |
| "range sum query" | precompute prefix array, O(1) query |
| "2D matrix sum query" | 2D prefix sum |
| "shortest subarray with sum β₯ k" | prefix sum + monotonic deque |
| "count range sum" | prefix sum + merge sort / BIT |
| "max sum rectangle no larger than k" | 2D prefix + sorted set |
| "running total / cumulative sum" | prefix sum array |
Brute force: two nested loops β for every (i, j) pair compute sum(i..j) from scratch.
β O(nΒ²) or O(nΒ³) time β too slow for large inputs.
β Interviewer says "O(n)" or "O(n log n)" β cannot recompute sum every time.
β Use Prefix Sum: precompute once in O(n), answer any range query in O(1).
Signal 1 β "Subarray sum equals / divisible by / at least K"
Any problem asking about a property of a subarray SUM (not max/min element) is a prefix sum problem. The key identity:
sum(i..j) = prefix[j+1] - prefix[i].
Signal 2 β "Count of subarrays" with some sum condition
When you need to COUNT how many subarrays satisfy a sum condition, store prefix sums in a HashMap. Each lookup is O(1).
Signal 3 β "Pivot / equilibrium index"
Left sum == right sum β prefix sum from left and right simultaneously, or use total sum minus left prefix.
Signal 4 β "Equal count of two elements" (0s and 1s, X and Y)
Remap one element to -1. Now "equal count" = "subarray sum = 0". This converts the problem to subarray sum = 0 β prefix sum + HashMap.
Signal 5 β "Range sum query" (multiple queries on same array)
If the array is static and you have many queries, precompute prefix array. Each query
sum(l, r)=prefix[r+1] - prefix[l]in O(1).
Problem involves subarray / range SUM?
β
βΌ
Single query or multiple queries?
β
βββ MULTIPLE QUERIES (static array) β Prefix array, O(1) per query
β
βββ COUNT subarrays with sum = k? β prefix + HashMap {sum β count}
β
βββ COUNT subarrays with sum % k = 0? β prefix + modulo + HashMap
β
βββ EQUAL count of two values? β remap to Β±1, sum = 0 β HashMap
β
βββ PIVOT / EQUILIBRIUM index? β leftSum == totalSum - leftSum - nums[i]
β
βββ SHORTEST subarray with sum β₯ k? β prefix + monotonic deque (HARD)
β
βββ COUNT range sums in [lo, hi]? β prefix + merge sort / BIT (HARD)
β
βββ MAX sum rectangle β€ k? β 2D prefix + sorted set (HARD)
Build a prefix[] array where prefix[i] = sum of nums[0..i-1].
int[] prefix = new int[n + 1]; // prefix[0] = 0
for (int i = 0; i < n; i++)
prefix[i + 1] = prefix[i] + nums[i];Core identity:
sum(i, j) = prefix[j+1] - prefix[i]
(sum from index i to j inclusive)
Why it works:
nums = [3, 1, 4, 1, 5]
prefix = [0, 3, 4, 8, 9, 14]
sum(1, 3) = prefix[4] - prefix[1] = 9 - 3 = 6 β (1+4+1)
sum(0, 4) = prefix[5] - prefix[0] = 14 - 0 = 14 β (3+1+4+1+5)
The HashMap extension (for counting): Instead of finding a specific range, we ask:
"How many previous prefix sums equal
currentSum - k?"
If prefix[j] - prefix[i] = k, then subarray (i, j] has sum k.
Store every prefix sum in a HashMap as you go β each lookup is O(1).
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1); // empty prefix has sum 0 β count = 1
int sum = 0, count = 0;
for (int num : nums) {
sum += num;
count += map.getOrDefault(sum - k, 0); // how many prefixes equal sum-k?
map.put(sum, map.getOrDefault(sum, 0) + 1);
}Rule 1 β Always initialise map.put(0, 1) before the loop
// WRONG β misses subarrays starting from index 0
Map<Integer, Integer> map = new HashMap<>();
// map is empty β if sum == k on first element, count += map.get(0) = null β NPE or 0
// CORRECT β seed the map with sum=0 having count=1
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1); // represents the empty prefix before index 0This handles the case where a subarray starting from index 0 sums to exactly k.
Rule 2 β For modulo problems, handle negative remainders in Java
// WRONG β Java % can return negative for negative sums
int rem = sum % k;
// CORRECT β always normalise to positive remainder
int rem = ((sum % k) + k) % k;Java's % operator returns a result with the sign of the dividend.
-7 % 3 = -1 in Java, but we want 2. The +k) % k trick fixes this.
Rule 3 β For "equal 0s and 1s" problems, remap 0 β -1 first
// Remap before building prefix sum
for (int i = 0; i < nums.length; i++)
if (nums[i] == 0) nums[i] = -1;
// Now "equal 0s and 1s" = "subarray sum = 0"
// Apply standard prefix + HashMap with target k = 0After remapping, any subarray with sum = 0 has equal counts of +1 and -1 (original 1s and 0s). This converts the problem to a standard prefix sum lookup.
| Answer | Variant |
|---|---|
Fixed sum k β does it exist? |
Prefix + HashMap (existence check) |
Fixed sum k β count subarrays |
Prefix + HashMap (frequency count) |
Sum divisible by k |
Prefix + modulo + HashMap |
| Equal count of two values | Remap to Β±1, sum = 0, HashMap |
| Range query on static array | Precompute prefix array |
| Shortest subarray with sum β₯ k | Prefix + monotonic deque |
| Count sums in range [lo, hi] | Prefix + merge sort / Fenwick tree |
| Max rectangle sum β€ k | 2D prefix + sorted set |
| Dimension | Approach |
|---|---|
| 1D | prefix[i+1] = prefix[i] + nums[i] |
| 2D | prefix[i][j] = nums[i][j] + prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1] |
// 2D prefix sum β rectangle sum query
int rectSum = prefix[r2+1][c2+1]
- prefix[r1][c2+1]
- prefix[r2+1][c1]
+ prefix[r1][c1];Decision shortcut:
- "count subarrays with sum = k" β HashMap, seed with
{0:1}- "sum divisible by k" β modulo map,
((sum%k)+k)%k- "equal 0s and 1s" β remap 0β-1, then sum=0
- "pivot index" β
leftSum == total - leftSum - nums[i]- "shortest with sum β₯ k" β deque (prefix values must be negative allowed)
- "range query, many queries" β prefix array
Common core:
prefix[i] = prefix[i-1] + nums[i-1]
What differs: what you store in HashMap, what you look up
| Variant | HashMap key | HashMap value | Lookup target |
|---|---|---|---|
| Count subarrays sum=k | prefix sum | frequency | sum - k |
| Sum divisible by k | sum % k (normalised) |
frequency | same remainder |
| Equal 0s and 1s | prefix sum (after remap) | first index seen | same sum (longest) |
| Pivot index | β (no map) | β | 2 * leftSum + nums[i] == total |
| Shortest sum β₯ k | β (deque) | β | deque front |
| Count range sum | prefix sum | sorted list | binary search in [lo,hi] |
// βββ TEMPLATE A: Prefix Array (Range Sum Query) βββββββββββββββββββββββββββββββ
int[] prefix = new int[n + 1];
for (int i = 0; i < n; i++)
prefix[i + 1] = prefix[i] + nums[i];
// Query sum(l, r) inclusive:
int rangeSum = prefix[r + 1] - prefix[l];
// βββ TEMPLATE B: Count Subarrays with Sum = k (HashMap) ββββββββββββββββββββββ
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1); // β ALWAYS seed this first
int sum = 0, count = 0;
for (int num : nums) {
sum += num;
count += map.getOrDefault(sum - k, 0); // subarrays ending here with sum=k
map.merge(sum, 1, Integer::sum); // increment frequency of sum
}
return count;
}
// βββ TEMPLATE C: Sum Divisible by k (Modulo HashMap) βββββββββββββββββββββββββ
public int subarraysDivByK(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1);
int sum = 0, count = 0;
for (int num : nums) {
sum += num;
int rem = ((sum % k) + k) % k; // β normalise negative remainder
count += map.getOrDefault(rem, 0);
map.merge(rem, 1, Integer::sum);
}
return count;
}
// βββ TEMPLATE D: Equal 0s and 1s (Remap + First-seen index) ββββββββββββββββββ
public int findMaxLength(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
map.put(0, -1); // β seed with index -1 (before array starts)
int sum = 0, maxLen = 0;
for (int i = 0; i < nums.length; i++) {
sum += (nums[i] == 0) ? -1 : 1; // remap 0β-1
if (map.containsKey(sum)) {
maxLen = Math.max(maxLen, i - map.get(sum));
} else {
map.put(sum, i); // store FIRST occurrence only (for max length)
}
}
return maxLen;
}
// βββ TEMPLATE E: Pivot Index βββββββββββββββββββββββββββββββββββββββββββββββββ
public int pivotIndex(int[] nums) {
int total = 0;
for (int n : nums) total += n;
int leftSum = 0;
for (int i = 0; i < nums.length; i++) {
// leftSum == rightSum β leftSum == total - leftSum - nums[i]
if (2 * leftSum + nums[i] == total) return i;
leftSum += nums[i];
}
return -1;
}
// βββ TEMPLATE F: Shortest Subarray with Sum β₯ k (Deque) ββββββββββββββββββββββ
public int shortestSubarray(int[] nums, int k) {
int n = nums.length;
long[] prefix = new long[n + 1];
for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + nums[i];
int res = Integer.MAX_VALUE;
Deque<Integer> deque = new ArrayDeque<>(); // stores indices, monotone increasing prefix
for (int i = 0; i <= n; i++) {
// Remove from front: if prefix[i] - prefix[front] >= k β valid, try to shrink
while (!deque.isEmpty() && prefix[i] - prefix[deque.peekFirst()] >= k) {
res = Math.min(res, i - deque.pollFirst());
}
// Remove from back: maintain increasing order of prefix values
while (!deque.isEmpty() && prefix[i] <= prefix[deque.peekLast()]) {
deque.pollLast();
}
deque.addLast(i);
}
return res == Integer.MAX_VALUE ? -1 : res;
}
// βββ TEMPLATE G: 2D Prefix Sum ββββββββββββββββββββββββββββββββββββββββββββββββ
int[][] prefix2D = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
prefix2D[i][j] = mat[i-1][j-1]
+ prefix2D[i-1][j]
+ prefix2D[i][j-1]
- prefix2D[i-1][j-1];
// Rectangle sum (r1,c1) to (r2,c2) inclusive:
int rectSum = prefix2D[r2+1][c2+1]
- prefix2D[r1][c2+1]
- prefix2D[r2+1][c1]
+ prefix2D[r1][c1];SIGNAL IN PROBLEM β VARIANT β KEY CODE
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
"subarray sum equals k" β HashMap count β map.put(0,1); count += map.get(sum-k)
"sum divisible by k" β Modulo HashMap β rem = ((sum%k)+k)%k
"equal 0s and 1s / equal X and Y" β Remap + HashMap β 0β-1, find sum=0
"pivot / equilibrium index" β Left/right total β 2*leftSum + nums[i] == total
"range sum query (multiple)" β Prefix array β prefix[r+1] - prefix[l]
"shortest subarray sum β₯ k" β Prefix + deque β monotone deque on prefix array
"count range sums in [lo,hi]" β Prefix + merge sort β count inversions in prefix array
"max rectangle sum β€ k" β 2D prefix + TreeSet β fix rows, binary search in set
"2D rectangle sum query" β 2D prefix array β inclusion-exclusion formula
The rules β burn these into memory:
// ALWAYS seed the map before the loop
map.put(0, 1);
// ALWAYS normalise modulo for negative sums
int rem = ((sum % k) + k) % k;
// ALWAYS remap 0 β -1 for "equal count" problems
sum += (nums[i] == 0) ? -1 : 1;
// Range sum identity
sum(l, r) = prefix[r+1] - prefix[l]
// Pivot condition (no HashMap needed)
2 * leftSum + nums[i] == total// BUG β misses subarrays that start from index 0
Map<Integer, Integer> map = new HashMap<>();
// If nums = [3], k = 3: sum=3, look for 3-3=0 β not in map β count=0 (WRONG, answer is 1)
// FIX β always seed before the loop
map.put(0, 1);
// Now sum=3, look for 0 β found with count 1 β count=1 β// BUG β Java % keeps the sign of the dividend
int sum = -7, k = 3;
int rem = sum % k; // rem = -1 (WRONG β we want 2, since -7 = (-3)*3 + 2)
// FIX β normalise to positive
int rem = ((sum % k) + k) % k; // (-1 + 3) % 3 = 2 β// BUG β overwriting the first occurrence loses the longest subarray
map.put(sum, i); // always update β shortest subarray, not longest
// FIX β only store first occurrence
if (!map.containsKey(sum)) map.put(sum, i); // keep earliest index β longest subarray β// BUG β prefix array of size n (should be n+1)
int[] prefix = new int[n];
prefix[0] = nums[0];
// sum(0, j) = prefix[j] β inconsistent, easy to get wrong bounds
// FIX β size n+1, prefix[0] = 0 always
int[] prefix = new int[n + 1]; // prefix[0] = 0 (implicit)
for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + nums[i];
// sum(l, r) = prefix[r+1] - prefix[l] β clean and consistent// BUG β sliding window only works for non-negative arrays
// For "shortest subarray with sum β₯ k" with negatives present:
// shrinking the window doesn't guarantee sum decreases β wrong answer
// FIX β use prefix sum + monotonic deque
// Deque handles negative numbers correctly because it doesn't assume monotone sums// BUG β double-subtracts the top-left corner
prefix[i][j] = nums[i-1][j-1] + prefix[i-1][j] + prefix[i][j-1];
// (i-1,j) and (i,j-1) both include (i-1,j-1) β subtracted twice
// FIX β add back the overlap
prefix[i][j] = nums[i-1][j-1]
+ prefix[i-1][j]
+ prefix[i][j-1]
- prefix[i-1][j-1]; // β add back once| Problem | Time | Space | Notes |
|---|---|---|---|
| Range Sum Query (static) | O(n) build, O(1) query | O(n) | precompute once, answer many |
| Subarray Sum Equals K | O(n) | O(n) | single pass + HashMap |
| Find Pivot Index | O(n) | O(1) | total sum β left prefix, no map needed |
| Subarray Sums Divisible by K | O(n) | O(k) | modulo map has at most k distinct keys |
| Contiguous Array (equal 0s/1s) | O(n) | O(n) | remap + first-seen HashMap |
| Shortest Subarray Sum β₯ K | O(n) | O(n) | prefix + monotonic deque |
| Count Range Sum | O(n log n) | O(n) | prefix + merge sort or BIT |
| Max Sum Rectangle β€ K | O(mΒ² Β· n log n) | O(n) | fix row pair, 1D problem per pair |
General rules:
- Prefix sum array build is always O(n) time, O(n) space.
- HashMap-based variants are O(n) time, O(n) space β one pass.
- Hard variants (deque, merge sort, BIT) are O(n log n) time.
- 2D variants multiply by the extra dimension: O(mΒ·n) build, O(1) query.
- When array has only non-negatives and you need min/max subarray β consider sliding window first (O(1) space). Use prefix sum when negatives are present or you need counts.
Prefix Sum is one of the most quietly powerful patterns in DSA. Master the HashMap extension β it converts O(nΒ²) brute force into O(n) elegantly.