涉及的知识点
第八周的练习主要涉及前缀和、差分、RMQ问题(基于ST表)、树状数组
拓展:线段树
前缀和
前缀和基本定义:一个数组某项下标之前(包括此元素)的所有数组元素和
前缀和在输入时便可以在线处理数据,之后在查询区间和时,可以很快地得到两个下标间的区间和。
一维
由定义,
p
r
e
[
n
]
=
∑
i
=
0
n
v
a
l
u
e
[
i
]
pre[n]=\sum_{i=0}^nvalue[i]
pre[n]=∑i=0nvalue[i],n为定义中的下标,
元素的递推式为
p
r
e
[
n
]
=
p
r
e
[
n
−
1
]
+
v
a
l
u
e
[
n
]
pre[n]=pre[n-1]+value[n]
pre[n]=pre[n−1]+value[n]。
代码实现
int value[10000],pre[10000]
for(int i=1;i<=N;i++)
{
cin >>value[i];
pre[i]=pre[i-1]+value[i];
}
for(int i=1;i<N;i++)
cout <<pre[i]<<" ";
二维
由定义,
p
r
e
[
n
]
[
m
]
=
∑
i
=
0
n
∑
j
=
0
m
pre[n][m]=\sum_{i=0}^n\sum_{j=0}^m
pre[n][m]=∑i=0n∑j=0m,n、m为定义中的下标
元素的递推式为
p
r
e
[
n
]
[
m
]
=
p
r
e
[
n
−
1
]
[
m
]
+
p
r
e
[
n
]
[
m
−
1
]
−
p
r
e
[
n
−
1
]
[
m
−
1
]
+
v
a
l
u
e
[
n
]
[
m
]
pre[n][m]=pre[n-1][m]+pre[n][m-1]-pre[n-1][m-1]+value[n][m]
pre[n][m]=pre[n−1][m]+pre[n][m−1]−pre[n−1][m−1]+value[n][m]。
代码实现
int value[10000][10000],pre[10000][10000];
for(int i=1;i<=N;i++)
for(int j=1;j<=M;j++)
{
cin >>value[i][j];
pre[i][j]=pre[i-1][j]+pre[i][j-1]-pre[i-1][j-1]+value[i][j];
}
for(int i=1;i<=N;i++)
for(int j=1;j<=M;j++)
cout <<pre[i][j]<<" ";
差分
差分基本定义:对于一个数组中的某个元素,其差分为自身与先前一个元素的差,当然该元素不能为首个元素,定义首个元素的差分为1。
一维
由定义,
d
i
f
f
e
r
[
i
]
=
v
a
l
u
e
[
i
]
−
v
a
l
u
e
[
i
−
1
]
differ[i]=value[i]-value[i-1]
differ[i]=value[i]−value[i−1]
对于一个一维序列进行区间操作,可以通过差分记录下每次操作,最后一次性进行所有操作,与前缀和结合,差分较难理解的便是操作时序列的首项+k(k为操作数,也可以为-k),末项的后一项-k(或+k)。
代码实现
int l,R,k;
cin >>l>>r>>k;
differ[l]+=k;
differ[r+1]-=k;
int add=0;
for(int i=1;i<=N;i++)
{
add+=differ[i];//注意,当到达r+1时,add正好由k变为0
value[i]+=value[i-1]+add;
}
二维
方法和一维类似,只不过需要记下四个位置对应的操作。
代码实现
while(m--)
{
int x1,y1,x2,y2,k;
cin >>x1>>y1>>x2>>y2>>k;
differ[x1][y1]+=k;
differ[x2+1][y2+1]+=k;
differ[x2+1][y1]-=k;
differ[x1][y2+1]-=k;
}
RMQ问题(基于ST表)
RMQ(Range Minimum/Maximum Query),即区间最值查询,该算法用较长时间预处理( O ( n log n ) O(n\log n) O(nlogn)),之后在 O ( 1 ) O(1) O(1)内处理查询。
在RMQ算法中,使用一个二维数组 d p [ ] [ ] dp[ ][ ] dp[][]记录划分区间的最大/小值,在存储的时候采用二分的方法,即每次存储的是一个大区间的两个平分后的子区间,如 d p [ i ] [ j ] dp[i][j] dp[i][j]表示从序号i开始连续 2 j 2^j 2j个数的最小值。
求 d p [ i ] [ j ] dp[i][j] dp[i][j]时可以采用二分的方法,即求 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j−1]和 d p [ i + 2 j − 1 ] [ j − 1 ] dp[i+2^{j-1}][j-1] dp[i+2j−1][j−1]两个区间的最小值,前者为从 i → i + 2 j − 1 − 1 i\rightarrow i+2^{j-1}-1 i→i+2j−1−1,后者为 i + 2 j − 1 → i + 2 j − 1 i+2^{j-1}\rightarrow i+2^j-1 i+2j−1→i+2j−1。
那么,状态转移方程就可以写出:
d p [ i ] [ j ] = m i n ( d p [ i ] [ j − 1 ] , d p [ i + 1 < < ( j − 1 ) ] [ j − 1 ] ) dp[i][j]=min(dp[i][j-1],dp[i+1<<(j-1)][j-1]) dp[i][j]=min(dp[i][j−1],dp[i+1<<(j−1)][j−1])
具体的代码实现如下:
void RMQ()
{
for(int i=1;i<=N;i++)//初始化
dp[i][0]=arr[i];
for(int j=1;(1<<j)<=N;j++)//j取1、2、4...,步长
for(int i=1;i+(1<<j)-1<=N;i++)
//第一次更新0~1、1~2,之后j移位,第二次更新0~2,2~4,以此类推
dp[i][j]=min(dp[i][j-1],dp[i+(1<<j-1)][j-1]);
树状数组
难题解析
拓展
线段树
线段树是一棵完美二叉树,树上每个节点维护一个区间,根维护整个区间,每个节点维护的是父节点的区间二分后的子区间之一,根据节点中维护的数据不同,线段树可提供不同功能,下面以RMQ为例
代码实现
#define MAX (1<<18)-1;
int N=1,dat[MAX];//存储线段树的全局数组
void init(int _N)//简单起见,将元素个数扩大到2的幂
{
while(N<_N)N<<=1;
for(int i=0;i<(N<<1)-1;i++)//设置所有值为INT_MAX
dat[i]=INT_MAX;
}
void updata(int id,int a)
{
id+=N-1;//叶结点
dat[id]=a;
while(id)//向上更新
{
id=(id-1)>>1;
dat[id]=min(dat[(id<<1)+1],dat[(id<<1)+2]);
}
}
参考文献
- 前缀和与差分
- 前缀和、二维前缀和与差分的小总结
- 前缀和、差分
- RMQ算法讲解
- 《挑战程序设计竞赛》