前缀和、差分与树状数组

前缀和

什么是前缀和?
前缀和数组S[i]就是给定数组a[N]从1到i项的和。
即S[i]=a[1]+a[2]+…+a[i]
这是从一维的角度考虑的,二维的思路差不多,只不过原元素序列变成了一个元素矩阵,S就是求一块矩形区域内所有数的和。
注意事项:
由于一维前缀和的递推公式是:S[i]=S[i-1]+a[i]
我们注意到下标是从i-1开始的,所以建议无论是原数组还是前缀和数组,无论是一维还是二维,最好下标都从1开始,S[0]和a[0]都置为0,这样不容易出现下标对应上的错误。
时间优势:
通常来说,我们要计算某段区间/某一矩形区域的和的话,暴力的写法会让时间复杂度达到O(n) / O(n2),但是如果进行前缀和预处理的话,当我们求某一区间或者某一区域的和时,时间复杂度就会降低到O(1)。
所以前缀和的作用就是:快速求出元素组中某段区间的和/快速求出元素矩阵中某块矩形区域的和

一维前缀和

求解[l,r]区间和的步骤:
1.for循环求出前缀和数组S,记得S[0]=0,下标从1开始
2.ans=S[r]-S[l-1]
例题:
acwing.795.前缀和
题目描述:
输入一个长度为 n 的整数序列。
接下来再输入 m 个询问,每个询问输入一对 l,r。
对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。
输入格式:
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。
输出格式:
共 m 行,每行输出一个询问的结果。
数据范围:
1≤l≤r≤n ,
1≤n,m≤100000,
−1000≤数列中元素的值≤1000
代码实现:

n,m=map(int,input().split())
a=[0]+list(map(int,input().split()))
S=[0 for i in range(n+1)]
for i in range(1,n+1):
    S[i]=S[i-1]+a[i]
for i in range(m):
    l,r=map(int,input().split())
    ans=S[r]-S[l-1]
    print(ans)

原题链接: link

二维前缀和

正如前边所说,二维前缀和就是快速计算某一矩形区域内元素的和。
关于二维前缀和有两个问题要解决,跟一维的一样。
问题1:怎么求解前缀和矩阵?
S[i,j]表示的是从a[1,1]到a[i,j]围成的区域的元素和。
原矩阵:
在这里插入图片描述
前缀和矩阵:
在这里插入图片描述

根据容斥原理,S[i,j]=S[i-1,j]+S[i,j-1]-S[i-1,j-1]+a[i,j](绿色区域加了两次,所以要减去一次)
问题2:怎么利用前缀和计算某块区域的元素和?
在这里插入图片描述

可以看出,sum(a[x1,y1],…a[x2,y2])=S[x2,y2]-S[x2,y1-1]-S[x1-1,y2]+S[x1-1,y1-1]
S[x1-1,y1-1]是S[x2,y1-1]和S[x1-1,y2]交叉部分,多减了一次,所以要再加上一次。
(白色圆圈表示不包含,黑色圆圈表示包含)
例题:
acwing.796.子矩阵的和
问题描述:
输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。
对于每个询问输出子矩阵中所有数的和。
输入格式:
第一行包含三个整数 n,m,q。
接下来 n 行,每行包含 m 个整数,表示整数矩阵。
接下来 q 行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。
输出格式:
共 q 行,每行输出一个询问的结果。
数据范围:
1≤n,m≤1000,
1≤q≤200000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤矩阵内元素的值≤1000
代码实现:

n,m,q=map(int,input().split())
board=[[0 for i in range(m+1)]]
for i in range(n):
    board.append([0]+list(map(int,input().split())))
S=[[0 for i in range(m+1)] for i in range(n+1)]
for i in range(1,n+1):
    for j in range(1,m+1):
        S[i][j]=S[i-1][j]+S[i][j-1]-S[i-1][j-1]+board[i][j]
for i in range(q):
    x1,y1,x2,y2=map(int,input().split())
    ans=S[x2][y2]-S[x2][y1-1]-S[x1-1][y2]+S[x1-1][y1-1]
    print(ans)

原题链接:link

差分

