195 lines
8.6 KiB
YAML
195 lines
8.6 KiB
YAML
title: Subarray Sum Equals K
|
|
slug: subarray-sum-equals-k
|
|
difficulty: medium
|
|
leetcode_id: 560
|
|
leetcode_url: https://leetcode.com/problems/subarray-sum-equals-k/
|
|
categories:
|
|
- arrays
|
|
- hash-tables
|
|
patterns:
|
|
- prefix-sum
|
|
|
|
function_signature: "def subarray_sum(nums: list[int], k: int) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { nums: [1, 1, 1], k: 2 }
|
|
expected: 2
|
|
- input: { nums: [1, 2, 3], k: 3 }
|
|
expected: 2
|
|
hidden:
|
|
- input: { nums: [1], k: 1 }
|
|
expected: 1
|
|
- input: { nums: [1], k: 0 }
|
|
expected: 0
|
|
- input: { nums: [-1, -1, 1], k: 0 }
|
|
expected: 1
|
|
- input: { nums: [1, -1, 0], k: 0 }
|
|
expected: 3
|
|
- input: { nums: [3, 4, 7, 2, -3, 1, 4, 2], k: 7 }
|
|
expected: 4
|
|
- input: { nums: [0, 0, 0, 0], k: 0 }
|
|
expected: 10
|
|
- input: { nums: [1, 2, 3], k: 6 }
|
|
expected: 1
|
|
|
|
description: |
|
|
Given an array of integers `nums` and an integer `k`, return *the total number of subarrays whose sum equals to* `k`.
|
|
|
|
A subarray is a contiguous **non-empty** sequence of elements within an array.
|
|
|
|
constraints: |
|
|
- `1 <= nums.length <= 2 * 10^4`
|
|
- `-1000 <= nums[i] <= 1000`
|
|
- `-10^7 <= k <= 10^7`
|
|
|
|
examples:
|
|
- input: "nums = [1,1,1], k = 2"
|
|
output: "2"
|
|
explanation: "The subarrays [1,1] (indices 0-1) and [1,1] (indices 1-2) both sum to 2."
|
|
- input: "nums = [1,2,3], k = 3"
|
|
output: "2"
|
|
explanation: "The subarrays [1,2] and [3] both sum to 3."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're walking along a number line, keeping a running total of all the numbers you've seen so far. This running total is called a **prefix sum**.
|
|
|
|
Here's the key insight: if you've reached a certain prefix sum `current_sum`, and you want to find a subarray ending at this position that sums to `k`, you need to find a *previous* prefix sum that equals `current_sum - k`. Why? Because the difference between two prefix sums gives you the sum of the subarray between them.
|
|
|
|
Think of it like this: if your cumulative total at position `j` is 10, and your cumulative total at position `i` was 3, then the sum of elements from index `i+1` to `j` is `10 - 3 = 7`. So if you want a subarray sum of `k = 7`, you look for any previous prefix sum of `current_sum - k = 10 - 7 = 3`.
|
|
|
|
The trick is to use a **hash map** to count how many times each prefix sum has occurred. As you iterate through the array, you can instantly look up how many times `current_sum - k` has appeared before — each occurrence represents a valid subarray ending at the current index.
|
|
|
|
approach: |
|
|
We solve this using the **Prefix Sum + Hash Map** technique:
|
|
|
|
**Step 1: Initialise tracking variables**
|
|
|
|
- `count`: Set to `0` to track the total number of valid subarrays
|
|
- `current_sum`: Set to `0` to track the running prefix sum
|
|
- `prefix_counts`: A hash map initialised with `{0: 1}` — this accounts for subarrays that start from index 0
|
|
|
|
|
|
|
|
**Step 2: Iterate through the array**
|
|
|
|
- Add the current element to `current_sum`
|
|
- Calculate `complement = current_sum - k`
|
|
- If `complement` exists in `prefix_counts`, add its count to our result — each occurrence represents a valid subarray ending here
|
|
- Add `current_sum` to `prefix_counts` (increment its count if it already exists)
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- Return `count` after processing all elements
|
|
|
|
|
|
|
|
The hash map initialisation `{0: 1}` is crucial: it handles the case where a prefix sum *exactly* equals `k`, meaning the subarray starts from index 0.
|
|
|
|
common_pitfalls:
|
|
- title: The Brute Force Trap
|
|
description: |
|
|
A natural first approach is to check every possible subarray with nested loops:
|
|
- Outer loop for start index
|
|
- Inner loop for end index
|
|
- Calculate sum for each subarray
|
|
|
|
This results in **O(n^2) time complexity** (or O(n^3) if you recalculate sums each time). With `n = 2 * 10^4`, this means up to 400 million operations — too slow!
|
|
wrong_approach: "Nested loops checking every subarray"
|
|
correct_approach: "Prefix sum with hash map for O(n) lookup"
|
|
|
|
- title: Forgetting to Initialise the Hash Map with Zero
|
|
description: |
|
|
If you start with an empty hash map, you'll miss subarrays that begin at index 0.
|
|
|
|
For example, with `nums = [3]` and `k = 3`: the prefix sum after the first element is 3, and we need `current_sum - k = 3 - 3 = 0` to exist in our map. Without `{0: 1}` initialisation, we'd return 0 instead of 1.
|
|
wrong_approach: "Starting with an empty hash map"
|
|
correct_approach: "Initialise with {0: 1} to handle subarrays starting at index 0"
|
|
|
|
- title: Using Sliding Window Instead
|
|
description: |
|
|
Sliding window doesn't work here because the array can contain **negative numbers**. With negative numbers, expanding the window doesn't guarantee the sum increases, and shrinking doesn't guarantee it decreases.
|
|
|
|
Sliding window is only valid when all elements are positive (or all negative), where the sum is monotonic with window size.
|
|
wrong_approach: "Sliding window technique"
|
|
correct_approach: "Prefix sum with hash map (handles negative numbers)"
|
|
|
|
- title: Updating the Hash Map Before Checking
|
|
description: |
|
|
The order of operations matters. You must check for `current_sum - k` in the hash map *before* adding `current_sum` to the map.
|
|
|
|
If you add first, you might count a subarray of length 0 (from an index to itself) when `k = 0`.
|
|
wrong_approach: "Adding current_sum to map before checking complement"
|
|
correct_approach: "Check complement first, then add current_sum to map"
|
|
|
|
key_takeaways:
|
|
- "**Prefix sum pattern**: The difference between two prefix sums gives the sum of elements in between — a powerful technique for range sum queries"
|
|
- "**Hash map for O(1) lookups**: Instead of searching for complements with nested loops, store counts in a hash map"
|
|
- "**Initialisation matters**: Starting with `{0: 1}` handles edge cases where the subarray starts at index 0"
|
|
- "**When sliding window fails**: This problem demonstrates why sliding window requires monotonic relationships — negative numbers break that assumption"
|
|
|
|
time_complexity: "O(n). We traverse the array once, with O(1) hash map operations at each step."
|
|
space_complexity: "O(n). In the worst case, all prefix sums are unique, requiring O(n) space in the hash map."
|
|
|
|
solutions:
|
|
- approach_name: Prefix Sum with Hash Map
|
|
is_optimal: true
|
|
code: |
|
|
def subarray_sum(nums: list[int], k: int) -> int:
|
|
# Count of valid subarrays found
|
|
count = 0
|
|
# Running prefix sum
|
|
current_sum = 0
|
|
# Map of prefix_sum -> count of occurrences
|
|
# Initialise with 0:1 to handle subarrays starting at index 0
|
|
prefix_counts = {0: 1}
|
|
|
|
for num in nums:
|
|
# Update running sum
|
|
current_sum += num
|
|
|
|
# Check if (current_sum - k) exists in our map
|
|
# If so, there are that many subarrays ending here with sum k
|
|
complement = current_sum - k
|
|
if complement in prefix_counts:
|
|
count += prefix_counts[complement]
|
|
|
|
# Add current prefix sum to the map
|
|
prefix_counts[current_sum] = prefix_counts.get(current_sum, 0) + 1
|
|
|
|
return count
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass through the array with O(1) hash map operations.
|
|
|
|
**Space Complexity:** O(n) — Hash map stores up to n unique prefix sums.
|
|
|
|
We use the mathematical insight that `sum(i, j) = prefix_sum(j) - prefix_sum(i-1)`. By storing prefix sum counts in a hash map, we can instantly find how many previous positions would create a valid subarray ending at the current position.
|
|
|
|
- approach_name: Brute Force
|
|
is_optimal: false
|
|
code: |
|
|
def subarray_sum(nums: list[int], k: int) -> int:
|
|
count = 0
|
|
n = len(nums)
|
|
|
|
# Try every possible starting index
|
|
for i in range(n):
|
|
current_sum = 0
|
|
# Try every possible ending index
|
|
for j in range(i, n):
|
|
current_sum += nums[j]
|
|
# Check if this subarray sums to k
|
|
if current_sum == k:
|
|
count += 1
|
|
|
|
return count
|
|
explanation: |
|
|
**Time Complexity:** O(n^2) — Nested loops check all possible subarrays.
|
|
|
|
**Space Complexity:** O(1) — Only tracking count and current_sum.
|
|
|
|
This approach checks every contiguous subarray by trying all start/end combinations. While correct, it's too slow for large inputs. Included to show why the prefix sum optimisation is necessary.
|