ST表
st表可以解决区间最值的问题。可以做到O(nlogn)预处理 ,O(1)查询,但是不支持修改。
st表的大概思路就是用st[i][j]来表示从i开始的2的j次方个树中的最值,查询时就从左端点开始,找到区间长度是2的多少次方,然后进行查询。然而,很明显,我们要查询的区间长度不一定是2的多少次幂。那怎么做到O(1)查询呢,这就要用到最值的特性。
如图,假如我们要查询2到7之间的最大值,但是7-2+1在22与23之间,我们选择22,也就是st[2][2],那剩下的6,7怎么办,我们考虑倒着从7往回算,也就是在st[7-22][2]与st[2][2]取max作为从2到7的最大值。
首先,进行预处理,st[i][j]表示从i开始的2的j次方,那么st[i][j]就应该是从i开始2的j-1次方与从i+2j-1开始的2的j-1次方中的最大值,那么进行递推就好了。
代码:
1 #include<cstdio> 2 #include<iostream> 3 using namespace std; 4 const int N=100100; 5 int n,m,a[N],st[N][20],log[N],cf[20]; 6 void pre() 7 { 8 log[2]=1; 9 log[1]=0; 10 for(int i=3;i<=n;++i) 11 { 12 log[i]=log[i/2]+1; 13 } 14 cf[0]=1; 15 cf[1]=2; 16 for(int i=2;i<=log[n]+1;++i) 17 { 18 cf[i]=cf[i-1]*2; 19 } 20 } 21 int read() 22 { 23 int x=0,f=1;char c=getchar(); 24 while(c<'0'||c>'9') 25 { 26 if(c=='-') f=-1; 27 c=getchar(); 28 } 29 while(c>='0'&&c<='9') 30 { 31 x=x*10+c-'0'; 32 c=getchar(); 33 } 34 return x; 35 } 36 int ff(int x,int y) 37 { 38 int l=y-x+1,k=log[l]; 39 int f=max(st[x][k],st[y-cf[k]+1][k]); 40 return f; 41 } 42 int main() 43 { 44 n=read(),m=read(); 45 pre(); 46 for(int i=1;i<=n;++i) 47 { 48 st[i][0]=a[i]=read(); 49 50 } 51 52 for(int j=1;j<=log[n];++j) 53 { 54 for(int i=1;i+cf[j]-1<=n;++i) 55 { 56 st[i][j]=max(st[i][j-1],st[i+cf[j-1]][j-1]); 57 } 58 } 59 for(int i=1,x,y;i<=m;++i) 60 { 61 x=read(),y=read(); 62 cout<<ff(x,y)<<"\n"; 63 } 64 return 0; 65 }
树状数组
其实,树状数组的原理我并不是很懂,但是因为其短小精炼的代码,令我非常喜欢。。。。
树状数组不需要预处理,只有修改与查询两种操作。修改可以是加或减一个值,查询的是一个区间和。
首先我们需要一个数组tree。
然后就是修改,只需写一个几行的子函数,然后将修改元素的下标和要加的元素传入函数,然后奇迹就发生了。
查询传入要查询的下标就可以查询从1到改元素之间的区间和,如果查询从l到r,只需分别求出其从以到他们的区间和,然后相减即可,类似于前缀和。具体的都在代码里了。
1 #include<cstdio> 2 #include<iostream> 3 using namespace std; 4 const int N=500005; 5 int tree[N],n,m; 6 void add(int a,int pos) 7 { 8 while(pos<=n){ 9 tree[pos]+=a; 10 pos+=pos&-pos; 11 } 12 } 13 int cc(int pos) 14 { 15 int ans=0; 16 while(pos>=1){ 17 ans+=tree[pos]; 18 pos-=pos&-pos; 19 } 20 return ans; 21 } 22 int main() 23 { 24 scanf("%d%d",&n,&m); 25 for(int i=1,x;i<=n;++i) 26 { 27 scanf("%d",&x); 28 add(x,i); 29 } 30 for(int i=1,bz,x,y;i<=m;++i){ 31 scanf("%d%d%d",&bz,&x,&y); 32 if(bz==1) 33 add(y,x); 34 else{ 35 int kk=cc(y),zz=cc(x-1); 36 printf("%d\n",kk-zz); 37 } 38 } 39 return 0; 40 }
上面讲的是树状数组的单点修改和区间查询,下面写一下,树状数组的区间修改单点查询。
首先介绍一下差分数组和前缀和。
前缀和就是记录前几个数的和,差分数组呢就是记录当前位置减去前一个位置的数的差。
然后要用到一个前缀和与差分数组的性质:前缀和数组的差分数组是原数组,差分数组的前缀和是原数组。
证明很显然,动手一推就知道了。
那么这与树状数组有什么关系呢,通过上面那个树状数组,我们知道,树状数组可以记录前几个数的和,现在我们要做的是区间修改和单点查询。
这时又要用到一个差分数组的一个性质。
差分数组进行区间加减时比较方便,比如如果将从i到j之间的数加k,那么只需将他们的差分数组i位置+k,并且将j+1位置的数-k即可。
证明同样很显然,动手推。
那么这就有联系了,我们树状数组维护的相当于是前缀和,然后我们如果用他维护原数组的差分数组,那么他就相当于维护的原数组,这样利用第一个性质单点查询就解决了。再就是区间修改,因为我们维护的是差分数组,所以利用性质二进行区间修改就好了。
看代码就很清楚明了了
1 #include<cstdio> 2 #include<iostream> 3 using namespace std; 4 const int N=500010; 5 int n,m,a[N],tree[N]; 6 void read(int &x) 7 { 8 x=0;int f=1;char c=getchar(); 9 while(c<'0'||c>'9') { if(c=='-') f=-1; c=getchar(); } 10 while(c>='0'&&c<='9'){ x=x*10+c-'0'; c=getchar();} 11 x=x*f; 12 } 13 void add(int pos,int w) 14 { 15 while(pos<=n) 16 { 17 tree[pos]+=w; 18 pos+=pos&-pos; 19 } 20 return; 21 } 22 int ff(int pos) 23 { 24 int ans=0; 25 while(pos>=1) 26 { 27 ans+=tree[pos]; 28 pos-=pos&-pos; 29 } 30 return ans; 31 } 32 33 34 int main() 35 { 36 int last=0; 37 read(n),read(m); 38 for(int i=1,x;i<=n;++i) 39 { 40 read(x); 41 add(i,x-last); 42 last=x; 43 } 44 for(int i=1,x,y,z,k;i<=m;++i) 45 { 46 read(x); 47 if(x==1) 48 { 49 read(y),read(z),read(k); 50 add(y,k); 51 add(z+1,-k); 52 } 53 else if(x==2) 54 { 55 read(y); 56 printf("%d\n",ff(y)); 57 } 58 } 59 return 0; 60 }