差分可以看作是前缀和的逆运算。
什么是差分数组?
首先给定一个原数组a:a[1],a[2],a[3]…a[n];
然后我们构造一个数组b:b[1],b[2],b[3]…b[m]
使得a[i]=b[1]+b[2]+b[3]+…+b[i]
也就是说a就是b的前缀和数组,反过来,b就是a的差分数组。
那么差分数组的构造方式就可以是:
a[0]=0
b[1]=a[1]-a[0];
b[2]=a[2]-a[1];

b[n]=a[n]-a[n-1]
有了差分数组,可以在O(n)的时间复杂度内通过前缀和运算构造出原数组。
差分数组的作用
以一维数组为例:
差分可以在O(1)的时间复杂度内完成对某段区间值的统一修改。
简单的来说,用暴力的做法的话,需要经过一次遍历,对某段区间的每个数进行修改,时间复杂度是O(n),但是通过差分可以很快的完成统一地修改,时间复杂度是O(1)。
所以差分的作用就是:可以在O(1)的时间复杂度内完成对某段区间/某块区域的统一修改。

一维差分

基本原理:
给定区间[l,r],让我们把a数组中[l,r]区间中的每一个数都加上c---->a[l]+c,a[l+1]+c,…,a[r]+c。
考虑差分的做法,必须要明确a是b的前缀和数组,如果对b进行修改,a也会发生相应的变化,如果对b[i]进行修改,凡是包含b[i]项的所有前缀和,即从a[i]到a[n]都会受到影响。
那么我们让b[l]+c,a数组就会相应的变化为:a[l]+c,a[l+1]+c,…,a[n]+c
我们让b[r+1]-c,a数组就会相应的变化为:a[r+1]-c,a[r+1]-c,…,a[n]-c
我们只需要对b[l]进行加c,b[r+1]进行减c这样的操作,a[l]到a[r]就会全部加c,这就是一维差分的原理。
步骤:
1.遍历原数组a,构造差分数组
2.修改b[l],b[r+1](思考下为什么是b[r+1])
3.前缀和运算,用b数组更新a数组
例题:
acwing.797.差分
问题描述:
输入一个长度为 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,m=map(int,input().split())
a=[0]+list(map(int,input().split()))
b=[0 for i in range(n+2)]
for i in range(1,n+1):
    b[i]=a[i]-a[i-1]
for i in range(m):
    l,r,c=map(int,input().split())
    b[l]+=c
    b[r+1]-=c
for i in range(1,n+1):
    a[i]=a[i-1]+b[i]
ss=' '.join('%s' % x for x in a[1:])
print(ss.strip())

原题链接:link

二维差分

二维差分可以快速完成对某块矩形区域的统一修改。
问题1:如何构造差分数组?
在这里有种很神奇的处理方式,还是那句话,必须明确a是b的前缀和矩阵,b是a的差分矩阵,对b[i,j]的修改势必会影响到从a[i,j]往后的每一个数
假设矩阵a和矩阵b刚开始为空,但实际上矩阵a不为空,那么每次让b以[i,j]为右上角,[i,j]为左下角的区域内去插入一个a[i,j],等价于原矩阵a[i,j]到a[i,j]加上一个a[i,j],是不是就把矩阵a构造出来了,在这个过程中,差分矩阵b也被顺带构造出来了。
问题2:如何在[x1,y1]到[x2,y2]范围内修改原矩阵a的值?
这里直接给公式,可以自己画画图结合一维差分理解理解。

def insert(x1,y1,x2,y2,c):
	b[x1][y1]+=c
	b[x1][y2+1]-=c
	b[x2+1][y1]-=c
	b[x2+1][y2+1]+=c

