前缀和 也是一个 常用的技巧。
但是理解止步于概念,在实战中不习惯用到,这里进行专项突破。
套路总结:
对于数组:nums (长度 n)
对应前缀和数组:preSums (长度为n+1), preSums[i] 表示前 i 个 元素的和。
例如:
preSums[0] 表示 前 0个元素的和
preSums[1] 表示 前 1个元素的和, 即 nums[0]
preSums[2] 表示 前 2个元素的和, 即 nums[0] + nums[1]
…
preSums[n] 表示 前 n个元素的和, 即 nums[0] + nums[1] + … + nums[n - 1]
这样规定 preSums 的好处是,preSums[j] - preSums[i] ,当 i = 0,就可方便表示 前 j 个数的和。
// 初始条件:const preSums = [0];let map = new Map(); // key 为前缀和,value 为 出现次数map.set(0, 1); // 前缀和为0,即 preSums[0],的次数为 1.为什么是0呢?因为加法中,0 没有副作用// 计算前缀和for (let j = 0; j < n; j++) {preSums[j + 1] = preSums[j] + nums[j];map.set(preSums[j + 1], (map.get(preSums[j + 1]) || 0) + 1); // 无脑记录就好}
724. 寻找数组的中心下标
我这里写的有点特殊,前缀和数组sums 第一项不填0,而是 sums[i] 代表:nums[0…i] 的和
var pivotIndex = function(nums) {let sums = []; // 方便计算 sums[j] - sums[i - 1] 表示 i...j 的和let sum = 0;for (let num of nums) {sum += num;sums.push(sum);}let n = sums.length;for (let i = 0; i < n; i++) {let leftSum = sums[i - 1] || 0;let rightSum = sums[n - 1] - sums[i]if (leftSum === rightSum) {return i;}}return -1;};
560. 和为 K 的子数组 930. 和相同的二元子数组 (两个题目重复)
一开始有思维定式,就是先生成完整的 前缀和数组,然后再谋划下一步,其实,有些技术在生成前缀树的过程做是最好的:
先说下流程:
1)找子数组,下标为 [i,…,j]
2) 我们很容易就知道,子数组和 = sums[j] - sums[i] sums是前缀和数组
3) 这样我们就可以遍历一次前缀和,看 是否存在这样的组合 sums[i] 和 sums[j]
var subarraySum = function(nums, k) {let sums = []; // sums[i] 代表:nums[0...i] 的和let sum = 0;for (let num of nums) {sum += num;sums.push(sum);}let count = 0;let n = sums.length;for (let i = 0; i <= n; i++) {p_j = sums[i] + k; // j 的下标还得 比 i 大if (sums.includes(p_j)) {if (sums.indexOf(p_j) >= i) {count++;}}}return count;};
于是我们写了上面的代码,但是发现了我们不好解决的问题,不光是 sums.includes(p_j) 的问题,还要保证两个问题:1)i > j;2)还要考虑前缀和里重复的问题。这样的话,我们这套思路就需要优化了。
为保证 i > j, 其实我们可以从 构建 前缀和数组 数组思考:前缀和的定义为 sums[j] = nums[0….j-1],每次变化的是结尾 j 的值,比 j 大的我们还没有遍历到,所以我们可以保证了。
重复的问题,我们可以通过 map 的形式 也能搞定。
所以,我们再写代码:
var subarraySum = function(nums, k) { // 子数组不是子序列const n = nums.length;if (n === 1) {return nums[0] === k ? 1 : 0;}const preSums = [0];let map = new Map();map.set(0, 1);let count = 0;for (let j = 0; j < n; j++) {preSums[j + 1] = preSums[j] + nums[j];let targetLeft = preSums[j + 1] - k;if (map.has(targetLeft)) {count += map.get(targetLeft);}map.set(preSums[j + 1], (map.get(preSums[j + 1]) || 0) + 1); // 无脑记录就好}return count;};
1248. 统计「优美子数组」
和 560 一模一样哈,但是不同的是,这道题不是 求和,而是求 奇数的个数
var numberOfSubarrays = function(nums, k) {let n = nums.length;let countArr = [0];let map = new Map();map.set(0, 1); // [0]:0 个 奇数,初始有1个let count = 0;for (let i = 0; i < n; i++) {if (nums[i] % 2 === 1) { // yescountArr[i + 1] = countArr[i] + 1;} else {countArr[i + 1] = countArr[i];}let targetLeft = countArr[i + 1] - k;if (map.has(targetLeft)) {count += map.get(targetLeft);}map.set(countArr[i + 1], (map.get(countArr[i + 1]) || 0) + 1);}return count;};
974. 和可被 K 整除的子数组
一样的思路,只是在 一个数的同余时,有些问题
var subarraysDivByK = function(nums, k) {let n = nums.length;let preSums = [0];let map = new Map(); // key 为 (前缀和 % k), value 依然是 个数map.set(0, 1);let count = 0;for (let i = 0; i < n; i++) {preSums[i + 1] = preSums[i] + nums[i];let remainder = (preSums[i + 1] % k + k) % k; // 3 k !!!if (map.has(remainder)) {count += map.get(remainder);}map.set(remainder, (map.get(remainder) || 0) + 1);}return count;};
523. 连续的子数组和
还是和 974 相同的思路,但是需要调整细节,只是要求 子数组必须至少有两个数!!
var checkSubarraySum = function(nums, k) {let n = nums.length;let preSums = [0];let map = new Map(); // key 为 前缀和,value 为 下标组成的数组map.set(0, [-1]);for (let i = 0; i < n; i++) {preSums[i + 1] = preSums[i] + nums[i];let remainder = (preSums[i + 1] % k + k) % k;let indexList = map.get(remainder) || [];if (map.has(remainder)) {let has = indexList.filter((v) => (i - v >= 2)).length > 0; // 多的判断条件if (has) {return true;}}indexList.push(i);map.set(remainder, indexList);}return false;};
小结:
看到 子数组 连续 和,就要想到这样的思路
