523. Continuous Subarray Sum
Problem Description
You are given an integer array nums
and an integer k
. Your task is to determine whether the array contains a "good subarray."
A subarray is considered "good" if it meets both of these conditions:
- It has a length of at least 2 elements
- The sum of its elements is a multiple of
k
Important points to remember:
- A subarray must be contiguous (elements must be consecutive in the original array)
- Any integer
x
is a multiple ofk
if there exists some integern
wherex = n * k
- The value
0
is always considered a multiple ofk
The function should return true
if at least one good subarray exists in nums
, and false
otherwise.
For example, if nums = [23, 2, 4, 6, 7]
and k = 6
, the subarray [2, 4]
would be a good subarray because its length is 2 and its sum (6) is a multiple of 6.
Intuition
The key insight comes from understanding how modular arithmetic works with prefix sums. When we want to find if a subarray has a sum that's a multiple of k
, we can leverage the fact that if two prefix sums have the same remainder when divided by k
, then the subarray between them must have a sum divisible by k
.
Let's think about it step by step:
-
If we have a prefix sum up to index
i
assum_i
and a prefix sum up to indexj
assum_j
(wherej < i
), then the sum of the subarray fromj+1
toi
equalssum_i - sum_j
. -
For this subarray sum to be divisible by
k
, we need(sum_i - sum_j) % k = 0
. -
This happens when
sum_i % k = sum_j % k
. In other words, both prefix sums have the same remainder when divided byk
.
So instead of checking every possible subarray (which would be inefficient), we can track the remainders of prefix sums as we go through the array. If we ever see the same remainder appearing at two different positions that are at least 2 indices apart, we know there's a good subarray between them.
Why do we need the positions to be at least 2 indices apart? Because the subarray needs to have at least 2 elements. If we find the same remainder at positions j
and i
where i - j > 1
, then the subarray from index j+1
to i
contains at least i - (j+1) + 1 = i - j
elements, which is at least 2.
The hash table approach naturally falls out from this insight - we store each remainder with its first occurrence position, and when we see that remainder again, we check if enough distance has passed to form a valid subarray.
Learn more about Math and Prefix Sum patterns.
Solution Approach
We implement the solution using a prefix sum technique combined with a hash table to efficiently track remainders.
Here's how the algorithm works:
-
Initialize the hash table: We create a dictionary
d
with an initial entry{0: -1}
. This represents that a remainder of0
appears at position-1
(before the array starts). This handles the edge case where a valid subarray starts from index0
. -
Track the running sum: We maintain a variable
s
to store the running prefix sum modulok
. We update it ass = (s + x) % k
for each elementx
in the array. -
Process each element: For each element at index
i
:- Calculate the new prefix sum remainder:
s = (s + nums[i]) % k
- Check if this remainder has been seen before:
- If not seen: Store the current remainder with its position:
d[s] = i
- If seen before: Check if the distance between the current position
i
and the stored positiond[s]
is greater than 1 (meaning at least 2 elements in between). Ifi - d[s] > 1
, we've found a valid subarray and returnTrue
.
- If not seen: Store the current remainder with its position:
- Calculate the new prefix sum remainder:
-
Return result: If we complete the iteration without finding any valid subarray, return
False
.
The algorithm's efficiency comes from:
- Time Complexity:
O(n)
wheren
is the length of the array, as we traverse it only once - Space Complexity:
O(min(n, k))
for the hash table, which stores at mostk
different remainders
The beauty of this approach is that we don't need to explicitly calculate subarray sums. By tracking remainders and their positions, we can determine if a valid subarray exists in a single pass through the array.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with nums = [23, 2, 4, 6, 7]
and k = 6
.
Step-by-step execution:
Initialization:
- Hash table
d = {0: -1}
(remainder 0 at position -1) - Running sum
s = 0
Index 0 (value = 23):
- Calculate new sum:
s = (0 + 23) % 6 = 23 % 6 = 5
- Check if remainder 5 exists in
d
: No - Add to hash table:
d = {0: -1, 5: 0}
- Continue
Index 1 (value = 2):
- Calculate new sum:
s = (5 + 2) % 6 = 7 % 6 = 1
- Check if remainder 1 exists in
d
: No - Add to hash table:
d = {0: -1, 5: 0, 1: 1}
- Continue
Index 2 (value = 4):
- Calculate new sum:
s = (1 + 4) % 6 = 5 % 6 = 5
- Check if remainder 5 exists in
d
: Yes! It was stored at index 0 - Check distance:
2 - 0 = 2 > 1
✓ - Found a valid subarray!
The algorithm returns True
because we found that the subarray from index 1 to index 2 (elements [2, 4]
) has a sum of 6, which is divisible by 6.
Why this works: When we saw remainder 5 at index 0 and again at index 2, it means:
- Prefix sum up to index 0:
23
has remainder5
- Prefix sum up to index 2:
23 + 2 + 4 = 29
has remainder5
- Therefore, the subarray sum between them:
29 - 23 = 6
is divisible by 6 (remainder 0)
Solution Implementation
1class Solution:
2 def checkSubarraySum(self, nums: List[int], k: int) -> bool:
3 # Dictionary to store the first occurrence index of each remainder
4 # Initialize with remainder 0 at index -1 to handle edge cases
5 remainder_to_index = {0: -1}
6
7 # Running sum modulo k
8 running_sum = 0
9
10 # Iterate through the array with index and value
11 for index, num in enumerate(nums):
12 # Update running sum and take modulo k
13 running_sum = (running_sum + num) % k
14
15 # If this remainder hasn't been seen before, record its first occurrence
16 if running_sum not in remainder_to_index:
17 remainder_to_index[running_sum] = index
18 # If remainder was seen before and subarray length is at least 2
19 elif index - remainder_to_index[running_sum] > 1:
20 # Found a valid subarray whose sum is divisible by k
21 return True
22
23 # No valid subarray found
24 return False
25
1class Solution {
2 public boolean checkSubarraySum(int[] nums, int k) {
3 // HashMap to store the remainder and its earliest index
4 // Key: remainder when sum is divided by k
5 // Value: the earliest index where this remainder appears
6 Map<Integer, Integer> remainderIndexMap = new HashMap<>();
7
8 // Initialize with remainder 0 at index -1
9 // This handles the case where the subarray starts from index 0
10 remainderIndexMap.put(0, -1);
11
12 // Running sum of the array elements
13 int runningSum = 0;
14
15 // Iterate through each element in the array
16 for (int currentIndex = 0; currentIndex < nums.length; currentIndex++) {
17 // Update running sum and calculate remainder
18 runningSum = (runningSum + nums[currentIndex]) % k;
19
20 // Check if this remainder has been seen before
21 if (!remainderIndexMap.containsKey(runningSum)) {
22 // First time seeing this remainder, store current index
23 remainderIndexMap.put(runningSum, currentIndex);
24 } else if (currentIndex - remainderIndexMap.get(runningSum) > 1) {
25 // If the remainder was seen before and the subarray length > 1
26 // then we found a valid subarray whose sum is divisible by k
27 return true;
28 }
29 }
30
31 // No valid subarray found
32 return false;
33 }
34}
35
1class Solution {
2public:
3 bool checkSubarraySum(vector<int>& nums, int k) {
4 // Map to store the first occurrence index of each remainder
5 // Initialize with remainder 0 at index -1 to handle edge cases
6 unordered_map<int, int> remainderIndexMap{{0, -1}};
7
8 int cumulativeSum = 0;
9
10 // Iterate through the array
11 for (int i = 0; i < nums.size(); ++i) {
12 // Calculate cumulative sum modulo k
13 cumulativeSum = (cumulativeSum + nums[i]) % k;
14
15 // If this remainder hasn't been seen before, record its first occurrence
16 if (remainderIndexMap.find(cumulativeSum) == remainderIndexMap.end()) {
17 remainderIndexMap[cumulativeSum] = i;
18 }
19 // If remainder was seen before and subarray length is at least 2
20 else if (i - remainderIndexMap[cumulativeSum] > 1) {
21 return true;
22 }
23 }
24
25 return false;
26 }
27};
28
1/**
2 * Checks if there exists a continuous subarray of size at least 2
3 * that sums up to a multiple of k
4 * @param nums - Array of integers to check
5 * @param k - The divisor to check if subarray sum is a multiple of
6 * @returns true if such a subarray exists, false otherwise
7 */
8function checkSubarraySum(nums: number[], k: number): boolean {
9 // Map to store the first occurrence index of each remainder
10 // Initialize with remainder 0 at index -1 to handle edge cases
11 const remainderToIndexMap: Record<number, number> = { 0: -1 };
12
13 // Running sum of elements
14 let runningSum: number = 0;
15
16 // Iterate through each element in the array
17 for (let currentIndex = 0; currentIndex < nums.length; currentIndex++) {
18 // Update running sum and calculate remainder when divided by k
19 runningSum = (runningSum + nums[currentIndex]) % k;
20
21 // Check if this remainder has been seen before
22 if (!remainderToIndexMap.hasOwnProperty(runningSum)) {
23 // First time seeing this remainder, store the current index
24 remainderToIndexMap[runningSum] = currentIndex;
25 } else if (currentIndex - remainderToIndexMap[runningSum] > 1) {
26 // Found a subarray with length > 1 whose sum is divisible by k
27 // This works because if two prefix sums have the same remainder,
28 // the subarray between them has a sum divisible by k
29 return true;
30 }
31 }
32
33 // No valid subarray found
34 return false;
35}
36
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the array nums
. The algorithm iterates through the array exactly once using a single for loop with enumerate(nums)
. Within each iteration, all operations performed are constant time: computing the modulo ((s + x) % k
), dictionary lookup (s not in d
), dictionary insertion (d[s] = i
), and arithmetic comparison (i - d[s] > 1
).
The space complexity is O(min(n, k))
, which can be simplified to O(n)
in the worst case. The dictionary d
stores remainder values as keys. Since we're taking modulo k
, there can be at most k
distinct remainders (from 0
to k-1
). However, in the worst case where all elements produce different remainders and k ≥ n
, we could store up to n
entries in the dictionary. Therefore, the space complexity is bounded by the minimum of n
and k
, but asymptotically we consider it as O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Handling the Minimum Length Requirement Correctly
The Pitfall: A common mistake is checking index - remainder_to_index[running_sum] >= 1
instead of > 1
. This would incorrectly accept subarrays of length 1.
Why it happens: The distance calculation can be confusing. If the same remainder appears at indices i
and j
, the subarray between them is nums[i+1:j+1]
, which has length j - i
.
Example:
- If
nums = [5, 0, 0, 0]
andk = 3
- At index 0: remainder is 2, store
{2: 0}
- At index 1: remainder is still 2, distance is
1 - 0 = 1
- Using
>= 1
would incorrectly returnTrue
for a single-element subarray
Solution: Always use strict inequality > 1
to ensure at least 2 elements in the subarray.
2. Updating the Hash Table Too Early
The Pitfall: Updating remainder_to_index[running_sum] = index
before checking if the remainder already exists would overwrite the first occurrence, making it impossible to find the longest valid subarray starting from an earlier position.
Incorrect approach:
# WRONG: This overwrites the first occurrence running_sum = (running_sum + num) % k remainder_to_index[running_sum] = index # Updates immediately if index - remainder_to_index[running_sum] > 1: # Always false! return True
Solution: Only update the hash table when the remainder hasn't been seen before:
# CORRECT: Preserve the first occurrence if running_sum not in remainder_to_index: remainder_to_index[running_sum] = index elif index - remainder_to_index[running_sum] > 1: return True
3. Forgetting the Initial Hash Table Entry
The Pitfall: Not initializing the hash table with {0: -1}
causes the algorithm to miss valid subarrays that start from index 0.
Example:
- If
nums = [6, 3, 5, 2]
andk = 9
- The subarray
[6, 3]
has sum 9, which is divisible by 9 - Without
{0: -1}
, at index 1 we have remainder 0, but no previous occurrence to compare with
Solution: Always initialize with remainder_to_index = {0: -1}
to handle prefix sums that are themselves multiples of k
.
4. Not Handling Negative Numbers Properly
The Pitfall: In some programming languages, the modulo operation with negative numbers can produce negative remainders, which breaks the algorithm's logic.
Example in some languages:
-5 % 3
might return-2
instead of1
Solution: Ensure positive remainders by using:
running_sum = ((running_sum + num) % k + k) % k
Or in Python, which handles this correctly by default, the standard modulo operation works fine.
Which of the following shows the order of node visit in a Breadth-first Search?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
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
Want a Structured Path to Master System Design Too? Don’t Miss This!