Files
codetutor/backend/data/questions/continuous-subarray-sum.yaml
2025-05-25 10:16:13 +01:00

203 lines
9.5 KiB
YAML

title: Continuous Subarray Sum
slug: continuous-subarray-sum
difficulty: medium
leetcode_id: 523
leetcode_url: https://leetcode.com/problems/continuous-subarray-sum/
categories:
- arrays
- hash-tables
- math
patterns:
- prefix-sum
description: |
Given an integer array `nums` and an integer `k`, return `true` if `nums` has a **good subarray** or `false` otherwise.
A **good subarray** is a subarray where:
- its length is **at least two**, and
- the sum of the elements of the subarray is a multiple of `k`.
**Note** that:
- A **subarray** is a contiguous part of the array.
- An integer `x` is a multiple of `k` if there exists an integer `n` such that `x = n * k`. `0` is **always** a multiple of `k`.
constraints: |
- `1 <= nums.length <= 10^5`
- `0 <= nums[i] <= 10^9`
- `0 <= sum(nums[i]) <= 2^31 - 1`
- `1 <= k <= 2^31 - 1`
examples:
- input: "nums = [23,2,4,6,7], k = 6"
output: "true"
explanation: "[2, 4] is a continuous subarray of size 2 whose elements sum up to 6."
- input: "nums = [23,2,6,4,7], k = 6"
output: "true"
explanation: "[23, 2, 6, 4, 7] is a continuous subarray of size 5 whose elements sum up to 42. 42 is a multiple of 6 because 42 = 7 * 6."
- input: "nums = [23,2,6,4,7], k = 13"
output: "false"
explanation: "No contiguous subarray of length at least 2 has a sum that is a multiple of 13."
explanation:
intuition: |
This problem seems to ask us to check every possible subarray sum, but that would be too slow. The key insight comes from **modular arithmetic** and **prefix sums**.
Imagine you're tracking a running total as you walk through the array. At each position, you calculate the prefix sum up to that point. Now, here's the crucial observation: if two prefix sums have the **same remainder when divided by `k`**, then the subarray between them has a sum that's a **multiple of `k`**.
Think of it like this: if `prefix[i] % k == prefix[j] % k` where `j > i`, then:
- `prefix[j] - prefix[i]` gives the sum of elements from index `i+1` to `j`
- Since both have the same remainder, their difference is divisible by `k`
For example, with `k = 6`: if `prefix[2] = 25` (remainder 1) and `prefix[5] = 49` (remainder 1), then the sum from index 3 to 5 is `49 - 25 = 24`, which is divisible by 6.
So instead of checking all subarray sums, we just need to find two positions with the **same prefix sum remainder** that are at least 2 indices apart.
approach: |
We solve this using a **Prefix Sum with Hash Map** approach:
**Step 1: Initialise the hash map**
- Create a hash map `remainder_index` to store the first index where each remainder was seen
- Set `remainder_index[0] = -1` to handle the case where the subarray starts from index 0
- Initialise `prefix_sum = 0` to track the running total
&nbsp;
**Step 2: Iterate through the array**
- For each element at index `i`, add it to `prefix_sum`
- Calculate `remainder = prefix_sum % k`
- If this remainder exists in our hash map:
- Check if `i - remainder_index[remainder] >= 2` (subarray length at least 2)
- If yes, return `true` — we found a valid subarray
- If this remainder is not in the hash map:
- Store it with the current index: `remainder_index[remainder] = i`
- We only store the *first* occurrence to maximise subarray length
&nbsp;
**Step 3: Return the result**
- If we complete the loop without finding a valid subarray, return `false`
&nbsp;
The key optimisation is that we only store the **first occurrence** of each remainder. This ensures that when we find a matching remainder later, the subarray between them is as long as possible, giving us the best chance of meeting the length requirement.
common_pitfalls:
- title: The Brute Force Trap
description: |
A naive approach would check every possible subarray:
- Outer loop `i` from `0` to `n-1`
- Inner loop `j` from `i+1` to `n-1`
- Calculate sum from `i` to `j` and check if divisible by `k`
This results in **O(n^2) time complexity** (or O(n^3) if summing naively). With `n = 10^5`, this means up to 10 billion operations — guaranteed **Time Limit Exceeded**.
wrong_approach: "Nested loops checking all subarray sums"
correct_approach: "Prefix sum with hash map for O(n) time"
- title: Forgetting the Base Case
description: |
The hash map must be initialised with `{0: -1}`, not empty. This handles subarrays that start from index 0.
For example, with `nums = [6, 1]` and `k = 6`:
- After index 0: `prefix_sum = 6`, `remainder = 0`
- Without `{0: -1}`, we wouldn't find a match
- With `{0: -1}`, we check `0 - (-1) = 1`, which fails the length check
- After index 1: `prefix_sum = 7`, `remainder = 1`
- But with `nums = [6, 6]`, after index 1: `remainder = 0`, and `1 - (-1) = 2` passes!
wrong_approach: "Starting with an empty hash map"
correct_approach: "Initialise with {0: -1} for subarrays starting at index 0"
- title: Updating Instead of Keeping First Index
description: |
When we see a remainder we've seen before, we should NOT update the hash map. We want the **earliest** index for each remainder to maximise the subarray length.
If we keep updating, we might miss valid subarrays:
- `remainder = 3` first seen at index 1
- `remainder = 3` seen again at index 2 (if we update, we lose index 1)
- `remainder = 3` seen at index 4: checking against index 2 gives length 2, but index 1 would give length 3
wrong_approach: "Always updating the hash map with current index"
correct_approach: "Only store the first occurrence of each remainder"
- title: Off-by-One in Length Check
description: |
The problem requires subarray length **at least 2**, not just "more than 1". The check should be `i - remainder_index[remainder] >= 2`.
With indices `i` and `j` where `j < i`, the subarray from `j+1` to `i` has length `i - j`. So we need `i - j >= 2`.
wrong_approach: "Checking `> 1` or `> 2` incorrectly"
correct_approach: "Check `i - stored_index >= 2`"
key_takeaways:
- "**Prefix sum + hash map**: A powerful combination for subarray sum problems. Store prefix sums (or their remainders) to find subarrays with specific properties in O(n) time."
- "**Modular arithmetic insight**: If `prefix[i] % k == prefix[j] % k`, then the sum between them is divisible by `k`. This transforms a sum problem into a remainder-matching problem."
- "**Base case matters**: Initialising with `{0: -1}` handles subarrays starting from index 0. Always consider edge cases involving the array start."
- "**Related problems**: This pattern applies to [Subarray Sum Equals K](/questions/subarray-sum-equals-k), Subarray Sums Divisible by K, and other prefix sum problems."
time_complexity: "O(n). We traverse the array once, and hash map operations (insert, lookup) are O(1) on average."
space_complexity: "O(min(n, k)). The hash map stores at most `k` distinct remainders (0 to k-1), or `n` entries if `n < k`."
solutions:
- approach_name: Prefix Sum with Hash Map
is_optimal: true
code: |
def check_subarray_sum(nums: list[int], k: int) -> bool:
# Map: remainder -> first index where this remainder was seen
# {0: -1} handles subarrays starting from index 0
remainder_index = {0: -1}
prefix_sum = 0
for i, num in enumerate(nums):
# Update running prefix sum
prefix_sum += num
# Get remainder when divided by k
remainder = prefix_sum % k
# Have we seen this remainder before?
if remainder in remainder_index:
# Check if subarray length is at least 2
if i - remainder_index[remainder] >= 2:
return True
# Don't update - keep the earliest index
else:
# First time seeing this remainder - store the index
remainder_index[remainder] = i
return False
explanation: |
**Time Complexity:** O(n) — Single pass through the array with O(1) hash map operations.
**Space Complexity:** O(min(n, k)) — At most `k` distinct remainders can exist.
We use the mathematical property that two prefix sums with the same remainder mod `k` define a subarray whose sum is divisible by `k`. By storing only the first occurrence of each remainder, we maximise our chances of finding a subarray of length at least 2.
- approach_name: Brute Force
is_optimal: false
code: |
def check_subarray_sum(nums: list[int], k: int) -> bool:
n = len(nums)
# Try every starting position
for i in range(n - 1):
# Accumulate sum for subarrays starting at i
subarray_sum = nums[i]
# Try every ending position (at least 2 elements)
for j in range(i + 1, n):
subarray_sum += nums[j]
# Check if sum is multiple of k
if subarray_sum % k == 0:
return True
return False
explanation: |
**Time Complexity:** O(n^2) — Nested loops checking all subarrays.
**Space Complexity:** O(1) — Only tracking the running sum.
This approach checks every possible subarray of length at least 2. While correct, it exceeds time limits for large inputs (n = 10^5). Included to illustrate why the prefix sum approach is necessary.