229 lines
10 KiB
YAML
229 lines
10 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
|
|
|
|
function_signature: "def check_subarray_sum(nums: list[int], k: int) -> bool:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { nums: [23, 2, 4, 6, 7], k: 6 }
|
|
expected: true
|
|
- input: { nums: [23, 2, 6, 4, 7], k: 6 }
|
|
expected: true
|
|
- input: { nums: [23, 2, 6, 4, 7], k: 13 }
|
|
expected: false
|
|
hidden:
|
|
- input: { nums: [0, 0], k: 1 }
|
|
expected: true
|
|
- input: { nums: [1, 0], k: 2 }
|
|
expected: false
|
|
- input: { nums: [5, 0, 0, 0], k: 3 }
|
|
expected: true
|
|
- input: { nums: [1, 2, 3], k: 5 }
|
|
expected: true
|
|
- input: { nums: [1, 2, 12], k: 6 }
|
|
expected: false
|
|
- input: { nums: [23, 2, 4, 6, 6], k: 7 }
|
|
expected: true
|
|
- input: { nums: [1, 1], k: 1 }
|
|
expected: true
|
|
|
|
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
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- If we complete the loop without finding a valid subarray, return `false`
|
|
|
|
|
|
|
|
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.
|