Problem overview
The function subarray_sum receives a vector of integers, nums, and a number target. It must find a contiguous run of elements whose sum equals target and return its first and last index as a two-element vector. If several runs qualify, any of them is accepted. If none does, it returns [-1, -1].
The numbers can be negative as well as positive. In the example, nums = [1, 1, 5, 2, 1, 3, 10, 2, 1] and target = 21. The elements at indices 2 to 6 are 5, 2, 1, 3 and 10, which add up to 21, so the answer is [2, 6].
The budget is O(n) time and O(n) extra space.
Approach: prefix sums and a lookup table
The brute-force idea tries every start and every end and adds up each run, which is O(n²) with a running sum and O(n³) without one. The usual speed-up for non-negative numbers is a sliding window, but it relies on the sum growing as the window grows. With negative numbers that fails, so the lesson uses another tool.
The prefix sum at a position is the sum of everything from index 0 up to that position. The sum of the run from index a to index i is the prefix sum at i minus the prefix sum just before a. So a run ending at i sums to target exactly when some earlier prefix sum equals sum - target, where sum is the prefix sum at i.
That turns the search into a lookup. While scanning left to right, remember every prefix sum seen so far, together with where the run that leaves that prefix out would start. At each position, ask whether sum - target is already in the table. A HashMap answers in constant time on average, so the whole scan is linear.
Take nums = [3, 4, -2, 5] and target = 7. The prefix sums are 3, 7, 5 and 10. At index 1 the sum is 7 and sum - target is 0. The empty prefix has sum 0 and the run would start at index 0, so the answer is [0, 1]: 3 + 4 = 7.
Step by step
- Create the table as a mutable
HashMap<i32, i32>. It maps a prefix sum to the index right after that prefix, which is where a run that leaves the prefix out begins. It ismutbecause it is modified. - Insert the empty prefix: sum 0, start index 0. Without this entry, a run that begins at index 0 could never be found, because there is no earlier prefix to subtract.
- Declare the running sum
sumas a mutablei32starting at 0. - Loop over each possible last index
iin0..nums.len(). - Add
nums[i]tosum, so it becomes the prefix sum ati. - Compute
diffassum - target: the prefix sum an earlier position would need for the run after it to totaltarget. - Look
diffup.getreturns anOptionholding a reference, soif let Some(&start)runs the body only on a hit and copies the stored index out of the reference. - On a hit, return
vec![start, i as i32]. The indexiis ausize, so it needs anas i32conversion to fit the vector ofi32. - Otherwise record the current prefix: insert
sumwith the valuei as i32 + 1. Theasoperator binds tighter than+, soiis converted first and then 1 is added. - After the loop, return
vec![-1, -1]with no semicolon, so it is the function's value.
The order inside the loop matters: the lookup comes before the insert. With target = 0, inserting first would let diff match the current prefix itself and report an empty run.
The solution in Rust
impl Solution {
fn subarray_sum(nums: Vec<i32>, target: i32) -> Vec<i32> {
let mut index_of_value: HashMap<i32, i32> = HashMap::new();
index_of_value.insert(0, 0);
let mut sum: i32 = 0;
for i in 0..nums.len() {
sum += nums[i];
let diff = sum - target;
if let Some(&start) = index_of_value.get(&diff) {
return vec![start, i as i32];
}
index_of_value.insert(sum, i as i32 + 1);
}
vec![-1, -1]
}
}
The Rust details worth a closer look:
getreturns anOption. A missing key givesNone, not a default value and not a panic.get(&diff)takes a reference to the key, and the result is anOption<&i32>.- The
&startpattern. InsideSome(&start), the&matches the reference and bindsstartto thei32behind it. Without it,startwould be a&i32, andvec![start, i as i32]would not compile. insertoverwrites. If a prefix sum appears twice, the secondinsertreplaces the stored start with the later one. That is harmless, since any valid run is accepted.returnversus the last expression. Leaving the loop early needsreturn. The finalvec![-1, -1]has no semicolon, so it is the value the function ends with.- Overflow. Built with optimizations, as the lessons are,
i32arithmetic that overflows wraps silently. A debug build would panic instead. The solution assumes sums that fit in ani32.
Complexity
Time: O(n). The loop runs at most n times, and each pass does one addition, one subtraction, one lookup and one insertion. The hash map operations take constant time on average.
Space: O(n). The map holds at most one entry for the empty prefix plus one per element.
Edge cases and common mistakes
- A run that starts at index 0. For
[1, 2, 3]and target 6, the answer[0, 2]depends on the entry for the empty prefix. Forgetting it gives[-1, -1]. - Target 0 with a zero-sum run. For
[4, -4, 3]and target 0, the prefix sums are 4, 0, 3. At index 1 the sum is 0,diffis 0, and the table returns start 0, so the answer is[0, 1]. - Target 0 with no such run. For
[1, 2, 3]the function returns[-1, -1]. This is the case where inserting before the lookup would wrongly report an empty run. - Empty vector. The loop does not run and the function returns
[-1, -1]. - Repeated prefix sums. For
[5, -2, -3, 4]and target 4, the prefix sums are 5, 3, 0, 4. The sum 0 is stored first for the empty prefix and again at index 2, where it is overwritten with start 3. At index 3 the function returns[3, 3], the single element 4. It is a valid run, though not the longest one. - Negative numbers. A sliding window that shrinks when the sum is too large breaks as soon as a negative number appears. The prefix-sum lookup does not care about signs.
Testing
nums = [1, 1, 5, 2, 1, 3, 10, 2, 1],target = 21gives[2, 6].nums = [1, 2, 3],target = 6gives[0, 2].nums = [4, -4, 3],target = 0gives[0, 1].nums = [3, -3, 5],target = 5gives[2, 2].nums = [1, 2],target = 10gives[-1, -1].
