线段树

线段树

        先看一个经典的问题模型:在X轴上有若干条线段,求线段覆盖的总长度。

        我们首先想到的做法是,设线段坐标范围为[min, max]。使用一个一维数组a,其中数组的第i个元素a[i]表示区间[i,i+1]的区间。数组初始时全部为0。
         对于每一条区间为[a,b]的线段,将[a,b]内所有对应的数组元素均设为1。最后统计数组中1的个数即可。 

//一般做法:
cin >> n;
memset(a,0,sizeof(a));
for(int i=0;i> x >> y;  //读入起点为x,终点为y的一条线段线段 
	for(int j=x;j<=y;j++) a[j]=1;
}

        当小标范围很大时,如[0..100000],这种方法效率就太低了。我们需要使用线段树。 



线段树的概念

        计算几何在长期的发展中,诞生了许多实用的数据结构,线段树就是其中的特例,它解决的问题是一维空间上的几何统计。
         由于线段是相互覆盖的,有时需要动态地取线段的并,例如取并区间的总长度,或者并区间的个数等等。一个线段对应一个区间,因此线段树类似区间树,是一个完全二叉树。
        下图就是一个能够表示[1,10]的线段树:

        下图是一个能够表示[1,9]的线段树:


线段树的数据结构定义

        由于线段树是一棵完全二叉树,那么我们可以用一维数组实现。父亲的区间是[a,b],(c=(a+b)/2)左儿子的区间是[a,c],右儿子的区间是[c+1,b],线段树需要的空间为数组大小的四倍。因线段树是一棵完全二叉树,如果根结点的编号为i,那么它的左孩子节点的编号为2*i,右孩子节点的编号为2*i+1。


线段树的基本操作


(1) 构造线段树 void build(int node, int left, int right);

         主要思想是递归构造,如果当前节点记录的区间只有一个值,则直接赋值,否则递归构造左右子树,最后回溯的时候给当前节点赋值。
        创建顺序为先序遍历,即先构造根节点,再构造左孩子,再构造右孩子。

void build(int node, int left, int right){ //node为当前节点编号;tree[node].sum存储该区间的和 
	tree[node].left=left, tree[node].right=right;
	if(left==right){ //只有一个元素,节点记录该单元素
		tree[node].sum=a[left];
		return;
	}
	int mid=(left+right)>>1;
	build(2*node, left, mid);
	build(2*node+1, mid+1, right);
	tree[node].sum=tree[2*node].sum + tree[2*node+1].sum;
} 

【例1】给含6个元素的数组:1、2、2、4、1、3建立线段树。

#include <iostream>   
using namespace std;  
struct tnode{
	int left,right,sum;
}tree[1001];
int a[251];   
void build(int node, int left, int right){ //node为当前节点编号;tree[node].sum存储该区间的和
	tree[node].left=left, tree[node].right=right;
	if(left==right){ 
		tree[node].sum=a[left];
		return;
	}
	int mid=(left+right)>>1;
	build(2*node, left, mid);
	build(2*node+1, mid+1, right);
	tree[node].sum=tree[2*node].sum + tree[2*node+1].sum;
} 
int main(){  
    a[0]=1, a[1]=2, a[2]=2, a[3]=4, a[4]=1, a[5]=3;  
    build(1, 0, 5);
    for(int i = 1; i<=20; ++i)  cout << "tree" << i << "=" << tree[i] << endl;  
    return 0;  
} 

     
        上图中,红色的数字为区间的范围;node为节点编号;如果节点是叶子节点,那么value的值存储的就是该节点元素的值,否则存储该区间最小值。由此可知,n个点的话大约共有2*n个结点,因此开3*n的结构体一定是够的。


(2) 区间查询int query(int node, int left, int right, int l, int r);

         其中node为当前查询节点,left,right为当前节点存储的区间,l,r为此次query所要查询的区间。
        主要思想是把所要查询的区间[a,b]划分为线段树上的节点,然后将这些节点代表的区间合并起来得到所需信息。
     
        比如【例1】中构造的线段树,如上图,如果询问区间是[0,2],或者询问的区间是[3,3],不难直接找到对应的节点回答这一问题。但并不是所有的提问都这么容易回答,比如[0,3],就没有哪一个节点记录了这个区间的最小值。当然,解决方法也不难找到:把[0,2]和[3,3]两个区间(它们在整数意义上是相连的两个区间)的最小值“合并”起来,也就是求这两个最小值的最小值,就能求出[0,3]范围的最小值。同理,对于其他询问的区间,也都可以找到若干个相连的区间,合并后可以得到询问的区间。

