Count subarrays with a sum divisible by k
You're given an integer array nums that may hold negative numbers and a positive integer
k. I'd like you to count how many contiguous subarrays have a sum that divides exactly by k.
Ranges are counted by where they sit in the array, so two ranges holding the same values at different positions
both count towards the total.
Examples
| Input | Output | Why |
|---|---|---|
nums = [4, 5, 0, -2, -3, 1], k = 5 | 7 | They're [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3] and the whole array. |
nums = [5], k = 9 | 0 | There's only the one range here and 5 doesn't divide by 9. |
nums = [0, 0, 0], k = 3 | 6 | Every one of the six ranges adds up to 0, and 0 divides by anything. |
Constraints
- 1 ≤ n ≤ 3 × 10⁴
- −10⁴ ≤ nums[i] ≤ 10⁴
- 1 ≤ k ≤ 10⁴
Hints
Hint 1
I'd start from prefix sums again because a range is still the difference P[j] − P[i] of two prefixes.
Hint 2
That difference divides by k exactly when P[i] and P[j] leave the same remainder after dividing by k, so the remainder is what you should be looking up.
Hint 3
Keep a count of how many prefixes you've seen with each remainder and start it at {0: 1}. Then watch what your language does with the remainder of a negative number.
Where people lose marks
- The mistake I see most often in interviews is taking the remainder with a plain % in a language where it can come out negative. However tidy the rest of the solution is, that quietly splits one remainder class into two separate keys, and the answer comes out too small with no error to warn you.
- Forgetting to start the count for remainder 0 at 1 rather than 0. Everything still compiles and runs happily, and every range that begins at the very first element of the array disappears from the total.
- Recording the current prefix in the counts before you've read them. Each prefix then matches itself, so the empty range gets counted once at every step of the sweep. Overall, I'd recommend reading first and recording second, in this problem and in the earlier counting one.
Dry run on [4,5,0,-2,-3,1],5
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 8nums405102-23-3415count1001020304total0
There are only 5 remainders, so the map is an array of 5 counts, with count[0] = 1 for the empty prefix.
-
Step 2 / 8nums40j5102-23-3415count1001020304prefix4rem4total0
We add 4 and get a prefix of 4, which leaves remainder 4, and because no earlier prefix has left that remainder nothing new ends here.
-
Step 3 / 8nums4051j02-23-3415count1001020314prefix9rem4total1
We add 5 and get a prefix of 9, which leaves remainder 4, and since we've already seen 1 prefix with that remainder there is 1 more range ending here, which brings the total to 1.
-
Step 4 / 8nums405102j-23-3415count1001020324prefix9rem4total3
We add 0 and get a prefix of 9, which leaves remainder 4, and since we've already seen 2 prefixes with that remainder there are 2 more ranges ending here, which brings the total to 3.
-
Step 5 / 8nums405102-23j-3415count1001020334prefix7rem2total3
We add -2 and get a prefix of 7, which leaves remainder 2, and because no earlier prefix has left that remainder nothing new ends here.
-
Step 6 / 8nums405102-23-34j15count1001120334prefix4rem4total6
We add -3 and get a prefix of 4, which leaves remainder 4, and since we've already seen 3 prefixes with that remainder there are 3 more ranges ending here, which brings the total to 6.
-
Step 7 / 8nums405102-23-3415jcount1001120344prefix5rem0total7
We add 1 and get a prefix of 5, which leaves remainder 0, and since we've already seen 1 prefix with that remainder there is 1 more range ending here, which brings the total to 7.
-
Step 8 / 8nums405102-23-3415total7
One pass, and 7 ranges have a sum divisible by 5.
How to think about it
You'll usually see this one called Subarray Sums Divisible by K. I'd describe it as the same
sweep we used for counting ranges that add up to k, with one important difference in the key we store
in the map.
A range from i to j − 1 adds up to P[j] − P[i], and that difference is
divisible by k precisely when the two prefixes leave identical remainders after division by
k. Consequently, as we sweep along the array, we maintain a map from every remainder to the number of
prefixes we've already encountered with it, and at each position we add the count for the current remainder to the
answer before incrementing it. Because there are only k possible remainders, the map can simply be an
array of length k, which keeps the extra memory proportional to k whatever the length of the
input happens to be.
The detail that catches people out is the sign of the remainder. With the % operator in C++, Java,
JavaScript or Rust, the remainder of minus 3 divided by 5 comes out as minus 3 when we actually want 2, which is why I'd
always write ((p % k) + k) % k in those languages. Python's % and Haskell's mod
already return a remainder between 0 and k − 1 for a positive k, so they don't need the
correction at all.
Complexity. Time O(n) · Space O(k)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
def divisible_sums(nums, k):
count = [0] * k
count[0] = 1 # the empty prefix
p = total = 0
for x in nums:
p += x
r = p % k # Python's % is already between 0 and k - 1
total += count[r] # read first...
count[r] += 1 # ...then record
return total#include <vector>
using namespace std;
long long solve(vector<int> nums, int k) {
vector<long long> count(k, 0);
count[0] = 1; // the empty prefix
long long p = 0, total = 0;
for (int x : nums) {
p += x;
int r = (int)(((p % k) + k) % k); // C++'s % can be negative
total += count[r]; // read first...
count[r]++; // ...then record
}
return total;
}class Solution {
public long divisibleSums(int[] nums, int k) {
long[] count = new long[k];
count[0] = 1; // the empty prefix
long p = 0, total = 0;
for (int x : nums) {
p += x;
int r = (int) (((p % k) + k) % k); // Java's % can be negative
total += count[r]; // read first...
count[r]++; // ...then record
}
return total;
}
}function divisibleSums(nums, k) {
const count = new Array(k).fill(0);
count[0] = 1; // the empty prefix
let p = 0, total = 0;
for (const x of nums) {
p += x;
const r = ((p % k) + k) % k; // JavaScript's % can be negative
total += count[r]; // read first...
count[r]++; // ...then record
}
return total;
}fn solve(nums: &[i64], k: i64) -> i64 {
let mut count = vec![0i64; k as usize];
count[0] = 1; // the empty prefix
let (mut p, mut total) = (0i64, 0i64);
for &x in nums {
p += x;
let r = p.rem_euclid(k) as usize; // never negative, unlike %
total += count[r]; // read first...
count[r] += 1; // ...then record
}
total
}import qualified Data.Map.Strict as M
solve :: [Int] -> Int -> Int
solve xs k = go (M.singleton 0 1) 0 0 xs
where
-- mod is already non-negative for positive k
go _ _ total [] = total
go m p total (x : rest) =
let p' = p + x
r = p' `mod` k
hit = M.findWithDefault 0 r m
in go (M.insertWith (+) r 1 m) p' (total + hit) rest
Variants to try
- Decide whether some range of length at least 2 has a sum divisible by k. (Store the first index of each remainder in place of a count and check how far back it is.)
- Let k be 0 as well. (Then you're back to counting ranges that add up to exactly 0, which is the earlier counting problem with a target of 0.)
Next problems
Count subarrays with a given sum
Negative numbers break the sliding window. Prefix sums in a hash map fix it, and the counting detail is where people slip.
Open the problem → Medium15 minProduct of every element except itself
Dividing the total by each element is everyone's first answer, and one zero breaks it. Two passes fix it without dividing at all.
Open the problem → Medium20 minLongest subarray with a given sum
I'd call this the counting problem's twin, with the same prefix map and one change, which is that you keep the first index of every total.
Open the problem →Write a solution and run it against the real test table.
Keep going
Count subarrays with a given sum
Negative numbers break the sliding window. Prefix sums in a hash map fix it, and the counting detail is where people slip.
Open the problem → Medium15 minProduct of every element except itself
Dividing the total by each element is everyone's first answer, and one zero breaks it. Two passes fix it without dividing at all.
Open the problem → Medium20 minLongest subarray with a given sum
I'd call this the counting problem's twin, with the same prefix map and one change, which is that you keep the first index of every total.
Open the problem → Arrays and hashingThe pattern behind it
Trade memory for time: a hash map answers "have I seen this?" in O(1), and most array problems reduce to asking it the right question.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.