线段树
先看一个经典的问题模型:在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;
}