void query(int node, int l, int r){
	int left=tree[node].left, right=tree[node].right;
	if(left==l && right==r){ //如果当前的区间就是查询区间,直接取该节点的值,返回
		ans+=tree[node].sum;
		return;
	}
	int mid=(left+right)>>1;
	if(r<=mid) query(2*node,l,r);
	else if(l>mid) query(2*node+1,l,r);
	else{
		query(2*node,left,mid,l,mid);
		query(2*node+1,mid+1,r);
	}
}

         可见,这样的过程一定选出了尽量少的区间,它们相连后正好涵盖了整个[l,r],没有重复也没有遗漏。同时,考虑到线段树上每层的节点最多会被选取2个,一共选取的节点数也是O(log n)的,因此查询的时间复杂度也是O(log n)。 线段树并不适合所有区间查询情况,它的使用条件是“相邻的区间的信息可以被合并成两个区间的并区间的信息”。即问题是可以被分解解决的。



(3) 区间或节点的更新 及 线段树的动态维护change

         这是线段树核心价值所在。

一、单节点更新insert

        下面的程序中,给ind节点加上x,当前的节点为node;初始从根节点1开始查询。当ind节点的值更新后,通过递归回溯,整个线段树tree[node].sum也已经更新了。

void insert(int node, int ind, int x){  //节点ind加x,node为当前节点 
	int left=tree[node].left, right=tree[node].right;
	if(left==right){
		tree[node].sum+=x;
		return
	}
	int mid=(left+right)>>1;
	if(ind<=mid) insert(node*2, ind, x);
	else insert(node*2+1, ind, x);
	tree[node].sum=tree[node*2].sum + tree[node*2+1].sum    //回溯更新父节点
}

二、区间更新change(线段树中最有用的)

         对于要更新的区间[l,r],先判断[l,r]在线段树区间[left,right]的哪个区间内,然后按照建树的方法,二分递归到叶子节点进行更新。更新操作只更新需要更新的区间,而不用把[1,n]全部搜一遍。

void change(int node, int l, int r, int k){ 
	//其中node为当前查询节点,left,right为当前节点存储的区间,l,r为此次query所要查询的区间,修改的值k
	int left=tree[node].left, right=tree[node].right;
	if(left==right){
		tree[node].sum+=k;
		return;
	}
	int mid=(left+right)>>1;
	if(left==l && right==r){ //如果当前区间正好是修改区间,二分递归到叶子节点
		change(2*node, left, mid, k);
		change(2*node+1, mid+1, right, k);		
	}
	else if(r<=mid) change(2*node, l, r, k); //如果当前修改区间在左孩子
	else if(l>mid) change(2*node+1, l, r, k);//如果当前修改区间在右孩子
	else{ //如果当前修改区间横跨左右孩子,如在区间[1,5]内修改[2,4]的值
		change(2*node, l, mid, k);
		change(2*node+1, mid+1, r, k);
	}
	tree[node].sum=tree[2*node].sum + tree[2*node+1].sum;
}


  【例题2】查询

        给你N个数,有两种操作:
        1:给区间[a,b]的所有数增加X
        2:询问区间[a,b]的数的和。
输入描述
        第一行一个正整数n,接下来n行n个整数,再接下来一个正整数Q,每行表示操作的个数,如果第一个数是1,后接3个正整数,表示在区间[a,b]内每个数增加X,如果是2,表示操作2询问区间[a,b]的和是多少。
输出描述 Output Description
        对于每个询问输出一行一个答案
样例输入 Sample Input
        3
        1
        2
        3
        2
        1 2 3 2
        2 2 3
样例输出 Sample Output
        9
数据范围
        1<=n<=200000
        1<=q<=200000 

