前缀和
对于一个数组
A
A
A ,它的前缀和数组
S
S
S 是通过递推能求出的基本信息之一:
S
[
i
]
=
∑
j
=
1
i
A
[
j
]
S[i] = \sum_{j = 1}^{i}A[j]
S[i]=j=1∑iA[j]
前缀和的作用往往用于快速的求数组的一部分和,即数组
A
A
A 某个下标区间内的数的和,可以表示为前缀和相减的形式:
s
u
m
(
l
,
r
)
=
∑
i
=
l
r
A
[
i
]
=
S
[
r
]
−
S
[
l
−
1
]
sum(l, r) = \sum_{i = l} ^ {r} A[i] = S[r] - S[l - 1]
sum(l,r)=i=l∑rA[i]=S[r]−S[l−1]
在二维数组中,也可以推出类似的二维前缀和。
对于二维空间,我们可以表示成下面几张图:
阴影部分为
S
[
i
−
1
]
[
j
]
S[i - 1][j]
S[i−1][j] 。
阴影部分为 S [ i ] [ j − 1 ] S[i][j - 1] S[i][j−1] 。
阴影部分为 S [ i − 1 ] [ j ] + S [ i ] [ j − 1 ] S[i - 1][j] + S[i][j - 1] S[i−1][j]+S[i][j−1] 。
阴影部分为
S
[
i
−
1
]
[
j
]
+
S
[
i
]
[
j
−
1
]
−
S
[
i
−
1
]
[
j
−
1
]
S[i - 1][j] + S[i][j - 1]-S[i - 1][j - 1]
S[i−1][j]+S[i][j−1]−S[i−1][j−1] 。
容易的得出二维数组中,前缀和的递推式就是:
S
[
i
,
j
]
=
S
[
i
−
1
,
j
]
+
S
[
i
,
j
−
1
]
−
S
[
i
−
1
,
j
−
1
]
+
A
[
i
,
j
]
S[i,j] = S[i - 1, j] + S[i,j-1]-S[i-1,j-1] +A[i,j]
S[i,j]=S[i−1,j]+S[i,j−1]−S[i−1,j−1]+A[i,j]
同理我要计算一个子矩阵的和,已知矩阵左上角坐标
(
x
1
,
y
1
)
(x_1,y_1)
(x1,y1) ,右下角坐标为
(
x
2
,
y
2
)
(x_2,y_2)
(x2,y2) ,可得出:
∑
i
=
x
1
x
2
∑
j
=
y
1
y
2
A
[
i
]
[
j
]
=
S
[
x
2
,
y
2
]
−
S
[
x
1
]
[
y
2
]
−
S
[
x
2
]
[
y
1
]
+
S
[
x
1
]
[
y
1
]
\sum_{i=x_1}^{x_2}\sum_{j=y_1}^{y_2}A[i][j] = S[x_2,y_2]-S[x_1][y_2]-S[x_2][y_1]+S[x_1][y_1]
i=x1∑x2j=y1∑y2A[i][j]=S[x2,y2]−S[x1][y2]−S[x2][y1]+S[x1][y1]
推导式子其实使用了 “容斥原理” 的思想,利用它也可以推出三维数组的三维前缀和。
【例题】激光炸弹
一种新型炸弹,可以摧毁一个边长为
R
R
R 的正方形内所有目标。现在地图上有
N
(
N
≤
1
0
4
)
N~(N\le 10^4)
N (N≤104) 个目标,用整数
X
i
,
Y
i
X_i,Y_i
Xi,Yi (其值在区间
[
0
,
5000
]
[0,5000]
[0,5000] 之内)表示目标在地图上的位置,每个目标价值
W
i
W_i
Wi 。
激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆破范围,即那个边长为 R R R 的正方形的边必须和 x x x , y y y 轴平行。若目标位于爆破正方形的边上,该目标不会被摧毁。求一颗炸弹最多能炸掉地图上的总价值为多少的目标。
分析:
问题简述就是在一个二维平面找出一个长度为
R
R
R 的正方形,它的权值和最大。那么这个二维平面在
[
0
,
5000
]
[0, 5000]
[0,5000] 内,所有我们可以使用
O
(
n
2
)
O(n^2)
O(n2) 来预处理出一个二维前缀和数组,再用一个
O
(
n
2
)
O(n^2)
O(n2) 来遍历所有长度
R
R
R 的正方形的权值和,找出最大值就行了。
代码如下:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 5010;
int n, r;
int sa[N][N];
int main()
{
scanf("%d%d", &n, &r);
// r最大5001有意义
r = min(r, 5001);
int x, y, w;
for(int i = 0; i < n; ++i) {
scanf("%d%d%d", &x, &y, &w);
sa[x + 1][y + 1] += w; // 前缀和通常以1开头,好处理。
}
for(int i = 1; i <= 5001; ++i) {
for (int j = 1; j <= 5001; j ++ ) {
sa[i][j] += sa[i - 1][j] + sa[i][j - 1] - sa[i - 1][j - 1];
}
}
int res = 0;
for(int i = r; i <= 5001; ++i) {
for(int j = r; j <= 5001; ++j) {
res = max(res, sa[i][j] - sa[i][j - r] - sa[i - r][j] + sa[i - r][j - r]);
}
}
printf("%d", res);
return 0;
}
差分
对于一个给定的数组
A
A
A ,他的差分数列
B
B
B 定义为:
B
[
1
]
=
A
[
1
]
,
B
[
i
]
=
A
[
i
]
−
A
[
i
−
1
]
(
2
≤
i
≤
n
)
B[1] = A[1],~B[i]=A[i]-A[i - 1] ~(2\le i \le n)
B[1]=A[1], B[i]=A[i]−A[i−1] (2≤i≤n)
“差分”与“前缀和”其实是一对互逆运算,差分数组 B B B 的前缀和数组就是数组 A A A ,前缀和数组 S S S 的差分数组也是数组 A A A。
在数组 A A A 的区间 [ l , r ] [l,r] [l,r] 上加上 d d d (即把 A l , A l + 1 , ⋯ , A r A_l,A_{l+1},\cdots,A_r Al,Al+1,⋯,Ar 都加上 d d d ),其差分数组 B B B 的变化就是在 B l B_l Bl 加 d d d , B r + 1 B_r + 1 Br+1 减 d d d ,其它位置不变。
通常这个操作可以使得原本在数组 A A A 上的区间操作,变为在数组 B B B 上的单点操作。
【例题】增减序列
给定一个长度为 n ( n ≤ 1 0 5 ) n~(n\le 10^5) n (n≤105) 的数列 { a 1 , a 2 , ⋯ , a n } \{a_1,a_2,\cdots,a_n\} {a1,a2,⋯,an} ,每次可以选择一个区间 [ l , r ] [l,r] [l,r] ,使下标在这个区间内的数都加 1 1 1 或者减 1 1 1 。
求至少需要多少次操作才能使数列中的所有数都一样,并求出在保证最少次数的前提下,最终得到的数列有多少种。
分析:
如果仅仅看区间操作,那么就很难处理这个情况,但是我们要是使用差分数组来看这个数列就不一样了。
假设数列 a a a 的全部元素已经相同了,即 { a , a , ⋯ , a } \{a,a,\cdots,a\} {a,a,⋯,a} ,那么它的差分数就是 { a , 0 , ⋯ , 0 } \{a,0,\cdots,0\} {a,0,⋯,0},可以看出除了第一项为 a a a ,其它数都为 0 0 0 ,也就是我们要通过区间操作使得差分数列的 b 2 , b 3 , ⋯ , b n b_2,b_3,\cdots,b_n b2,b3,⋯,bn 变为 0 0 0 ,这时原来的区间操作就变为了差分序列的单点操作。
如果在 b 2 , b 3 , ⋯ , b n b_2,b_3,\cdots,b_n b2,b3,⋯,bn 上使用单点操作可以使用使得两个正负相反的使用一次操作抵消,所有要优先去除他的,这里花费的次数就是 m i n ( p , q ) min(p,q) min(p,q) , p p p 为在 b 2 , b 3 , ⋯ , b n b_2,b_3,\cdots,b_n b2,b3,⋯,bn 上所有正数的和, q q q 为在 b 2 , b 3 , ⋯ , b n b_2,b_3,\cdots,b_n b2,b3,⋯,bn 所有负数的绝对值和。
这样操作完了后,还剩下 ∣ p − q ∣ |p - q| ∣p−q∣ 的值需要变为 0 0 0 ,这时可以与 b 1 b_1 b1 搭配消耗掉,也可以和 b n + 1 b_{n + 1} bn+1 一起搭配消耗掉,而差分数列的 b 1 b_1 b1 有多少种取值,就代表有多少原数组 A A A 全部相同的种类数。所以 b 1 b_1 b1 的变化区间就在 [ b 1 , b 1 + ∣ p − q ∣ ] [b_1,b_1+|p-q|] [b1,b1+∣p−q∣] 这个区间内,总数为 ∣ p − q ∣ + 1 |p - q| + 1 ∣p−q∣+1 个。
代码如下:
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 5;
int n;
LL a[N], b[N];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
scanf("%lld", a + i);
for (int i = 1; i <= n; i ++ )
b[i] = a[i] - a[i - 1];
LL ans = 0, q = 0, p = 0;
for (int i = 2; i <= n; i ++ ) {
if(b[i] > 0) q += b[i];
if(b[i] < 0) p += -b[i];
}
LL res = a[1];
printf("%lld\n%lld", min(q, p) + abs(p - q), abs(p - q) + 1);
return 0;
}
【例题】最高的牛
有 N N N 头牛站成一行。两头牛能互相看见,当且仅当它们中间的牛的身高都比它们矮。现在,我们只知道其中最高的牛是第 P P P 头,它的身高是 H H H ,不知道剩余 N − 1 N - 1 N−1 头牛的身高。但是,我们还知道 M M M 对关系,每对关系都指明了两头牛 A i A_i Ai 和 B i B_i Bi 可以互相看见。求每头牛的身高最大可能是多少。
数据范围: 1 ≤ N , M ≤ 1 0 4 , 1 ≤ H ≤ 1 0 6 1\le N,M\le 10^4, ~1\le H\le10^6 1≤N,M≤104, 1≤H≤106 。
分析:
确定了最高身高 H H H 后,我们也确定了每头牛最高也是 H H H 了。最初没有任何关系的约数,就代表所以牛都可以是 H H H ,但是有了关系的约束后,设这个关系就是 a i a_i ai , b i b_i bi ( a i < b i a_i < b_i ai<bi),那么在 [ a i + 1 , b i − 1 ] [a_i +1,b_i-1] [ai+1,bi−1] 这段区间内的牛至少身高得减去 1 1 1 ,这样才不会遮蔽视野。这时区间减一的操作我们就可以利用差分来变为单点操作了,而原本全为 H H H 的数组,它的差分数组也变为了 [ H , 0 , ⋯ , 0 ] [H,0,\cdots,0] [H,0,⋯,0] 这样的。最后跑一个前缀和就可以求出满足条件的最大牛身高了。
因为关系可能重复,而我们做了一次后就不必再做一次,所以要去重操作。
代码如下:
#include <iostream>
#include <algorithm>
#include <map>
using namespace std;
typedef pair<int, int> PII;
const int N = 10010;
int b[N];
map<PII, bool> mp;
int main()
{
int n, p, h, m;
scanf("%d%d%d%d", &n, &p, &h, &m);
for(int i = 1; i <= n; ++i)
b[i] = 0;
b[1] = h;
while (m -- ) {
int u, v;
scanf("%d%d", &u, &v);
if(u > v) swap(u, v);
PII t = {u, v};
if(mp.count(t)) continue;
b[u + 1] -=1;
b[v] += 1;
mp[t] = true;
}
for (int i = 1; i <= n; i ++ )
b[i] += b[i - 1];
for (int i = 1; i <= n; i ++ ) {
printf("%d\n", b[i]);
}
return 0;
}