Facebook Pixel

3729. Count Distinct Subarrays Divisible by K in Sorted Array

HardArrayHash TablePrefix Sum
LeetCode ↗

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:

  1. Divisibility condition — A subarray with sum s is good if and only if s % k == 0. Using prefix sums, a subarray nums[i+1..j] is good exactly when prefix[j] % k == prefix[i] % k.
  2. Sorted input — Because nums is 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 length m, a subarray of length h (all elements equal to v) appears m - h + 1 times positionally, but should only be counted once if its sum h * v is divisible by k.

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 with cnt[0] = 1).
  • Then subtracting the over-counted duplicates: for every maximal run of equal values of length m, and for each subarray length h from 1 to m, if (h * v) % k == 0, that value sequence was counted m - h + 1 times but should count only once, so we subtract the extra m - h occurrences.

The final result is the number of distinct good subarrays.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Prefix Sums?

This problem maps to Prefix Sums through a short path in the full flowchart.

Tree orgraph?noPrefixsums /range sum?yesPrefix Sums

Subarray divisibility by k is detected via prefix sum modulo matching, then duplicates from runs of equal values are subtracted.

Open in Flowchart

Intuition

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 m equal values v, a subarray of length h (for 1 <= h <= m) appears m - h + 1 times 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:

  1. Count all good subarrays by position using the prefix-sum-modulo trick — this over-counts duplicates.
  2. Walk through each maximal run of equal values of length m. For each length h where the sum h * v is divisible by k, that sequence was counted m - h + 1 times but should count once, so subtract the surplus m - h.

Two perspectives confirm this is efficient:

  • The prefix-sum pass is O(n).
  • The correction loop looks nested, but the inner loop over h runs m times per run, and the runs partition the array, so the total work is also O(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 s pairs with the current position to form a subarray whose sum is divisible by k, so we add cnt[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 while advances j past all elements equal to nums[i], so m = j - i is the length of the current run of the value v = nums[i].
  • For each possible subarray length h inside this run (1 <= h <= m), the subarray consists of h copies of v, with sum h * v. If (h * v) % k == 0, this sequence is good — but Phase 1 counted it m - h + 1 times (once for each starting position inside the run), while it should be counted exactly once. So we subtract the surplus m - h.
  • Finally, i = j jumps 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 pointers i and j only move forward, and the inner for h loop performs m iterations per run; since the runs partition the array, the total across all runs is n iterations.
  • Space complexity: O(min(n, k)) for the hash map cnt, since remainders modulo k take at most k distinct values, and at most n + 1 prefixes 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).

Stepxs = (s + x) % 2cnt[s] beforeans (+= cnt[s])cnt after
11(0 + 1) % 2 = 100{0: 1, 1: 1}
22(1 + 2) % 2 = 110 + 1 = 1{0: 1, 1: 2}
32(1 + 2) % 2 = 121 + 2 = 3{0: 1, 1: 3}
42(1 + 2) % 2 = 133 + 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, and 1 % 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 → counted m - h + 1 = 3 times, should be once → subtract m - h = 2. Now ans = 6 - 2 = 4.
  • h = 2: sum = 4, divisible by 2 → counted 2 times, should be once → subtract 1. Now ans = 4 - 1 = 3.
  • h = 3: sum = 6, divisible by 2 → counted 1 time, already correct → subtract 0. Still ans = 3.

Final Answer

ans = 3, matching the distinct good subarrays:

  1. [2]
  2. [2, 2]
  3. [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
47
1import 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}
58
1class 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};
48
1/**
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}
63

Time and Space Complexity

  • Time Complexity: O(n), where n is the length of nums.

    • The first loop iterates over all n elements once, performing O(1) work per element (prefix-sum update, hash table lookup, and increment), contributing O(n).
    • The second part scans the array using two pointers i and j to identify maximal blocks of equal consecutive elements. For a block of length m, the inner loop for h in range(1, m + 1) runs m times. Since the blocks partition the array, the total work is m_1 + m_2 + ... = n, so this phase is also O(n).
    • Overall: O(n) + O(n) = O(n).
  • Space Complexity: O(min(n, k)).

    • The Counter stores prefix sums modulo k, so it holds at most min(n + 1, k) distinct keys.
    • All other variables (ans, s, i, j, m, h) use constant extra space, so the hash table dominates.

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) % k is always non-negative. In C++, Java, or JavaScript, % can yield negative results if values can be negative, causing remainders like -2 and k - 2 to 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) % k to 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 Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

A 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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More