第八周的练习主要涉及前缀和、差分、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(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]);
}
}