#include <iostream>   
using namespace std;  
int n,q,ans,a[1000000];
struct tnode{
	int left,right,sum;
}tree[1000000];
void build(int node, int left, int right){ //node为当前节点编号;tree[node].sum存储该区间的和 
	tree[node].left=left, tree[node].right=right;
	if(left==right){ //如果是叶子节点,直接存入sum中 
		tree[node].sum=a[left];
		return;
	}
	int mid=(left+right)>>1;
	build(2*node, left, mid);
	build(2*node+1, mid+1, right);
	tree[node].sum=tree[2*node].sum + tree[2*node+1].sum;
}  
void change(int node, int l, int r, int k){  //区间[l,r]所有元素增加k 
	int left=tree[node].left, right=tree[node].right;
	if(left==right){
		tree[node].sum+=k;
		return;
	}
	int mid=(left+right)>>1;
	if(left==l && right==r){
		change(2*node, left, mid, k);
		change(2*node+1,mid+1, right, k);		
	}
	else if(r<=mid) change(2*node,l,r,k);
	else if(l>mid) change(2*node+1,l,r,k);
	else{
		change(2*node,l,mid,k);
		change(2*node+1,mid+1,r,k);
	}
	tree[node].sum=tree[2*node].sum + tree[2*node+1].sum;
}
void query(int node, int l, int r){
	int left=tree[node].left, right=tree[node].right;
	if(left==l && right==r){
		ans+=tree[node].sum;
		return;
	}
	int mid=(left+right)>>1;
	if(r<=mid) query(2*node,l,r);
	else if(l>mid) query(2*node+1,l,r);
	else{
		query(2*node,l,mid);
		query(2*node+1,mid+1,r);
	}
}
int main(){
	cin >> n;
	for(int i=1;i<=n;i++) cin >> a[i];
	build(1,1,n);
	cin >> q;
	for(int i=1;i<=q;i++){
		int op,x,y,z;
		cin >> op;
		if(op==1){
			cin >> x >> y >> z;
			change(1,x,y,z); //x,y区间增加z 
		}
		if(op==2){
			cin >> x >> y;
			ans=0;
			query(1,x,y);
			cout << ans << endl;
		}	
	} 
}


【例3】stars (poj2352)

         天文学家经常把每一颗恒星作为一个平面直角坐标系中的点来观测,他们称之为星图。在星图中,每个星星的等级,等于在它左边且在它下边(包括水平和垂直方向)的星星的数量。

        例如,上面这个星图中,星星5的等级是3(由三颗星星组成,1,2和4)。星星2和星星4的等级都是1。在这个星图中,只有1个0级的星星(1),2个1级的星星(2,4),1个2级的星星(3),1个3级的星星(5)。
        给定一个星图,你能算出每个星星的等级吗? 
【输入格式】
        第一行一个整数n,表示星图中星星的颗数(1<=n<=15000)。接下来的n行,每行2个整数x和y,以空格隔开,(0<=x,y<=32000)。没有两颗星星在同一个位置。给出的星星坐标是按y轴递增的,如果y坐标相等,则按x坐标递增。
【输出格式】
        输出共n行,每行一个整数。第一行为0级的星星数量,第二行为1级的星星数量,。。。,最后一行为n-1级星星的数量。
【输入样例】
        5
        1 1
        5 1
        7 1
        3 3
        5 5
【输入样例】
        1 
        2
        1
        1
        0

        
解题思路:用线段树维护横坐标为1到x的点有多少个。不用管y,因为在读入时y就是升序的,所以一定保证yi>=yj 

#include <cstdio>
using namespace std;  
int maxx=0,n,ans[30001];
struct ta{
	int x,y;
}a[15001];
struct tnode{
	int left,right,sum;
}tree[100001];
void build(int node, int left, int right){  
	tree[node].left=left, tree[node].right=right;
	if(left==right)	return;
	int mid=(left+right)>>1;
	build(2*node, left, mid);
	build(2*node+1, mid+1, right);
}  
void insert(int node, int d){ 
	int left=tree[node].left, right=tree[node].right;
	tree[node].sum++;
	if(left==right) return;
	int mid=(left+right)>>1;
	if(d <= mid) insert(node*2, d);
	else if (d > mid) insert(node*2+1, d);
}
int query(int node, int l, int r){
	int left=tree[node].left, right=tree[node].right;
	if(left==l && right==r) return tree[node].sum;
	int mid=(left+right)>>1;
	if(r<=mid) return query(node*2,l,r);
	else if(l>mid) return query(2*node+1,l,r);
	else return query(2*node,l,mid) + query(node*2+1, mid+1,r);
}
int main(){
	cin >> n;
	for(int i=1;i<=n;i++){
		cin >> a[i].x >> a[i].y;
		if(a[i].x>maxx) maxx=a[i].x;
	} 
	build(1,0,maxx);
	for(int i=1;i<=n;i++){
		int x=a[i].x;
		ans[query(1,0,x)]++;
		insert(1,x);	
	} 
	for(int i=0;i<n;i++) cout << ans[i] << endl;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值