3729. Count Distinct Subarrays Divisible by K in Sorted Array
Problem Description
You are given an integer array nums sorted in non-descending order and a positive integer k.
A subarray of nums (a contiguous, non-empty sequence of elements) is called good if the sum of its elements is divisible by k.
Your task is to return the number of distinct good subarrays of nums.
The key twist here is the word distinct: two subarrays are considered the same if their sequences of values are identical, even if they occupy different positions in the array. In other words, duplicates by value must be counted only once.
For example, in the array [1, 1, 1], although there are 6 possible subarray positions, there are only 3 distinct subarrays by value:
[1][1, 1][1, 1, 1]
So when counting good subarrays, you must be careful not to count the same value sequence more than once.
Two important properties shape the problem:
- Divisibility condition — A subarray with sum
sis good if and only ifs % k == 0. Using prefix sums, a subarraynums[i+1..j]is good exactly whenprefix[j] % k == prefix[i] % k. - Sorted input — Because
numsis sorted in non-descending order, any two subarrays with identical value sequences must consist of a run of equal elements. This means duplicate subarrays can only arise inside maximal blocks of repeated values, e.g., a block like[3, 3, 3, 3]. Within a block of lengthm, a subarray of lengthh(all elements equal tov) appearsm - h + 1times positionally, but should only be counted once if its sumh * vis divisible byk.
Therefore, the overall answer can be obtained by:
- First counting all good subarrays by position (the classic prefix-sum-modulo counting technique with a hash map
cnt, starting withcnt[0] = 1). - Then subtracting the over-counted duplicates: for every maximal run of equal values of length
m, and for each subarray lengthhfrom1tom, if(h * v) % k == 0, that value sequence was countedm - h + 1times but should count only once, so we subtract the extram - hoccurrences.
The final result is the number of distinct good subarrays.
How We Pick the Algorithm
Why Prefix Sums?
This problem maps to Prefix Sums through a short path in the full flowchart.
Subarray divisibility by k is detected via prefix sum modulo matching, then duplicates from runs of equal values are subtracted.
Open in FlowchartIntuition
If the problem only asked for the number of subarrays whose sum is divisible by k, it would be a classic prefix-sum problem: a subarray nums[i+1..j] has a sum divisible by k exactly when prefix[j] % k == prefix[i] % k. So we can scan the array once, keep a running sum modulo k, and use a hash map to count how many earlier prefixes share the same remainder — each match contributes one good subarray. This counts every good subarray by position.
The real difficulty is the word distinct: identical value sequences appearing at different positions must be counted only once. Counting distinct subarrays in a general array is hard (it usually needs suffix structures), so we should ask: why does the problem tell us the array is sorted?
Here is the key observation. Suppose two different positions hold the same value sequence, say nums[i..i+L-1] and nums[j..j+L-1] with i < j. The two windows overlap or are adjacent in value terms: the first element of the second copy, nums[j], must equal nums[i], and since the array is non-descending, every element between index i and j + L - 1 is squeezed between equal values — so they are all equal. In other words, in a sorted array, duplicate subarrays can only occur inside a maximal run of identical elements.
This turns a hard "count distinct" problem into a simple correction step:
- Within a run of
mequal valuesv, a subarray of lengthh(for1 <= h <= m) appearsm - h + 1times positionally, but it represents just one distinct value sequence. - Outside such runs, every positional subarray is automatically distinct, so the prefix-sum count is already correct for them.
So the plan emerges naturally:
- Count all good subarrays by position using the prefix-sum-modulo trick — this over-counts duplicates.
- Walk through each maximal run of equal values of length
m. For each lengthhwhere the sumh * vis divisible byk, that sequence was countedm - h + 1times but should count once, so subtract the surplusm - h.
Two perspectives confirm this is efficient:
- The prefix-sum pass is
O(n). - The correction loop looks nested, but the inner loop over
hrunsmtimes per run, and the runs partition the array, so the total work is alsoO(n).
By combining the classic counting technique with the structural guarantee provided by sortedness, we get an exact count of distinct good subarrays without ever needing heavyweight string/sequence deduplication machinery.
Pattern Learn more about Prefix Sum patterns.
Solution Approach
The solution runs in two phases: count everything by position, then subtract the duplicates that only sorted arrays can produce inside runs of equal values.
Phase 1: Count all good subarrays by position (prefix sum + hash map)
We use a Counter named cnt to record how many prefixes have produced each remainder modulo k. It is initialized as cnt = Counter({0: 1}) — the empty prefix has sum 0, which allows subarrays starting at index 0 to be counted correctly.
cnt = Counter({0: 1}) ans = s = 0 for x in nums: s = (s + x) % k ans += cnt[s] cnt[s] += 1
For each element x:
- Update the running prefix sum modulo
k:s = (s + x) % k. - Every earlier prefix with the same remainder
spairs with the current position to form a subarray whose sum is divisible byk, so we addcnt[s]to the answer. - Record the current remainder:
cnt[s] += 1.
After this loop, ans equals the number of good subarrays counted positionally — duplicates included.
Phase 2: Subtract over-counted duplicates inside equal-value runs
Because nums is sorted, identical value sequences can only appear inside a maximal block of repeated elements. We scan the array with a two-pointer pattern to locate each such block:
n = len(nums)
i = 0
while i < n:
j = i + 1
while j < n and nums[j] == nums[i]:
j += 1
m = j - i
for h in range(1, m + 1):
if (h * nums[i]) % k == 0:
ans -= m - h
i = j
- The inner
whileadvancesjpast all elements equal tonums[i], som = j - iis the length of the current run of the valuev = nums[i]. - For each possible subarray length
hinside this run (1 <= h <= m), the subarray consists ofhcopies ofv, with sumh * v. If(h * v) % k == 0, this sequence is good — but Phase 1 counted itm - h + 1times (once for each starting position inside the run), while it should be counted exactly once. So we subtract the surplusm - h. - Finally,
i = jjumps to the start of the next run.
Note that subarrays which are not fully contained inside a single run are guaranteed to be distinct by value (the sortedness argument from the intuition), so no correction is needed for them.
Why the result is exact
- Phase 1 counts every good
(start, end)pair: this equals the distinct count plus the duplicate surplus. - Phase 2 removes precisely that surplus, run by run, length by length.
Complexity Analysis
- Time complexity:
O(n). The prefix-sum pass visits each element once. In Phase 2, the pointersiandjonly move forward, and the innerfor hloop performsmiterations per run; since the runs partition the array, the total across all runs isniterations. - Space complexity:
O(min(n, k))for the hash mapcnt, since remainders moduloktake at mostkdistinct values, and at mostn + 1prefixes are recorded.
Example Walkthrough
Let's trace the algorithm on nums = [1, 2, 2, 2] and k = 2.
Phase 1: Count all good subarrays by position
We maintain a running prefix sum s modulo k, and a counter cnt initialized to {0: 1} (representing the empty prefix).
| Step | x | s = (s + x) % 2 | cnt[s] before | ans (+= cnt[s]) | cnt after |
|---|---|---|---|---|---|
| 1 | 1 | (0 + 1) % 2 = 1 | 0 | 0 | {0: 1, 1: 1} |
| 2 | 2 | (1 + 2) % 2 = 1 | 1 | 0 + 1 = 1 | {0: 1, 1: 2} |
| 3 | 2 | (1 + 2) % 2 = 1 | 2 | 1 + 2 = 3 | {0: 1, 1: 3} |
| 4 | 2 | (1 + 2) % 2 = 1 | 3 | 3 + 3 = 6 | {0: 1, 1: 4} |
After Phase 1, ans = 6. Let's verify by listing every positional subarray whose sum is divisible by 2:
[2]at index 1,[2]at index 2,[2]at index 3 → 3 subarrays[2, 2]at indices 1–2,[2, 2]at indices 2–3 → 2 subarrays[2, 2, 2]at indices 1–3 → 1 subarray
That's 3 + 2 + 1 = 6 positional matches — but only 3 distinct value sequences: [2], [2, 2], and [2, 2, 2]. The duplicates all live inside the run of equal 2s, exactly as the sortedness argument predicts.
Phase 2: Subtract duplicates inside equal-value runs
Scan for maximal runs of equal values:
Run 1: value v = 1, length m = 1 (index 0).
h = 1: sum= 1 * 1 = 1, and1 % 2 ≠ 0→ not good, nothing to subtract.
Run 2: value v = 2, length m = 3 (indices 1–3).
h = 1: sum= 2, divisible by 2 → countedm - h + 1 = 3times, should be once → subtractm - h = 2. Nowans = 6 - 2 = 4.h = 2: sum= 4, divisible by 2 → counted2times, should be once → subtract1. Nowans = 4 - 1 = 3.h = 3: sum= 6, divisible by 2 → counted1time, already correct → subtract0. Stillans = 3.
Final Answer
ans = 3, matching the distinct good subarrays:
[2][2, 2][2, 2, 2]
This small case showcases both phases working together: Phase 1's prefix-sum counting captures every good subarray by position (6 of them), and Phase 2's run-based correction removes exactly the surplus copies (3 of them) produced by the repeated 2s, leaving the precise distinct count.
Solution Implementation
1from typing import List
2from collections import Counter
3
4
5class Solution:
6 def numGoodSubarrays(self, nums: List[int], k: int) -> int:
7 # prefix_count records how many times each prefix-sum remainder
8 # (modulo k) has appeared; remainder 0 starts with count 1 to
9 # account for subarrays beginning at index 0.
10 prefix_count = Counter({0: 1})
11 answer = 0
12 prefix_sum = 0
13
14 # Step 1: count all subarrays whose sum is divisible by k.
15 # Two prefix sums with the same remainder modulo k bound a
16 # subarray whose sum is a multiple of k.
17 for num in nums:
18 prefix_sum = (prefix_sum + num) % k
19 # Every previous prefix with the same remainder forms a valid subarray.
20 answer += prefix_count[prefix_sum]
21 prefix_count[prefix_sum] += 1
22
23 n = len(nums)
24 left = 0
25
26 # Step 2: remove the unwanted subarrays that consist of identical
27 # elements. Scan each maximal run of equal values.
28 while left < n:
29 right = left + 1
30 # Extend the run while elements stay equal.
31 while right < n and nums[right] == nums[left]:
32 right += 1
33
34 run_length = right - left
35
36 # For every possible subarray length inside this run,
37 # check whether its sum (length * value) is divisible by k,
38 # and subtract the corresponding occurrences.
39 for sub_length in range(1, run_length + 1):
40 if (sub_length * nums[left]) % k == 0:
41 answer -= run_length - sub_length
42
43 # Jump to the start of the next run.
44 left = right
45
46 return answer
471import java.util.HashMap;
2import java.util.Map;
3
4class Solution {
5 public long numGoodSubarrays(int[] nums, int k) {
6 long answer = 0;
7
8 // Running prefix sum modulo k
9 int prefixMod = 0;
10
11 // Map from (prefix sum % k) -> number of prefixes seen with that remainder.
12 // Initialize with remainder 0 (the empty prefix) so that subarrays
13 // starting at index 0 are counted correctly.
14 Map<Integer, Integer> remainderCount = new HashMap<>();
15 remainderCount.put(0, 1);
16
17 // Step 1: Count all subarrays whose sum is divisible by k.
18 // A subarray (i, j] has sum divisible by k iff prefix[i] % k == prefix[j] % k.
19 for (int num : nums) {
20 prefixMod = (prefixMod + num) % k;
21 // Every previously seen prefix with the same remainder forms a valid subarray.
22 answer += remainderCount.getOrDefault(prefixMod, 0);
23 // Record the current remainder for future subarrays.
24 remainderCount.merge(prefixMod, 1, Integer::sum);
25 }
26
27 int n = nums.length;
28
29 // Step 2: Correct the overcount caused by runs of identical elements.
30 // Scan the array run by run (maximal blocks of equal values).
31 for (int start = 0; start < n; ) {
32 // Find the end of the current run of equal elements.
33 int end = start + 1;
34 while (end < n && nums[end] == nums[start]) {
35 end++;
36 }
37
38 int runLength = end - start;
39
40 // For each possible subarray length inside this run, check whether
41 // a subarray of identical values of that length has a sum divisible by k.
42 for (int len = 1; len <= runLength; len++) {
43 if (1L * nums[start] * len % k == 0) {
44 // There are (runLength - len + 1) such subarrays inside the run;
45 // keep exactly one of them and remove the remaining (runLength - len)
46 // duplicates from the answer.
47 answer -= (runLength - len);
48 }
49 }
50
51 // Jump to the next run.
52 start = end;
53 }
54
55 return answer;
56 }
57}
581class Solution {
2public:
3 long long numGoodSubarrays(vector<int>& nums, int k) {
4 long long answer = 0;
5 int prefixMod = 0;
6
7 // Map from prefix-sum remainder (mod k) to the number of times it has appeared.
8 unordered_map<int, int> remainderCount;
9 remainderCount[0] = 1; // The empty prefix has remainder 0.
10
11 // Step 1: count all subarrays whose sum is divisible by k.
12 // A subarray (i, j] has a sum divisible by k iff
13 // prefix[i] and prefix[j] share the same remainder mod k.
14 for (int num : nums) {
15 prefixMod = (prefixMod + num) % k;
16 // Every earlier prefix with the same remainder forms one valid subarray.
17 answer += remainderCount[prefixMod]++;
18 }
19
20 int n = nums.size();
21
22 // Step 2: correct the count over runs of consecutive equal elements.
23 for (int left = 0; left < n;) {
24 int right = left + 1;
25 // Extend the window while the values stay identical, forming one run.
26 while (right < n && nums[right] == nums[left]) {
27 ++right;
28 }
29 int runLength = right - left;
30
31 // Inside a run of identical values, a subarray of length `len`
32 // has sum nums[left] * len. If that sum is divisible by k,
33 // such a subarray appears (runLength - len + 1) times in the run,
34 // but only one occurrence should be kept; subtract the extras.
35 for (int len = 1; len <= runLength; ++len) {
36 if (1LL * nums[left] * len % k == 0) {
37 answer -= (runLength - len);
38 }
39 }
40
41 // Jump to the start of the next run.
42 left = right;
43 }
44
45 return answer;
46 }
47};
481/**
2 * Counts the number of "good" subarrays.
3 * A good subarray is one whose sum is divisible by k,
4 * excluding certain subarrays consisting of identical elements
5 * that are over-counted by the prefix-sum technique.
6 *
7 * @param nums - The input array of numbers
8 * @param k - The divisor
9 * @returns The number of good subarrays
10 */
11function numGoodSubarrays(nums: number[], k: number): number {
12 let ans: number = 0;
13
14 // Running prefix sum modulo k
15 let prefixSum: number = 0;
16
17 // Map from (prefix sum mod k) -> number of times it has appeared.
18 // Initialize with {0: 1} to account for subarrays starting at index 0.
19 const remainderCount: Map<number, number> = new Map<number, number>();
20 remainderCount.set(0, 1);
21
22 // Step 1: Count all subarrays whose sum is divisible by k.
23 // Two prefix sums with the same remainder mod k form such a subarray.
24 for (const num of nums) {
25 prefixSum = (prefixSum + num) % k;
26
27 // Every previous prefix with the same remainder forms a valid subarray.
28 ans += remainderCount.get(prefixSum) ?? 0;
29
30 // Record the current remainder.
31 remainderCount.set(prefixSum, (remainderCount.get(prefixSum) ?? 0) + 1);
32 }
33
34 const n: number = nums.length;
35
36 // Step 2: Remove over-counted subarrays within runs of equal elements.
37 // Scan each maximal block of identical values.
38 for (let i = 0; i < n; ) {
39 // Find the end of the current block of equal elements.
40 let j: number = i + 1;
41 while (j < n && nums[j] === nums[i]) {
42 ++j;
43 }
44
45 // Length of the current block of identical values.
46 const blockLength: number = j - i;
47
48 // For each possible subarray length h inside the block,
49 // if a subarray of h identical elements sums to a multiple of k,
50 // subtract the number of extra occurrences within the block.
51 for (let h = 1; h <= blockLength; ++h) {
52 if ((nums[i] * h) % k === 0) {
53 ans -= blockLength - h;
54 }
55 }
56
57 // Jump to the start of the next block.
58 i = j;
59 }
60
61 return ans;
62}
63Time and Space Complexity
-
Time Complexity:
O(n), wherenis the length ofnums.- The first loop iterates over all
nelements once, performingO(1)work per element (prefix-sum update, hash table lookup, and increment), contributingO(n). - The second part scans the array using two pointers
iandjto identify maximal blocks of equal consecutive elements. For a block of lengthm, the inner loopfor h in range(1, m + 1)runsmtimes. Since the blocks partition the array, the total work ism_1 + m_2 + ... = n, so this phase is alsoO(n). - Overall:
O(n) + O(n) = O(n).
- The first loop iterates over all
-
Space Complexity:
O(min(n, k)).- The
Counterstores prefix sums modulok, so it holds at mostmin(n + 1, k)distinct keys. - All other variables (
ans,s,i,j,m,h) use constant extra space, so the hash table dominates.
- The
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Subtracting m - h + 1 instead of m - h for duplicate runs
This is the single most frequent mistake in this problem. When a run of m equal values
contains a good subarray of length h, Phase 1 has counted it m - h + 1 times
(once per starting position inside the run). The natural — but wrong — instinct is to
subtract everything it was counted:
# WRONG: removes the subarray entirely, including the one copy we must keep if (sub_length * nums[left]) % k == 0: answer -= run_length - sub_length + 1
This deletes all occurrences, so a perfectly good distinct subarray contributes zero to the answer. The sequence should still be counted exactly once, so only the surplus must be removed:
# CORRECT: keep one occurrence, remove the extra (m - h) duplicates if (sub_length * nums[left]) % k == 0: answer -= run_length - sub_length
A quick sanity check that catches this bug: nums = [2, 2], k = 2.
Distinct good subarrays are [2] and [2, 2], so the answer must be 2.
Phase 1 counts 3 positional subarrays ([2] twice, [2, 2] once). With the wrong
formula you subtract 2 + 1 = 3 and return 0; with the correct one you subtract
only 1 and return 2.
Pitfall 2: Forgetting to initialize the remainder map with {0: 1}
If prefix_count starts empty, every good subarray that begins at index 0
(i.e., a prefix whose own sum is divisible by k) is silently missed:
# WRONG: prefixes that are themselves divisible by k are never counted prefix_count = Counter()
The empty prefix has sum 0, so seeding prefix_count = Counter({0: 1}) lets the
running remainder s == 0 pair with it correctly. Test with nums = [3], k = 3:
the empty map returns 0, the seeded map returns the correct 1.
Pitfall 3: Incrementing the counter before reading it
The order inside the loop matters. Updating the map first makes the current position pair with itself, counting the empty subarray at every index:
# WRONG: counts a zero-length subarray at each position prefix_count[prefix_sum] += 1 answer += prefix_count[prefix_sum]
Always read first, then record:
# CORRECT answer += prefix_count[prefix_sum] prefix_count[prefix_sum] += 1
Pitfall 4: Trying to deduplicate with a global hash of subarrays
A tempting "safe" approach is to store every subarray (e.g., as a tuple) in a set and
count distinct good ones directly. This ignores the sorted-input property, requires
O(n²) subarrays of up to O(n) length each, and blows up to O(n³) time and memory.
The sortedness guarantee — duplicates can only occur inside maximal runs of equal
values — is exactly what makes the O(n) correction in Phase 2 sufficient, so the
expensive deduplication structure is never needed.
Pitfall 5: Language-specific modulo and overflow issues when porting
The Python code is robust, but a direct translation can break elsewhere:
- Negative remainders: In Python,
(s + x) % kis always non-negative. In C++, Java, or JavaScript,%can yield negative results if values can be negative, causing remainders like-2andk - 2to be treated as different keys. Normalize with((s % k) + k) % k. - Integer overflow:
sub_length * nums[left]can exceed 32-bit range for large runs and values. Use 64-bit integers (long/long long), or better, compute(sub_length % k) * (nums[left] % k) % kto keep intermediates small.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapA person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
Recommended Readings
Prefix Sum Technique Explained Prefix Sum A prefix sum transforms an array into a new array of running totals For an input array arr define prefix k as the sum of all elements before index k prefix 0 0 prefix 1 arr 0 prefix 2 arr 0 arr 1 and so on The power
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!