例题:
acwing.798.差分矩阵
题目描述:
输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1) 和 (x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。
每个操作都要将选中的子矩阵中的每个元素的值加上 c。
请你将进行完所有操作后的矩阵输出。
输入格式:
第一行包含整数 n,m,q。
接下来 n 行,每行包含 m 个整数,表示整数矩阵。
接下来 q 行,每行包含 5 个整数 x1,y1,x2,y2,c,表示一个操作。
输出格式:
共 n 行,每行 m 个整数,表示所有操作进行完毕后的最终矩阵。
数据范围:
1≤n,m≤1000,
1≤q≤100000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤c≤1000,
−1000≤矩阵内元素的值≤1000
代码实现:

def insert(x1,y1,x2,y2,c):
    global b
    b[x1][y1]+=c
    b[x2+1][y1]-=c
    b[x1][y2+1]-=c
    b[x2+1][y2+1]+=c
n,m,q=map(int,input().split())
b=[[0 for i in range(m+2)] for i in range(n+2)]
a=[[0]*(m+1)]
for i in range(n):
    a.append([0]+list(map(int,input().split())))
for i in range(1,n+1):
    for j in range(1,m+1):
        insert(i,j,i,j,a[i][j])
for i in range(q):
    x1,y1,x2,y2,c=map(int,input().split())
    insert(x1,y1,x2,y2,c)
for i in range(1,n+1):
    for j in range(1,m+1):
        a[i][j]=a[i-1][j]+a[i][j-1]-a[i-1][j-1]+b[i][j]
for i in range(1,n+1):
    for j in range(1,m+1):
        print(a[i][j],end=' ')
    print(' ')

原题链接:link

树状数组

前缀和是一种离线运算,也就是说它是不支持修改的,当我们有多个询问,但是多个询问之间穿插着数的变化,前缀和这种方式就不行了,因为一旦数据更新,前缀和数组也要跟着更新,其实在时间上跟正常的求区间和没有区别了。
所以这里引入树状数组的概念。

树状数组支持的操作

①某个位置上数加一个数。(单点修改)
②求某一个前缀和。(区间查询)
这两个操作都可以在时间复杂度为O(logn)的条件下完成。
注:树状数组是在线的,即支持修改,前缀和是离线的,不支持修改。

实现原理

在这里插入图片描述
C[1]=A[1]
C[2]=A[2]+C[1]=A[2]+A[1]
C[3]=A[3]
C[4]=A[4]+C[3]+C[2]=A[4]+A[3]+A[2]+A[1]

x的二进制末尾0的个数k表示x在第k层,如上图所示:
其中C[x]=(x-2^k,x]=(x-lowbit(x),x](该区域的和)

树状数组包括的函数

1.lowbit(x):返回x的最后一位的1
2.add(x,v):在x位置加上v,并将后面相关联的位置也加上v
3.query(x):询问x的前缀和

例题

acwing.1264.动态求连续区间和
题目描述:
给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b] 的连续和。
输入格式:
第一行包含两个整数 n 和 m,分别表示数的个数和操作次数。
第二行包含 n 个整数,表示完整数列。
接下来 m 行,每行包含三个整数 k,a,b(k=0,表示求子数列[a,b] 的和;k=1,表示第 a 个数加 b)。
数列从 1 开始计数。
输出格式:
输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。
数据范围:
1 ≤ n ≤ 100000,
1 ≤ m ≤ 100000,
1 ≤ a ≤ b ≤ n,
数据保证在任何时候,数列中所有元素之和均在 int 范围内。

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8

输出样例:

11
30
35

代码实现:

def lowbit(x):
    #x的二进制后边有k个0,返回2^k
    return x&-x
def add(x,v):
    global tr,n
    i=x
    #之所以树状数组可以把修改某个数的时间复杂度降低到O(logn),就是因为后续被他影响的数是以lowbit(i)为间隔的
    #比如说:修改a[3],它只会影响到tr[4],tr[8],tr[16]...
    while i<=n:
        tr[i]+=v
        i+=lowbit(i)
def query(x):
    global tr
    res=0
    i=x
    #比如说我要计算前12项的和,s[12]=tr[12]+tr[8]
    #其中:
    #tr[12]
    #=tr[10]+tr[11]+a[12]
    #=tr[9]+a[10]+a[11]+a[12]
    #=a[9]+a[10]+a[11]+a[12]
    #tr[8]
    #=tr[4]+tr[6]+tr[7]+a[8]=tr[2]+tr[3]+a[4]+tr[5]+a[6]+a[7]+a[8]
    #=tr[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
    #tr[12]和tr[8]在add中已经完成
    while i!=0:
        res+=tr[i]
        i-=lowbit(i)
    return res
n,m=map(int,input().split())
a=[0]+list(map(int,input().split()))
tr=[0 for i in range(n+1)]
for i in range(1,n+1):
    #类似于构造差分数组
    add(i,a[i])
for i in range(m):
    k,x,y=map(int,input().split())
    if k==0:
        #和前缀和一样
        print(query(y)-query(x-1))
    elif k==1:
        add(x,y)

原题链接:link

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

so.far_away

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

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

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

打赏作者

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

抵扣说明:

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

余额充值