欢迎来到我的频道[【点击跳转专栏】]
作者说:我想说 基础 不等于 简单 ;算法能力不是一蹴而就的,而是来自日积月累的积累和练习!积小流终成江海,诸君 加油!!
文章目录
- 1.前缀和
- [1.1 一维前缀和](#1.1 一维前缀和)
- [1.2 最⼤⼦段和](#1.2 最⼤⼦段和)
- [1.3 二维前缀和](#1.3 二维前缀和)
- [1.4 激光炸弹](#1.4 激光炸弹)
- [2. 差分](#2. 差分)
- [2.1 一维差分](#2.1 一维差分)
- [2.2 海底高铁](#2.2 海底高铁)
- [2.3 二维差分(超级难感觉~)](#2.3 二维差分(超级难感觉~))
- [2.4 地毯](#2.4 地毯)
1.前缀和
前缀和与差分的核心思想是预处理,可以在暴力枚举的过程中,快速给出查询的结果,从而优化时间复杂度。
是经典的用空间替换时间的做法。
1.1 一维前缀和
https://ac.nowcoder.com/acm/problem/226282

前缀和模板题,直接套用「公式」创建前缀和数组,然后利用前缀和数组的「性质」处理 q q q 次查询。
- 创建前缀和数组: f [ i ] = f [ i − 1 ] + a [ i ] f[i] = f[i - 1] + a[i] f[i]=f[i−1]+a[i]
- 查询 [l, r] 区间和: f[r] − f[l − 1]
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
int l,r,n,m;
LL a[N],t[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
t[i]=t[i-1]+a[i];
}
while(m--)
{
cin>>l>>r;
cout<<t[r]-t[l-1]<<endl;
}
}
1.2 最⼤⼦段和
https://www.luogu.com.cn/problem/P1115

考虑以 (i) 位置的元素 (a[i])「为结尾」的最大子段和:
- 在求「区间和」时,相当于是用 (f[i]) 减去 (i) 位置前面的某一个 (f[x]);(
f[x]为最小的值!)- 如果想要「最大子段和」,也就是「最大区间和」,那么用 f[i] 减掉一个「前驱最小值」即可。
因此,我们可以创建 a数组的「前缀和」数组,然后在遍历前缀和数组的过程中,一边「更新前驱最小值」,一边「更新当前位置为结尾的最大子段和」。
通俗的说 就是:
用f[i]减去[1,i-1]1中所有前缀和的最小值即可!
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int a[N],t[N];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
t[i]=t[i-1]+a[i];
}
int ret = -0x3f3f3f3f;
int prevmin=0;//前面的最小值
for(int i=1;i<=n;i++)
{
ret=max(ret,t[i]-prevmin);
prevmin=min(prevmin,t[i]);
}
cout<<ret;
}
1.3 二维前缀和
https://ac.nowcoder.com/acm/problem/226333
二维前缀和模板题,直接套用「公式」创建前缀和矩阵,然后利用前缀和矩阵的「性质」处理 (q) 次询问。
- 创建前缀和矩阵:
f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + a[i][j]
- 查询以
(x1 , y1 )为左上⻆ ,(x2 , y2 )为右下⻆的⼦矩阵的和
⚠️:做这种题目,没必要死记公式 画一画就可以了
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e3+10;
LL a[N][N],t[N][N];
int m,n, q;
int main()
{
cin>>n>>m>>q;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>a[i][j];
t[i][j]=t[i-1][j]+t[i][j-1]-t[i-1][j-1]+a[i][j];
}
}
LL ret;
while(q--)
{
int x1,x2,y1,y2;
cin>>x1>>y1>>x2>>y2;
ret=t[x2][y2]-t[x1-1][y2]-t[x2][y1-1]+t[x1-1][y1-1];
cout<<ret<<endl;
}
}
1.4 激光炸弹
https://www.luogu.com.cn/problem/P2280
- 在坐标系中,枚举出所有变长为m的正方形,然后找出正方形中目标价值最大的即可!
如何枚举边⻓为 R 的所有正⽅形:仅需枚举右下⻆
(x2,y2)即可 ,那么结合边⻓x2-x1+1=m就可算出左上⻆的坐标(x2-m+1,y2-m+1)。然后利用
二维前缀和求出最大值即可!
⚠️:
- 题目中某一个位置会「重复」出现,因此 (a[i][j] += w);
- 半径 (R) 可能「大于等于5001」,此时炸弹可以摧毁所有目标,也就是整个矩阵的目标价值之和。
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=5010;
int a[N][N],t[N][N];
int n,m;
int main()
{
cin>>n>>m;
while(n--)
{
int x,y,v;
cin>>x>>y>>v;
x++,y++;
a[x][y]+=v;
}
n=5001;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
t[i][j]=t[i-1][j]+t[i][j-1]-t[i-1][j-1]+a[i][j];
}
}
int ret=0;
//如果m>=5001 相当于全部炸毁!
m=min(n,m);
//枚举
for(int i=m;i<=n;i++)
{
for(int j=m;j<=n;j++)
{
int x=i-m+1,y=j-m+1;
ret=max(ret,t[i][j]-t[i][y-1]-t[x-1][j]+t[x-1][y-1]);
}
}
cout<<ret;
}
2. 差分
前缀和与差分的核心思想是预处理,可以在暴力枚举的过程中,快速给出查询的结果,从而优化时间复杂度。是经典的用空间替换时间的做法。
学完差分之后会发现,前缀和与差分是一对互逆的运算。
2.1 一维差分
https://ac.nowcoder.com/acm/problem/226303

差分有什么作用?快速解决"将某一个区间所有元素统一加上或减去一个数"的操作!(
O(1))
差分模板题,先「创建」差分数组,然后根据差分数组的「性质」处理 q 次区间修改,最后「还原」出来原始的数组。
- 创建差分数组,根据定义:
(f[i] = a[i] - a[i - 1])(f[i]表示当前元素与前一个元素的差值!)
- 利用差分数组解决m次修改操作(
难点)性质:
原数组[L,R]区间加上 k 的这个操作,相当于差分数组中,f[L]+=k,f[R+1]-=k!
- 性质的证明!
数组的还原(
重点!)
可以用这段代码://还原出原始的数组
for(int i = 1; i <= n; i++)
{
f[i] = f[i - 1] + f[i];
cout << f[i] << " ";
}初始状态(差分数组) : 在执行循环之前,数组
f存储的是差分值。
- 假设原始数组为 A A A,差分数组为 D D D。
- 此时
f[i]存储的是 D i = A i − A i − 1 D_i = A_i - A_{i-1} Di=Ai−Ai−1。还原过程(前缀和) : 循环中的核心语句是
f[i] = f[i - 1] + f[i];。
- 这一步利用了差分的逆运算------前缀和。
- 数学原理: A i = ∑ k = 1 i D k = A i − 1 + D i A_i = \sum_{k=1}^{i} D_k = A_{i-1} + D_i Ai=∑k=1iDk=Ai−1+Di。
- 代码中,
f[i-1]已经是还原后的 A i − 1 A_{i-1} Ai−1,而等号右边的f[i]还是差分值 D i D_i Di。两者相加,就得到了原始值 A i A_i Ai,并覆盖回f[i]。输出 :
cout << f[i] << " ";输出的是还原后的原始数组元素。 📝 示例: 假设原始数组是[1, 2, 3, 4, 5]。差分数组
f初始化为:[1, 1, 1, 1, 1](即 2 − 1 , 3 − 2 , ... 2-1, 3-2, \dots 2−1,3−2,...,首项为1)。执行代码:
i=1:f[1] = f[0] + f[1] = 0 + 1 = 1i=2:f[2] = f[1] + f[2] = 1 + 1 = 2i=3:f[3] = f[2] + f[3] = 2 + 1 = 3- ...
结果 :
f变回了[1, 2, 3, 4, 5]。
补充:其实也可以直接根据差分数组的性质进行创建差分数组(
更常用!因为这样的好处就是直接不需要创建原始数组了!)
f[i] + = a[i], f[i + 1] − = a[i]相当于每次读入一个
a[i] 此时f[i]就会加一个a[i] 而f[i+1]此时就减去一个a[i]
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
LL f[N];
int n,m,l,r,k;
int main()
{
cin>>n>>m;
//创建差分数组
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
f[i]+=x;
f[i+1]-=x;
}
while(m--)
{
cin>>l>>r>>k;
f[l]+=k;
f[r+1]-=k;
}
for(int i=1;i<=n;i++)
{
f[i]=f[i-1]+f[i];
cout<<f[i]<<" ";
}
}
2.2 海底高铁
https://www.luogu.com.cn/problem/P3406
其实这道题的难点 我个人认为最大的就是读懂题目! 其次才是解法!
- 先考虑如何让花费最小,想要求最小花费,需要知道每一段高铁被「乘坐了多少次」,记作 f [ i ] f[i] f[i],那么最小花费就是「买票的花费」与「买卡的花费」两者之间的最小值:
- 买票花费: a [ i ] × f [ i ] a[i] \times f[i] a[i]×f[i];
- 买卡花费,乘车花费 + 工本费: b [ i ] × f [ i ] + c [ i ] b[i] \times f[i] + c[i] b[i]×f[i]+c[i];
- 那么最小花费就是: m i n c o s t = m i n ( a [ i ] × f [ i ] , b [ i ] × f [ i ] + c [ i ] ) mincost = min(a[i] \times f[i], b[i] \times f[i] + c[i]) mincost=min(a[i]×f[i],b[i]×f[i]+c[i])
2 . 接下来考虑如何求出每一段高铁被「乘坐了多少次」。根据访问城市的序列 p 1 , p 2 , p 3 , ... , p m p_1, p_2, p_3, \dots, p_m p1,p2,p3,...,pm
可知,对于任意一次访问 p i ∼ p i + 1 p_i \sim p_{i+1} pi∼pi+1,我们会乘坐 [ p i , p i + 1 − 1 ] [p_i, p_{i+1} - 1] [pi,pi+1−1] 之间所有的高铁,比如:
p i = 3 , p i + 1 = 6 p_i = 3, p_{i+1} = 6 pi=3,pi+1=6,那么 [ 3 , 5 ] [3, 5] [3,5] 之间所有的高铁都会被乘坐一次,相当于每个数都加上 1,「注意 6位置不会乘坐到」。那么我们就可以利用「差分数组」:
- 创建一个全为 0 的差分数组 f f f;
- 遍历访问序列,对于每一次访问: f [ p i ] + + , f [ p i + 1 ] − − f[p_i] ++, f[p_{i+1}] -- f[pi]++,f[pi+1]−−;
- 然后对差分数组做一次前缀和,就得到每个高铁乘坐的次数
注意城市访问的序列有可能 p i > p i + 1 p_i > p_{i+1} pi>pi+1,此时应该「交换」一下顺序。
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
LL f[N];
LL n,m,a,b,c;
int main()
{
cin>>n>>m;
int x;
cin>>x;
for(int i=2;i<=m;i++)
{
int y;
cin>>y;
if(x<y)
{
f[x]++;
f[y]--;
}
else
{
f[y]++;
f[x]--;
}
x=y;
}
//还原差分数组
for(int i=1;i<n;i++)
{
f[i]+=f[i-1];
}
LL ret=0;
for(int i=1;i<n;i++)
{
cin>>a>>b>>c;
LL tmp=a*f[i];
tmp=min(tmp,c+b*f[i]);
ret+=tmp;
}
cout<<ret;
}
2.3 二维差分(超级难感觉~)
https://ac.nowcoder.com/acm/problem/226337
这道题目的要求就是需要我们快速处理二维数组中,让某一个子矩阵加上或者减去一个元素的操作!可以类⽐「⼀维差分数组」的性质,推导出「⼆维差分矩阵」的性质:
- 在差分数组中某个位置标记:表⽰后续元素统⼀被修改;
- 在差分数组中求前缀和:能够还原出原始数组。
即:s[i][j] = s[i-1][j] + s[i][j-1] -s[i-1][j-1] + f[i][j]
假设我们需要将原始矩阵 a a a 中,以 ( x 1 , y 1 ) (x_1, y_1) (x1,y1) 为左上角, ( x 2 , y 2 ) (x_2, y_2) (x2,y2) 为右下角的子矩阵的每个元素都加上 k k k:
具体操作如下:
标记增量 :
f[x1][y1] += kf[x1][y2+1] -= kf[x2+1][y1] -= kf[x2+1][y2+1] += k
为什么是这 4 个点? 想象一下,当你对二维数组做前缀和
(即
s[i][j] = s[i-1][j] + s[i][j-1] -s[i-1][j-1] + f[i][j])[这块不懂的去看二维前缀和部分!]时,每一个点f[i][j]的值会向右下方无限传递。
- 操作 1
f[x1][y1] += k:
相当于在 ( x 1 , y 1 ) (x_1, y_1) (x1,y1) 点了一个灯。做前缀和后,这个 k k k 会传递给整个右下角区域 (从 x 1 x_1 x1 到 n n n,从 y 1 y_1 y1 到 n n n 的所有格子都会 + k +k +k)。
- 结果:影响范围太大了,不仅覆盖了目标矩形,还覆盖了右边和下边。
- 操作 2
f[x1][y2+1] -= k和 操作 3diff[x2+1][y1] -= k:
为了把影响范围限制住,我们需要把多出来的部分减掉。
- 在 y 2 + 1 y_2+1 y2+1 处减 k k k,是为了抵消掉目标矩形右侧 多出来的 + k +k +k。
- 在 x 2 + 1 x_2+1 x2+1 处减 k k k,是为了抵消掉目标矩形下方 多出来的 + k +k +k。
- 操作 4
diff[x2+1][y2+1] += k:
注意!当你执行操作 2 和 3 时,右下角那块区域( x > x 2 x > x_2 x>x2 且 y > y 2 y > y_2 y>y2)被减了两次 (因为它既在右侧,又在下方)。
根据容斥原理,多减了一次,所以要加回来一次。
- 图解还原过程
假设我们有一个 5 × 5 5 \times 5 5×5 的矩阵,目标是将中间红色区域( 2 , 2 2,2 2,2 到 4 , 4 4,4 4,4)加 k k k。
第一步:标记差分数组
1 2 3 4 5 1 . . . . . 2 . +k . . -k 3 . . . . . 4 . . . . . 5 . -k . . +k第二步:做前缀和(累加影响)
- 遇到
+k(左上角) :
前缀和开始累加,从这个点开始,右下方的所有格子都受到了+k的影响。
- 此时整个右下大区域都是 + k +k +k。
- 遇到
-k(左下角) :
前缀和遇到了-k,抵消了之前的+k。
- 此时,目标区域
右侧的格子变成了 k − k = 0 k - k = 0 k−k=0。影响被切断。- 遇到
-k(右上角) :
同理,前缀和遇到了-k,抵消了之前的+k。
- 此时,目标区域
下方的格子变成了 k − k = 0 k - k = 0 k−k=0。影响被切断。- 遇到
+k(右下角) :
这个位置原本因为左上角的+k变成了 k k k,又因为上方和左边的两个-k变成了 k − k − k = − k k - k - k = -k k−k−k=−k。
最后加上这个位置的+k,变成了 − k + k = 0 -k + k = 0 −k+k=0。
- 完美归零!
总结
差分矩阵做前缀和之所以能还原,是因为:
+k开启了一个向右下无限延伸的"增益场"。- 两个
-k分别切断了向右和向下的延伸。- 最后一个
+k修复了被重复切断的角落。最终,只有目标子矩阵区域保留了
+k,其余区域通过正负抵消归零。
数组的还原:
f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + f[i][j];
- 读取旧值 :
f[i][j]在这一刻还仅仅是差分数组在该点的值(增量)。- 读取邻居 :
f[i-1][j]和f[i][j-1]已经被更新过了,它们代表了上方和左方已经计算好的前缀和。- 计算并覆盖:算出新的前缀和,覆盖掉原来的差分值。
⚠️:这个规则不要背,需要的时候画一下推导就行!
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1010;
LL f[N][N];
int main()
{
int n,m,q;
cin>>n>>m>>q;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
int k;
cin>>k;
//把图上性质的(x1,y1) ~ (x2,y2) 换成(i,j)~ (i,j)
f[i][j]+=k;
f[i+1][j]-=k;
f[i][j+1]-=k;
f[i+1][j+1]+=k;
}
}
// 处理 q 次修改操作
while(q--)
{
int x1,x2,y1,y2,k;
cin>>x1>>y1>>x2>>y2>>k;
f[x1][y1]+=k;
f[x1][y2+1]-=k;
f[x2+1][y1]-=k;
f[x2+1][y2+1]+=k;
}
//还原数组 求前缀和!
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
f[i][j]=f[i-1][j]+f[i][j-1]-f[i-1][j-1]+f[i][j];
cout<<f[i][j]<<" ";
}
cout<<endl;
}
}
2.4 地毯
https://www.luogu.com.cn/problem/P3397
练手题 如果你二维差分弄明白了 这道题有手就行!
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int f[N][N];
int n,m;
int main()
{
cin>>n>>m;
while(m--)
{
int x1,x2,y1,y2;
cin>>x1>>y1>>x2>>y2;
f[x1][y1]++;
f[x1][y2+1]--;
f[x2+1][y1]--;
f[x2+1][y2+1]++;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
f[i][j]=f[i-1][j]+f[i][j-1]-f[i-1][j-1]+f[i][j];
cout<<f[i][j]<<" ";
}
cout<<endl;
}
}



















