|
| 1 | +// DIFFICULTY: MEDIUM |
| 2 | +// |
| 3 | +// Given an integer array nums and an integer k, return true if nums has a good subarray or false otherwise. |
| 4 | +// |
| 5 | +// A good subarray is a subarray where: |
| 6 | +// |
| 7 | +// - its length is at least two, and |
| 8 | +// - the sum of the elements of the subarray is a multiple of k. |
| 9 | +// |
| 10 | +// Note that: |
| 11 | +// |
| 12 | +// - A subarray is a contiguous part of the array. |
| 13 | +// - 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. |
| 14 | +// |
| 15 | +// See {@link https://leetcode.com/problems/continuous-subarray-sum/} |
| 16 | +export { checkSubarraySum }; |
| 17 | + |
| 18 | +// SOLUTION: |
| 19 | +// |
| 20 | +// The brute force solution is to calculate the sum of all subarrays of length 2 or more and check if it's a multiple of |
| 21 | +// of k. To do so, compute the prefix sum of the array and use it to calculate the sum of the subarray from [i, j]. |
| 22 | +// |
| 23 | +// A subarray sum from [i, j] can be calculated as prefixSum[j] - prefixSum[i] + nums[i]. We can check each subarray |
| 24 | +// sum to see if it's a multiple of k and return true if we find one. It is; however, O(n^2) to calculate subarray |
| 25 | +// sums in this way. Unfortunately, this will exceed the runtime limit for large arrays in LeetCode. |
| 26 | +// |
| 27 | +// To get a better solution we have to note that: |
| 28 | +// |
| 29 | +// sum[i, j] = prefixSum[j] - prefixSum[i] + nums[i] |
| 30 | +// sum[i, j] = prefixSum[j] - prefixSum[i - 1] |
| 31 | +// |
| 32 | +// This is because the sum[i, j] is inclusive, and subtracting prefixSum[i] subtracts out nums[i]. That's why we either |
| 33 | +// have to add it back in or use i - 1 as the index instead. Afterwards, we can look at the sum modulo k: |
| 34 | +// |
| 35 | +// sum[i, j] (mod k) = (prefixSum[j] - prefixSum[i - 1]) (mod k) |
| 36 | +// |
| 37 | +// If we want sum[i, j] % k === 0, then we should write: |
| 38 | +// |
| 39 | +// (prefixSum[j] - prefixSum[i - 1]) (mod k) = 0 |
| 40 | +// prefixSum[j] (mod k) = prefixSum[i - 1] (mod k) |
| 41 | +// |
| 42 | +// In other words, if prefixSums at positions (i - 1) and j have the same remainder modulo k, then the subarray sum from |
| 43 | +// [i, j] has remainder 0 modulo k. |
| 44 | +// |
| 45 | +// COMPLEXITY: |
| 46 | +// |
| 47 | +// Time complexity is O(n^2) because we are doing an outer and inner loop on the prefix sum array. |
| 48 | +// |
| 49 | +// Space complexity is O(n) because we are storing the prefix sum array. |
| 50 | +function checkSubarraySum(nums: number[], k: number): boolean { |
| 51 | + // Use a map of remainders to positions to store the prefix sum remainders, so we can check later if we have seen any |
| 52 | + // remainders that are the same modulo k. |
| 53 | + type Remainder = number; |
| 54 | + type Index = number; |
| 55 | + const remainders = new Map<Remainder, Index>(); |
| 56 | + |
| 57 | + // Maintain a running prefix sum as we loop through the array. |
| 58 | + let prefixSum = 0; |
| 59 | + for (let i = 0; i < nums.length; i++) { |
| 60 | + prefixSum += nums[i]; |
| 61 | + let remainder = prefixSum % k; |
| 62 | + |
| 63 | + // It might be the case that the remainder is negative, just as a quirk of how JavaScript handles the modulo |
| 64 | + // operator. |
| 65 | + // |
| 66 | + // For example, -5 % 3 = -2. But this is wrong because -2 is not congruent to -5 modulo 3. Instead: |
| 67 | + // |
| 68 | + // -5 (mod 3) = -5 + 3 (mod 3) = -2 (mod 3) = -2 + 3 (mod 3) = 1 (mod 3) |
| 69 | + // |
| 70 | + // That is, we want -5 % 3 = 1 instead. To fix this, we half to add the modulus to the remainder if it's negative. |
| 71 | + if (remainder < 0) { |
| 72 | + remainder += k; |
| 73 | + } |
| 74 | + |
| 75 | + // It's possible that the prefix sum already is a multiple of k. And it's possible that the remainder 0 isn't in |
| 76 | + // the remainders map yet. In this case, just check if we have a subarray of length 2 or more. |
| 77 | + if (remainder === 0 && i >= 1) { |
| 78 | + return true; |
| 79 | + } |
| 80 | + |
| 81 | + // If this is a previously seen remainder, we can check if we have a "good" subarray. |
| 82 | + if (remainders.has(remainder)) { |
| 83 | + const j = remainders.get(remainder)!; |
| 84 | + if (Math.abs(i - j) >= 2) { |
| 85 | + return true; |
| 86 | + } |
| 87 | + } |
| 88 | + // If we're seeing this remainder for the first time, store it in the map. |
| 89 | + else { |
| 90 | + remainders.set(remainder, i); |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + return false; |
| 95 | +} |
0 commit comments