1. 前缀和
前缀和可以简单理解为「数列的前 n 项的和」,是一种重要的预处理方式,能大大降低查询的时间复杂度。
二维/多维前缀和:
常见的多维前缀和的求解方法有两种。
基于容斥原理:
这种方法多用于二维前缀和的情形。给定大小为
m
×
n
m\times n
m×n 的二维数组 A,要求出其前缀和 S。那么,S 同样是大小为
m
×
n
m\times n
m×n 的二维数组,且
S i , j = ∑ i ′ ≤ i ∑ j ′ ≤ j A i ′ , j ′ S_{i,j} = \sum_{i'\le i}\sum_{j'\le j}A_{i',j'} Si,j=∑i′≤i∑j′≤jAi′,j′
类比一维的情形, S i , j S_{i,j} Si,j 应该可以基于 S i − 1 , j S_{i-1,j} Si−1,j 或 S i , j − 1 S_{i,j-1} Si,j−1 计算,从而避免重复计算前面若干项的和。但是,如果直接将 S i − 1 , j S_{i-1,j} Si−1,j 和 S i , j − 1 S_{i,j-1} Si,j−1 相加,再加上 A i , j A_{i,j} Ai,j,会导致重复计算 S i − 1 , j − 1 S_{i-1,j-1} Si−1,j−1 这一重叠部分的前缀和,所以还需要再将这部分减掉。这就是 容斥原理。由此得到如下递推关系:

实现时,直接遍历 ( i , j ) (i,j) (i,j) 求和即可。

同样的道理,在已经预处理出二位前缀和后,要查询左上角为
(
i
1
,
j
1
)
(i_1,j_1)
(i1,j1)、右下角为
(
i
2
,
j
2
)
(i_2,j_2)
(i2,j2) 的子矩阵的和,可以计算

这可以在 O(1) 时间内完成。
在二维的情形,以上算法的时间复杂度可以简单认为是 O(mn),即与给定数组的大小成线性关系。但是,当维度 k 增大时,由于容斥原理涉及的项数以指数级的速度增长,时间复杂度会成为 O ( 2 k N ) O(2^kN) O(2kN),这里 k k k 是数组维度,而 N N N 是给定数组大小。因此,该算法不再适用。
逐维前缀和:
对于一般的情形,给定 k k k 维数组 A A A,大小为 N N N,同样要求得其前缀和 S S S。这里,

从上式可以看出,
k
k
k 维前缀和就等于
k
k
k 次求和。所以,一个显然的算法是,每次只考虑一个维度,固定所有其它维度,然后求若干个一维前缀和,这样对所有
k
k
k 个维度分别求和之后,得到的就是
k
k
k 维前缀和。
三维前缀和的参考实现:
N1, N2, N3 = map(int,input().split())
a = [[[0 for _ in range(N3+1)] for _ in range(N2+1)] for _ in range(N1+1)]
for i in range(1, N1+1):
for j in range(1, N2+1):
for k in range(1, N3+1):
a[i][j][k] = int(input())
ps = [list(map(list, x)) for x in a]
for i in range(1, N1+1):
for j in range(1, N2+1):
for k in range(1, N3+1):
ps[i][j][k] += ps[i][j][k-1]
for i in range(1, N1+1):
for j in range(1, N2+1):
for k in range(1, N3+1):
ps[i][j][k] += ps[i][j-1][k]
for i in range(1, N1+1):
for j in range(1, N2+1):
for k in range(1, N3+1):
ps[i][j][k] += ps[i-1][j][k]
for i in range(1, N1+1):
for j in range(1, N2+1):
for k in range(1, N3+1):
print(ps[i][j][k], end=' ')
print()
print()
2. 差分
一维差分:
差分思想和前缀和是相反的。
首先我们先定义数组a, 其中a[1], a[2] … a[n]作为前缀和。
然后构造数组b,b[1], b[2] … b[n]为差分数组。其中通过差分数组的前缀和来表示a数组,即a[n] = b[1] + b[2]+…+b[n]。
一维差分数组的构造也很简单,即a[1] = b[1], b[2] = a[2] - a[1], b[n] = a[n] - a[n-1];
在做初始化的时候现将a,b全部初始化为0,然后可以按照下面的方式流式进行:
//eg:对于b[1]:
b[1] = 0 + a[1]
b[2] = 0 - a[1] # 最终:b[1] = a[1]
//对于b[2]:
b[2] = b[2] + a[2] # ==> 最终:b[2] = a[2] - a[1]
b[3] = b[3] - a[2]
差分数组的好处是可以简化运算,例如想要给一个区间 [l,r] 上的数组加一个常数c,原始的方法是依次加上c,这样的时间复杂度是O(n)的。但是如果采用差分数组的话,可以大大降低时间复杂度到O(1)。
由于a[n] = b[1] + b[2]+…+b[n],因此只需要将b[l] = b[l] + c 即可,这样l之后的数字会依次加上常数c,而在 b[r]处,将b[r+1] = b[r+1] - c ,这样r之后的数组又会恢复原值,仅需要处理这两个边界的差分数组即可,时间复杂度大大降低。

这里不是说真的变成O(1)了,而是说如果多次给一个序列上的某些区间上的数组加一个常数,可以先在这个序列的差分数组上先给相应位置l和r+1进行操作,多次操作后再对整个差分数组求前缀和就可以得到结果了。比如给元素组的一些区间加了m次常数,原数组的长度为n,则相比在元素组上操作的时间复杂度为m*n,在它的差分数组上操作的时间复杂度为n。
例题:差分
输入一个长度为 n 的整数序列。
接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。
请你输出进行完所有操作后的序列。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数序列。
接下来 m 行,每行包含三个整数 l,r,c表示一个操作。
输出格式
共一行,包含 n 个整数,表示最终序列。
数据范围
1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤1000,
−1000≤整数序列中元素的值≤1000
N = 100010
m,n = 0,0
a = [0]*N
b = [0]*N
def insert(l, r , c):
b[l] += c
b[r+1] -= c
n, m = map(int,input().split())
a = list(map(int,input().split()))
for i in range(1, n+1):
insert(i, i, a[i-1])
for _ in range(m):
l, r ,c = map(int,input().split())
insert(l, r, c)
for i in range(1, n+1):
b[i] += b[i - 1]
for i in range(1, n+1):
print(b[i], end=" ")

被折叠的 条评论
为什么被折叠?



