首先要感谢labuladong老师, 【labuladong】前缀和/差分数组技巧精讲, 在2025/05/21做每日一题3356. 零数组变换 II的时候, 我之前拙劣的 O ( n 2 ) O(n^2) O(n2)甚至更高的复杂度算法始终也过不了一个大用例, 答案中给出的方法用到了二分查找
和差分数组
, 二分查找
很好理解, 这个是之前掌握的, 但是差分数组
是个什么东西? 为什么只需要简单的在左加右减就可以实现区间的全加
呢? 这是什么黑科技? 于是乎我决定去批站上搜索一下, 在刚才提到的视频里找到了想要的答案. 找到答案以后也希望分享一下给其他跟我一样的小白, 很多视频都得靠自己悟, 多一些学习笔记, 可以少一些弯路.
示例采用的是java
写作, 但其实c
系列选手也可以很轻松的看明白, 因为for
循环都是差不多的, 因此只提供该语言示例(本身语言部分也不多).
急着看算法的可以直接跳到3. 和4.
3356. 零数组变换 II 这个题目就不赘述了, 感兴趣的小伙伴们可以自己去做一下, 我们截取刚才提到的答案中最关键(二分查找
部分不是重点, 因此忽略)的一部分:
private boolean check(int[] nums, int[][] queries, int k) {
int n = nums.length;
long[] diff = new long[n + 1];
// 构造差分
for (int i = 0; i < k; i++) {
int l = queries[i][0];
int r = queries[i][1];
int x = queries[i][2];
diff[l] += x;
if (r + 1 < n) diff[r + 1] -= x;
}
// 扫描前缀和并比对
long sum = 0;
for (int i = 0; i < n; i++) {
sum += diff[i];
if (sum < nums[i]) {
return false;
}
}
return true;
}
题目要求说是执行k
次queries
里的操作以后, 可以让整个nums
变成零数组, 求最小的k
值, 如果做不到的话返回-1
. 像我一样的菜鸡肯定会好奇, 这个差分数组
是干嘛的呀?是个啥? 下面扫描前缀和
凭什么可以保障他可以让k
最小? 先卖个关子, 我们带着这两个问题往下看.
上过高中的朋友们肯定知道等比数列
, 等差数列
, 其中有一个很关键的拿分点:数列求和
和他的变型, 我们经常可以拿到一些题型, 形如 S n = a n + a n + 1 S_n = a_n + a_{n+1} Sn=an+an+1之类的怪题, 折磨的我们痛不欲生, 在编程的世界里面, 很快他们又要折磨我们一遍.
前缀和
很简单大家就能想到, 无非就是把前n
个数组里的东西加起来得到 S n S_n Sn嘛, 这样就很方便的求出来 a n a_n an元素了, 我反正之前是这样想的, 然后并不知道他有什么用处, 看完了视频, 才发现他很容易应用于 a l e f t a_{left} aleft到 a r i g h t a_{right} aright之间的总和, 特别是结合滑动窗口
和频繁的要找 a l e f t a_{left} aleft到 a r i g h t a_{right} aright之间区间和之类的题目, 如刚刚提到的那题3356. 零数组变换 II
.
简单说一下就是 S n = a 0 + a 1 + . . . + a n S_n = a_0+a_1+...+a_n Sn=a0+a1+...+an, S 0 = a 0 S_0 = a_0 S0=a0, 放到代码里的话稍微有点变化, 如下图
S 0 = 0 S_0 = 0 S0=0, S 1 = 0 + a 0 S_1 = 0 + a_0 S1=0+a0, S 2 = 0 + a 0 + a 1 S_2 = 0 + a_0 + a_1 S2=0+a0+a1, S n = 0 + a 0 + a 1 + a 2 . . . + a n − 1 S_n = 0 + a_0 + a_1 + a_2 ... + a_{n-1} Sn=0+a0+a1+a2...+an−1
这样可以用于很方便的求某个分数区间有多少人之类的题目
这里主要分享的是差分数组, 前缀和
这里就先稍微展开一下不细讲
差分数组
是前缀和
的变型, 刚才我们知道了前缀和
是什么, 既然可以每个都加和, 那我们自然也可以每个都做差, 通过初始值
和每一段的差值
, 我们可以计算出每一个数组中的元素. 啥是差分数组
呢? 就是第一个元素照抄原数组 a 0 ′ = a 0 a_0^{\prime} = a_0 a0′=a0, 然后其他的元素等于 a n ′ = a n − a n − 1 a_n^{\prime} = a_n-a_{n-1} an′=an−an−1. ( a ′ a\prime a′代表新数组, a a a代表原数组)
举个例子:
先填入8
, 然后填入 2 − 8 = − 6 2-8=-6 2−8=−6, 再填入-4
, 以此类推.
怎么反推原数组呢?
很简单.
先把8
拿出来, 然后8+(-2)=2
填到第二个, 然后再填入2+4=6
填到第三个, 以此类推.
我们可以利用差分数组
很方便的在一个区间里面全部加x
, x
为任意实数.
举个例子, 我希望在刚才的数组中的第1个
到第3个
(8
为第0个
元素)内都加1
, 先看下正常要怎么搞:
第1个
到第3个
元素, 逐个加1
, 显而易见需要一个循环
, 复杂度为 O ( n ) O(n) O(n)代码为
for(int i = 1; i <= 3; i++) {
nums[i] += 1;
}
泛化提炼一下就是
for(int i = left; i <= right; i++) {
nums[i] += x;
}
如果我要用差分数组呢?
第1个
元素加1
, 在第4个
元素减1
.代码为diff[1] += 1;
和diff[4] -=1;
泛化提炼一下就是diff[left] += x;
和diff[right+1] -=x;
我只需要操作2
个地方, 复杂度显而易见是常数 O ( k ) O(k) O(k)
复杂度降了这么多! 但是复杂度
不会凭空消失, 他只是转移到计算第n个
元素上的值了而已, 亦或是另外开辟了内存, 使用了更多的空间复杂度
而已., 但不管怎么样, 如果批量修改区间数组元素的话, 节省的复杂度
远远超过增加的复杂度
然后肯定会有很多小伙伴们会问:
1. 为什么是这样子呢?
2. 你的数学证明在哪里?
3. 我不信你这个可以这么简单!
4. 还有你为什么要在右边加1?
5. 你为什么不给左边所有元素都加1?
6. 我理解不了啊!
我们来举例讲解一下为什么可以得出这样的结论:
假设我们在第1个
差分数组
的元素加了1, 那么我们可以发现第1个
还原出来的数组(也就是通过差分数组的前缀和计算出来的数组)
元素和第1个
还原出来的数组
的元素后面的全部元素都被加了1!
这很好理解吧, 因为我只拉大了还原出来的数组
第1个
元素和还原出来的数组
第2个
元素之间的差距, 后面的还原出来的数组
的元素计算都是基于还原出来的数组
第2个
元素来计算的, 因此第1个
还原出来的数组
元素之后的所有还原出来的数组
的元素都加了1.
示例
8+(-5)(原先是-6) = 3
3+4 = 7
7+(-3) = 4
4+(-2)=2
可以发现确实如此. 得到83742
再次注意, 这个时候没有减去右边的1, 从第一个元素开始, 数组整体都被加了1
然后为什么我们要在后面减1
呢? 因为我们不希望区间外的元素也被增加!
8+(-5)(原先是-6) = 3
3+4 = 7
7+(-3) = 4
4+(-3)(原先是-2, 被我们减回来了1)=1(符合原数组的`第4个`元素)
得到83741, 符合我们的预期.
通过这个例子可以发现, 我们想要对区间进行集体加和
的操作的话, 就在left
下标加x
, 在right+1
的下标减去x
.
可能还是有小伙伴不明白, 时间为什么节省了, 你可以自己尝试一下, 如果要对各种区间
内进行总共10000次
操作, 用循环和这个方法进行对比一下, 或者可以带入3356. 零数组变换 II中的queries
来看看, 很明显如果用循环的话至少是 O ( n 2 ) O(n^2) O(n2)以上, 如果用差分数组
的常数加法的话只需要 O ( n ) O(n) O(n)就可以加完了.
我们通过刚才的例子还可以发现, 差分数组
本身的前缀和
就可以得到原数组本身! 这也是为什么我说差分数组
是前缀和
的变种.
知道了原理, 那么, 有什么用呢?
对于刚才讲到的那道题3356. 零数组变换 II, 我们可以发现, 我们非常需要频繁的
对区间元素进行操作, 如果我们有了前缀和
和差分数组
的话呢?
我们在二分查找
之后, 可以利用直接提供的queries
来组成我们的差分数组
, 什么意思呢?
就是说把所有的queries
元素都进行加和
来得到我们的差分数组
, 用于计算第i个
元素能减去的最大值. 结合我们刚才所说的知识, 可以很容易理解这个操作. 为什么不用nums[0]
呢? 因为不需要, 我们只需要使用queries
就可以了, 因为我们要求的是纯净的对应元素的扣减最大值.nums[i]
需要放到后面去比较, 在这没用.
for (int i = 0; i < k; i++) {
int l = queries[i][0];
int r = queries[i][1];
int x = queries[i][2];
diff[l] += x;
if (r + 1 < n) diff[r + 1] -= x;
}
然后, 我们可以一边求这个差分数组
的前缀和
, (这里差分数组
的前缀和求的是第i个nums[i]
元素理论扣减的最大值, 而不是nums[i]
, 不是一个东西, 不要混淆), 一边判断和原数组
的第i个
元素谁更大.为什么可以这么干呢? 因为差分数组
求的是当前这个数我们可以减去的最大数. 什么意思呢? 比如有queries[0]
和queries[1]
都包含了第0个
元素的话, 如[0, 2, 2]
和[0, 2, 2]
, 那么在第0个
元素我们得到了2+2=4
, 如果这个数大于nums[0]
的话, 证明我们可以完全减去nums[0]
, 这个元素就可以跳过, 来比较下一个元素. 否则的话, 针对于这次进来的k
, 第i个
元素的nums[0]
的值就没法抵扣, 和题目发生冲突了, 无法实现目标. 为啥? 因为在当前k
情况下, 我们全部叠加完了啊, 针对于这个元素没有其他更多的扣减值了, 用完了你都不行, 那肯定得继续搜索下一个k
来看能不能满足了. 如果都不行, 那就返回-1
了.
long sum = 0;
for (int i = 0; i < n; i++) {
sum += diff[i];
if (sum < nums[i]) {
return false;
}
}
打了半天字, 用最蠢的语言写下了这篇分享, 希望可以帮助到大家. 如果觉得有帮助的话随意转载, 欢迎转载, 如果觉得有问题的话可以留言指出, 感谢大家