线段树

概念

线段树是擅长处理区间的。线段树是一颗完美二叉树(所有叶子结点的深度都相同,并且每个节点要么是叶子要么有两个儿子的树),树上的每个节点都在维护一个区间。根维护的是整个区间,每个节点维护的是父亲的区间二等分后的其中一个子区间。当有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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值