diff --git "a/content/posts/algorithm/data structure/\346\225\260\347\273\204-\345\211\215\347\274\200\345\222\214\346\225\260\347\273\204.md" "b/content/posts/algorithm/data structure/\346\225\260\347\273\204-\345\211\215\347\274\200\345\222\214\346\225\260\347\273\204.md" index c10a083..c429034 100644 --- "a/content/posts/algorithm/data structure/\346\225\260\347\273\204-\345\211\215\347\274\200\345\222\214\346\225\260\347\273\204.md" +++ "b/content/posts/algorithm/data structure/\346\225\260\347\273\204-\345\211\215\347\274\200\345\222\214\346\225\260\347\273\204.md" @@ -1,6 +1,7 @@ --- title: '数组-前缀和数组' date: 2023-02-19T15:57:07+08:00 +lastmod: 2024-02-20 tags: [Array] series: [data structure, trick] categories: [algorithm] @@ -8,7 +9,7 @@ categories: [algorithm] ### 概念 -应用场景:在原始数组不会被修改的情况下,**快速、频繁查询某个区间的累加和**。 +应用场景:在原始数组不会被修改的情况下,**快速、频繁查询某个区间的累加和**。通常涉及到连续子数组和相关问题时,就可以考虑使用前缀和技巧了。 核心思路:开辟新数组 `preSum[i]` 来存储原数组 `nums[0..i-1] `的累加和,`preSum[0] = 0`。这样,当求原数组区间和就比较容易了,区间 `[i..j]` 的和等于 `preSum[j+1] - preSum[i]` 的结果值。 @@ -16,13 +17,7 @@ categories: [algorithm] const preSum = [0] preSum[i] = preSum[i - 1] + nums[i - 1] // 查询 -preSum[r + 1] - preSum[l] - -// 或者 -const preSum = [nums[0]] -preSum[i] = preSum[i-1] + nums[i] -// 查询 -preSum[r] - preSum[l-1] // 需要判断 l 为 0 的情况,直接返回 preSum[r],这种的劣势是需要判断边界条件 +preSum[j + 1] - preSum[i] ``` ### 构造:[lc.303 区域和检索-数组不可变](https://leetcode.cn/problems/range-sum-query-immutable/) @@ -54,38 +49,6 @@ NumArray.prototype.sumRange = function(left, right) { */ ``` ---- - -上方那种事官解给出的逻辑,下面这种可能更容易理解一些。 - -```js -/** - * @param {number[]} nums - */ -var NumArray = function (nums) { - this.preSums = [nums[0]] - for (let i = 1; i < nums.length; ++i) { - this.preSums[i] = this.preSums[i - 1] + nums[i] - } -} - -/** - * @param {number} left - * @param {number} right - * @return {number} - */ -NumArray.prototype.sumRange = function (left, right) { - if (left === 0) return this.preSums[right] - return this.preSums[right] - this.preSums[left - 1] -} - -/** - * Your NumArray object will be instantiated and called as such: - * var obj = new NumArray(nums) - * var param_1 = obj.sumRange(left,right) - */ -``` - ### 构造:[lc.304 二维区域和检索-矩阵不可变](https://leetcode.cn/problems/range-sum-query-2d-immutable/) ```js @@ -130,9 +93,11 @@ NumMatrix.prototype.sumRegion = function (row1, col1, row2, col2) { ### lc.523 连续的子数组和 -这道题有点意思的,首先要知道一个数学知识:**同余定理:(a - b) % k == 0,则 a % k == b % k**。 +这道题有点意思的,首先要知道一个数学知识:**同余定理:a, b 模 k 后的余数相同,则 a,b 对 k 同余**,本题需要利用同余的整除性性质: + +** `a % k == b % k`,则 `(a-b) % k === 0`。即同余数之差能被 k 整除 ** -根据题意,需要获取到的信息是:**(preSum[j] - preSum[i]) % k === 0**,如过存在则返回 true,那么就可以转化为:**寻找是否有两个前缀和能 % k 后,余数相同的问题了~**,那就很自然想到用哈希表来存储 `{余数:索引}`了,不然这题还真挺难想的~ 再一次 respect 数学! +根据题意,需要获取到的信息是:**(preSum[j] - preSum[i]) % k === 0**,如过存在则返回 true,那么就可以转化为:**寻找是否有两个前缀和能 % k 后,余数相同的问题了~**,那就很自然想到用哈希表来存储 `{余数:索引}` 了,不然这题还真挺难想的~ 再一次 respect 数学! ```js var checkSubarraySum = function (nums, k) { @@ -141,14 +106,14 @@ var checkSubarraySum = function (nums, k) { let preSum = 0 for (let i = 0; i < nums.length; ++i) { preSum += nums[i] - let b = preSum % k - if (map[b] >= -1) { + let remainder = preSum % k + if (map[remainder] >= -1) { // 左开右闭区 - if (i - map[b] >= 2) { + if (i - map[remainder] >= 2) { return true } } else { - map[b] = i + map[remainder] = i } } return false @@ -268,8 +233,8 @@ Solution.prototype.pickIndex = function() { */ var subarraySum = function(nums, k) { const preSum = [0] - for(let i = 1; i <= nums.length; ++i) { - preSum[i] = preSum[i-1] + nums[i-1] + for(let i = 0; i < nums.length; ++i) { + preSum[i + 1] = preSum[i] + nums[i] } let count = 0 // 遍历出所有区间 @@ -404,12 +369,36 @@ var maxSubarraySumCircular = function (nums) { ### lc.974 和可被 k 整除的子数组 -与 lc.523 相呼应,如果不知道同余定理,则比较棘手。 +这题与题目 「lc.560 和为 K 的子数组」非常相似,同时与 lc.523 相呼应,如果不知道同余定理,则比较棘手。 ```js +/** + * @param {number[]} nums + * @param {number} k + * @return {number} + */ +var subarraysDivByK = function (nums, k) { + let res = 0 + let remainder = 0 + const map = { 0: 1 } // 存储 { %k : count } 这里求数量,则初始化为 1,之前有题是求距离,初始化为了 -1 + for (let i = 0; i < nums.length; ++i) { + // 当有负数时,js 语言的取模和数学上的取模是不一样的,所以为了修正这种逻辑,先 +个 k 再去模即可 + remainder = (((remainder + nums[i]) % k) + k) % k + // if(remainder < 0) remainder += k // 评论里看到也可以这样修正 + if (map[remainder]) { + res += map[remainder] + map[remainder]++ + } else { + map[remainder] = 1 + } + } + return res +} ``` +`remainder = (((remainder + nums[i]) % k) + k) % k`,这里我的理解是,因为使用了数学上的同余定理性质,所以程序的取余远算应当与之保持一致,比如 js 中 `-5 % 3 == -2`,数学中 `-5 % 3 == 1`。 + --- ### 前缀积与后缀积:[lc.238 除以自身以外的数组的乘积](https://leetcode.cn/problems/product-of-array-except-self/) @@ -462,3 +451,7 @@ var productExceptSelf = function (nums) { ``` > lc.327 lc.862 两道 hard 题,后续有时间再看看 😁 + +### 同余定理 + +> [维基百科 - 同余](https://zh.wikipedia.org/wiki/%E5%90%8C%E9%A4%98)