概念
线段树是擅长处理区间的。线段树是一颗完美二叉树(所有叶子结点的深度都相同,并且每个节点要么是叶子要么有两个儿子的树),树上的每个节点都在维护一个区间。根维护的是整个区间,每个节点维护的是父亲的区间二等分后的其中一个子区间。当有n个元素时,对区间的操作可以在O(logn)的时间内完成。
根据节点中维护的数据的不同,线段树可以提供不同的功能。下面我们以实现了Range Minimum Query(RMQ 区间最小查询)操作的线段树为例,进行说明。
基于线段树的RMQ的结构
下面要建立的线段树在给定数列a0,a1,···,an-1的情况下,可以在O(logn)时间内完成如下两种操作:
- 给定s和t,求as,as+1,···,at的最小值
- 给定i和x,把ai的值改成x
结构:从下往上,每个区间保存的值都是这个区间里面最小的值
基于线段树的RMQ的查询
即使查询的是一个比较大的区间,由于较靠上的节点对应较大的区间,通过这些区间就可以知道大部分值的最小值,从而只需要访问很少的节点就可以求得最小值。
要求某个区间的最小值,像下面这样递归处理就可以了。
- 如果所查询的区间和当前节点对应的区间完全没有交集,那么久返回一个不影响答案的值(例如INT_MAX)
- 如果所查询的区间完全包含了当前节点对应的区间,那么就返回当前节点的值。
- 以上两种情况都不满足的话,就对两个儿子进行递归处理,返回两个结果中的较小值。
基于线段树的RMQ的值的更新
在更新a0的值时,需要重新计算下所有包含它的父节点的值。
在更新ai的值时,需要对包含i的所有区间对应的节点重新进行计算。在更新时,可以从下面的节点开始向上不断更新,把每个节点的值更新为左右两个儿子的值的较小者就好了。
基于线段树的RMQ复杂度
不管哪种操作,对于每个深度都最多访问常数个节点。因此对于n个元素,每一次操作的复杂度都是O(logn)。
此外,n个元素的线段树的初始化的时间复杂度和总的空间复杂度都是O(n)。这是因为节点数时n+n/2+n/4+···=2n。
基于线段树的RMQ实现
const int MAX_N=1<<17;
int n,dat[MAX_N<<1];
void Init(int n_){
n=1;
//为了完美二叉树的保险起见,将元素个数扩大到2的幂
while(n<n_)n*=2;//此时的n为最后一层的第二个
for(int i=0;i<2*n-1;i++){
dat[i]=INF_MAX;
}
}
//更新把第k(0序)的值更新为a
void update(int k,int a){
k+=n-1;//n-1为叶子节点的第一个
dat[k]=a;
while(k>0){
k=(k-1)/2;
dat[k]=min(dat[(k<<1)+1],dat[(k<<1)+2]);
}
}
//查询区间为[a,b)
//k是节点的编号,l,r代表的是这个节点的区间是[l,r)
void query(int a,int b,int k,int l,int r){
if(r<=a||b<=l)return INT_MAX;//如果a>=r||l>=b即a与b完全不相交则返回INT_MAX
//如果[a,b)完全包含[l,r)的值,则直接返回当前节点的值
//注意是[a,b)完全包含[l,r)的值,而不是[l,r)包含[a,b)的值,出现这种情况只有两种可能。
//一、a==l,b==r;
//二、[l,r)是[a,b)的一个子区间即a<=l&&r<=b
if(a<=l&&r<=b)return dat[k];
else{
int v1=query(a,b,(k<<1)+1,l,(l+r)>>1);
int v2=quert(a,b,(k<<1)+2,(l+r)>>1,r);
return min(v1,v2);
}
}
需要用到线段树的问题:Crane POJ No.2991
每个节点表示一段连续的线段的集合,并且维护下面两个值。
- 把对应线段集合中的第一条线段转至垂直方向之后,从第一条线段的起点指向最后一条线段的终点的向量。
- (如果该节点有儿子节点)两个儿子节点对应的部分连接之后,右儿子需要转动的角度。
也就是说,如果节点i表示的向量是vxi,vyi,角度是angi,两个儿子节点是ch1和chr,那么就有:
vxi=vxchl+(cos(angi) x vxchr-sin(angi) x vychr)
vyi=vychl+(sin(angi)) x vcchr + cos(angi) x vychr)
这样,每次更新就可以在O(logn)时间内完成,而输出的值就是根节点对应的向量的值。
下面的实现和RMQ的有所不同,线段树的大小并没有扩大到2的幂。这个时候,线段树并不是棵完美二叉树,但在本题中同样可以完成操作。
#include<cstdio>
#include<iostream>
#include<cmath>
using namespace std;
const int ST_SIZE=1<<15-1;
int N,C;
int L[10005];
int S[10005],A[10005];
//线段树维护的数据
double vx[ST_SIZE],vy[ST_SIZE];
double ang[ST_SIZE];
//为了查询角度的变化而保存的当前角度的数组
double prv[10005];
//k是节点编号,l,r代表的是[L,r)的区间
//初始化线段树
void init(int k,int l,int r){
//由于线段一开始是竖直的,所以不存在x向量,那么x与y向量之间的夹角也为0
ang[k]=vx[k]=0.0;
//对于叶子节点初始时都是y向量,他的长度就是当前这条线段的长度(叶子节点就是初始时的一条线段)
if(r-l==1){
vy[k]=L[l];
}
else{//非叶子节点,他们的y向量的长度为左右两个子节点的长度之和
init((k<<1)+1,l,(l+r)>>1);
init((k<<1)+2,(l+r)>>1,r);
vy[k]=vy[(k<<1)+1]+vy[(k<<1)+2];
}
}
//s表示s和s+1的节点换为角度a
//v表示节点编号
//[l,r)表示区间
void change(int s,double a,int v,int l,int r){
if(s<=l)return;
else if(s<r){
int chl=(v<<1)+1,chr=(v<<1)+2;
int m=(l+r)>>1;
change(s,a,chl,l,m);
change(s,a,chr,m,r);
if(s<=m)ang[v]+=a;
double s=sin(ang[v]),c=cos(ang[v]);
vx[v]=vx[chl]+(c*vx[chr]-s*vy[chr]);
vy[v]=vy[chl]+(s*vx[chr]+c*vy[chr]);
}
}
void solve(){
init(0,0,N);
for(int i=0;i<N;i++)prv[i]=M_PI;//初始时都是180度即Π
//处理操作
for(int i=0;i<C;i++){
int s=S[i];
double a=A[i]/360.0*2*M_PI;//角度换为弧度
change(s,a-prv[s],0,0,N);
prv[s]=a;
printf("%.2f %.2f\n",vx[0],vy[0]);
}
}
int main(){
cin >> N >> C;
for(int i=0;i<N;i++){
cin >> L[i];
}
for(int i=0;i<C;i++){
cin >> S[i];
}
for(int i=0;i<C;i++){
cin >> A[i];
}
solve();
return 0;
}