Shortest Subarray with Sum at Least K
HardPrompt
Given an integer array nums and an integer k, return the length of the shortest non-empty subarray of nums with a sum of at least k. If there is no such subarray, return -1.
A subarray is a contiguous part of an array.
Example 1:
Input: nums = [1], k = 1
Output: 1Example 2:
Input: nums = [1,2], k = 4
Output: -1Example 3:
Input: nums = [2,-1,2], k = 3
Output: 3
Constraints:
1 <= nums.length <= 105-105 <= nums[i] <= 1051 <= k <= 109
Approaches
2 approaches with complexity analysis and trade-offs.
A straightforward but inefficient approach involves pre-calculating prefix sums to quickly find the sum of any subarray. We then iterate through all possible start and end points of a subarray, check if its sum is at least k, and keep track of the minimum length found.
Algorithm
- Create a prefix sum array
prefixof sizen + 1. Uselongto prevent integer overflow, as sums can be large. - Populate the
prefixarray:prefix[i+1] = prefix[i] + nums[i]forifrom0ton-1. - Initialize
minLength = n + 1to store the result. - Iterate with a start index
ifrom0ton-1. - Inside this loop, iterate with an end index
jfromiton-1. - Calculate the subarray sum:
sum = prefix[j+1] - prefix[i]. - If
sum >= k, updateminLength = Math.min(minLength, j - i + 1). - After the loops, if
minLengthis stilln + 1, return-1. Otherwise, returnminLength.
Walkthrough
This method improves upon a naive brute-force solution by optimizing the sum calculation for each subarray.
-
Prefix Sum Calculation: First, we create a prefix sum array,
prefix, of sizen + 1, wherenis the length ofnums.prefix[i]stores the sum of elements fromnums[0]tonums[i-1].prefix[0]is initialized to 0. This allows us to calculate the sum of any subarraynums[i...j]in constant time using the formulaprefix[j+1] - prefix[i]. This pre-computation step takes O(n) time. -
Iterating Subarrays: After computing the prefix sums, we use two nested loops to examine every possible non-empty subarray. The outer loop selects a starting index
ifrom0ton-1, and the inner loop selects an ending indexjfromiton-1. -
Checking Condition: For each subarray defined by
(i, j), we calculate its sumprefix[j+1] - prefix[i]. If this sum is greater than or equal tok, we have found a valid subarray. We then update our minimum length found so far withmin(minLength, j - i + 1). -
Result: We initialize
minLengthto a value larger thann(e.g.,n + 1). IfminLengthremains this value after checking all subarrays, it means no solution exists, and we return -1. Otherwise, we return the finalminLength.
class Solution { public int shortestSubarray(int[] nums, int k) { int n = nums.length; long[] prefix = new long[n + 1]; for (int i = 0; i < n; i++) { prefix[i + 1] = prefix[i] + nums[i]; } int minLength = n + 1; for (int i = 0; i < n; i++) { for (int j = i; j < n; j++) { long sum = prefix[j + 1] - prefix[i]; if (sum >= k) { minLength = Math.min(minLength, j - i + 1); } } } return minLength == n + 1 ? -1 : minLength; }}Complexity
Time
O(n^2), where n is the number of elements in `nums`. The prefix sum calculation takes O(n), but the nested loops to check every subarray dominate the runtime.
Space
O(n) to store the prefix sum array.
Trade-offs
Pros
More efficient than a naive O(n^3) brute-force approach.
Relatively easy to understand the logic based on prefix sums.
Cons
The O(n^2) time complexity is too slow for the given constraints (n <= 10^5) and will result in a 'Time Limit Exceeded' error.
The optimal solution uses prefix sums combined with a sliding window managed by a monotonic deque. This approach achieves linear time complexity by efficiently finding the shortest valid subarray ending at each position. The deque helps maintain potential starting points for subarrays in a way that handles the complication of negative numbers.
Algorithm
- Create a
longprefix sum arrayprefixof sizen + 1and populate it. - Initialize
minLength = n + 1and an emptyDeque<Integer> dq. - Iterate with index
yfrom0ton(representing the end of the subarray). - Check for valid subarrays: While the deque is not empty and
prefix[y] - prefix[dq.peekFirst()] >= k, updateminLength = min(minLength, y - dq.pollFirst())and remove the first element from the deque. - Maintain deque monotonicity: While the deque is not empty and
prefix[y] <= prefix[dq.peekLast()], remove the last element from the deque. - Add the current index
yto the end of the deque. - After the loop, if
minLength > n, return-1. Otherwise, returnminLength.
Walkthrough
This advanced approach leverages prefix sums to reframe the problem and a deque to maintain candidate indices efficiently.
The problem is to find the smallest y - x such that sum(nums[x...y-1]) >= k. Using prefix sums P, this is equivalent to finding the smallest y - x such that P[y] - P[x] >= k.
For a fixed endpoint y, we want to find an x < y that satisfies P[x] <= P[y] - k and is as large as possible (to minimize y - x).
We can process the prefix sums P from left to right (indexed by y). We use a deque to store indices x of promising starting points. The deque will store indices d_1, d_2, ... such that their corresponding prefix sums are strictly increasing: P[d_1] < P[d_2] < ....
Algorithm Steps:
- Prefix Sums: Compute the prefix sum array
Pof sizen+1. Uselongto avoid overflow. - Initialization: Create an empty deque
dqto store indices and initializeminLength = n + 1. - Iteration: Iterate through the prefix sum array with index
yfrom0ton. a. Condition Check (Shrinking Window from Left): Look at the index at the front of the deque,x = dq.peekFirst(). IfP[y] - P[x] >= k, we've found a valid subarray of lengthy - x. We updateminLengthand then removexfrom the deque (dq.pollFirst()). We remove it because for any future endpointy' > y, a subarray starting atxwould be longer thany' - x', wherex'is another valid start. We've found the shortest possible subarray starting atx, so it's no longer needed. b. Monotonicity Maintenance (Optimizing Candidates): Look at the index at the back of the deque,x_last = dq.peekLast(). IfP[y] <= P[x_last], we removex_lastfrom the deque (dq.pollLast()). The reason is thatyis a better future candidate for a starting index thanx_last. If we havex_last < yandP[x_last] >= P[y], then for any future endpointz > y, ifP[z] - P[x_last] >= k, it must be thatP[z] - P[y] >= kas well. The subarray starting aty(z-y) would be shorter than the one starting atx_last(z-x_last). So,x_lastbecomes redundant. c. Add to Window: Add the current indexyto the back of the deque.
After iterating through all y, if minLength is still n + 1, no such subarray exists. Otherwise, minLength holds the answer.
import java.util.ArrayDeque;import java.util.Deque; class Solution { public int shortestSubarray(int[] nums, int k) { int n = nums.length; long[] prefix = new long[n + 1]; for (int i = 0; i < n; i++) { prefix[i + 1] = prefix[i] + nums[i]; } int minLength = n + 1; Deque<Integer> dq = new ArrayDeque<>(); for (int y = 0; y <= n; y++) { // Condition: prefix[y] - prefix[x] >= k while (!dq.isEmpty() && prefix[y] - prefix[dq.peekFirst()] >= k) { minLength = Math.min(minLength, y - dq.pollFirst()); } // Maintain monotonic deque: prefix[d1] < prefix[d2] < ... while (!dq.isEmpty() && prefix[y] <= prefix[dq.peekLast()]) { dq.pollLast(); } dq.addLast(y); } return minLength == n + 1 ? -1 : minLength; }}Complexity
Time
O(n), where n is the length of `nums`. Each index is added to and removed from the deque at most once. The entire process is a single pass over the prefix sum array.
Space
O(n) for storing the prefix sum array and the deque. In the worst-case scenario, the deque could potentially store all `n+1` indices.
Trade-offs
Pros
Optimal O(n) time complexity, making it very efficient for large inputs.
Correctly handles arrays with negative numbers, which is a key challenge in this problem.
Cons
The logic is more complex and less intuitive than the brute-force approach.
Requires careful implementation to handle the deque operations and edge cases correctly.
Solutions
Solution
class Solution {public int shortestSubarray(int[] nums, int k) { int n = nums.length; long[] s = new long[n + 1]; for (int i = 0; i < n; ++i) { s[i + 1] = s[i] + nums[i]; } Deque<Integer> q = new ArrayDeque<>(); int ans = n + 1; for (int i = 0; i <= n; ++i) { while (!q.isEmpty() && s[i] - s[q.peek()] >= k) { ans = Math.min(ans, i - q.poll()); } while (!q.isEmpty() && s[q.peekLast()] >= s[i]) { q.pollLast(); } q.offer(i); } return ans > n ? -1 : ans; }}function shortestSubarray ( nums , k ) { const [ n , MAX ] = [ nums . length , Number . POSITIVE_INFINITY ]; const s = Array ( n + 1 ). fill ( 0 ); const q = []; let ans = MAX ; for ( let i = 0 ; i < n ; i ++ ) { s [ i + 1 ] = s [ i ] + nums [ i ]; } for ( let i = 0 ; i < n + 1 ; i ++ ) { while ( q . length && s [ i ] - s [ q [ 0 ]] >= k ) { ans = Math . min ( ans , i - q . shift ()); } while ( q . length && s [ i ] <= s [ q . at ( - 1 )]) { q . pop (); } q . push ( i ); } return ans === MAX ? - 1 : ans ; }class Solution {public: int shortestSubarray(vector<int> &nums, int k) { int n = nums.size(); vector<long> s(n + 1); for (int i = 0; i < n; ++i) s[i + 1] = s[i] + nums[i]; deque<int> q; int ans = n + 1; for (int i = 0; i <= n; ++i) { while (!q.empty() && s[i] - s[q.front()] >= k) { ans = min(ans, i - q.front()); q.pop_front(); } while (!q.empty() && s[q.back()] >= s[i]) q.pop_back(); q.push_back(i); } return ans > n ? -1 : ans; }};class Solution: def shortestSubarray(self, nums: List[int], k: int) -> int: s = list(accumulate(nums, initial=0)) q = deque() ans = inf for i, v in enumerate(s): while q and v - s[q[0]] >= k: ans = min(ans, i - q . popleft()) while q and s[q[- 1]] >= v: q . pop() q . append(i) return - 1 if ans == inf else ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.