前缀和&差分

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=iijjAi,j

类比一维的情形, S i , j S_{i,j} Si,j 应该可以基于 S i − 1 , j S_{i-1,j} Si1,j S i , j − 1 S_{i,j-1} Si,j1 计算,从而避免重复计算前面若干项的和。但是,如果直接将 S i − 1 , j S_{i-1,j} Si1,j S i , j − 1 S_{i,j-1} Si,j1 相加,再加上 A i , j A_{i,j} Ai,j,会导致重复计算 S i − 1 , j − 1 S_{i-1,j-1} Si1,j1 这一重叠部分的前缀和,所以还需要再将这部分减掉。这就是 容斥原理。由此得到如下递推关系:

在这里插入图片描述

实现时,直接遍历 ( 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=" ")

参考1:差分算法及模板详解

参考2:前缀和 & 差分

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

comli_cn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值