您的位置:首页 > 理论基础 > 数据结构算法

【学习笔记】高级数据结构--线状树 poj 3264

2014-07-19 20:49 274 查看
这几天学习ACM训练,第一次接触了线段树(Interval Tree)。

现在给大家简单说说我的一点点理解,以及poj上一道典型题目的源码和超详细的注释。

线段树:实际上称为区间树更好理解一些。线段:树上的每个节点对应于一个线段(区间)。

特点:(1)同一层的节点所代表的区间相互不会重叠。

(2)同一层节点所代表的区间,加起来是个连续的区间。

(3)叶子节点的区间长度是单位长度,不能再分。

线段树是一颗二叉树,树中每个结点表示了一个区间[a,b]。a,b通常是整数。对于每一个非叶节点所表示的节点[a,b],其左儿子表示区间为[a,(a+b)/2],右儿子表示区间为[(a+b)/2+1,b]。(除法去尾取整)。

图为区间【1,9】的线段树。



线段树的深度不超过log2(n)+1。

线段树上更新叶子节点和进行区间分解时间复杂度都是O(log(n))的。这些结论为线段树能在O(log(n))的时间内完成插入数据,更新数据、查找、统计等工作,提供了理论依据。

线段树的基本用途:线段树适用于和区间统计有关的问题。比如某些数据可以按区间进行划分,按区间动态进行修改,而且还需要按区间多次进行查询,那么使用线段树可以达到较快查询速度。

线段树的应用举例

给你一个数的序列A1A2……An。并且可能多次进行下列两个操作:

1、对序列里面的某个数进行加减

2、询问这个序列里面任意一个连续的子序列AiAi+1……Aj的和是多少。

希望第2个操作每次能在log(n)时间内完成

分析:

显然,[1,n]就是根节点对应的区间

可以在每个节点记录该节点对应的区间里面的数的和Sum。

对于操作1:因为序列里面Ai最多只会被线段树的log(n)个节点覆盖。只要求对线段树覆盖Ai的节点的Sum进行加操作,因此复杂度是log(n)

对于操作2:同样只需要找到区间所覆盖的“终止”节点,然后把所找“终止”节点的Sum累加起来。因为这些节点的数量是O(log(n))的,所以这一步的复杂度也是log(n)

如果走到节点[L,R]时,如果要查询的区间就是[L,R]

(求AL到AR的和)那么直接返回该节点的Sum,并累加到总的和上;

如果不是,则:

对于区间[L,R],取mid=(L+R)/2;

然后看要查询的区间与[L,mid]或[mid+1,R]哪个有交集,就进入哪个区间进行进一步查询。

因为这个线段树的深度最深的LogN,所以每次遍历操作都在LogN的内完成。但是常数可能很大。

如果是对区间所对应的一些数据进行修改,过程和查询类似。

总结:

用线段树解题,关键是要想清楚每个节点要存哪些信息(当然区间起终点,以及左右子节点指针是必须的),以及这些信息如何高效更新,维护,查询。不要一更新就更新到叶子节点,那样更新效率最坏就可能变成O(n)的了。

方法:先建树,然后插入数据,然后更新,查询。

---------------------------------------------------------------------------------------------------------------------------------------------------------

POJ 3264 Balanced Lineup

给定Q(1 ≤ Q≤ 200,000)个数A1,A2… AQ,,多次求任一区间Ai–Aj中最大数和最小数的差。

Sample Input

6 3 //6个数,3次个查询

1

7

3

4

2

5

1 5

4 6

2 2

Sample Output

6

3

0

源代码:

#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
const int INF=0xffffff0;
int minV= INF;
int maxV= -INF;
struct Node{
//定义了树中每个节点的信息
int L,R; //树节点的左孩子L,右孩子R
int minV,maxV;  //在树节点中记录在其范围内的max和min
int Mid(){   //寻找L和R的中间值返回
return (L+R)/2;
}
};
Node tree[400010];

void BuildTree(int root,int L,int R){
//相当于初始化整棵树,初始化每个树节点,传进来这个节点所包含的数值范围L到R
tree[root].L=L;
tree[root].R=R;
tree[root].minV= INF;
tree[root].maxV= -INF;
if(L!=R){  //递归调用该方法,初始化整棵树
BuildTree(2*root+1,L,(L+R)/2);
BuildTree(2*root+2,(L+R)/2+1,R);
}
}

void Insert(int root,int i,int v){
//将第i个数,其值为v,插入线段树
//实际上,Insert是构建线段树的过程
if(tree[root].L==tree[root].R){
//如果当前节点的L和R相等,说明到了叶子节点
//对叶子节点的操作是:将叶子本身的值v定义为该节点的max和min
tree[root].minV=tree[root].maxV=v;
return ;
}
//如果不是叶子节点
//则插入了新的节点此时需要更新包含这个节点在内的所有节点的max和min
// 首先对本节点进行操作:
//   如果新插入的v比当前节点的minV还小,就将其更新为当前节点的minV
//   如果新插入的v比当前节点的maxV还大,就将其更新为当前节点的maxV
// 其次对其后影响了的节点进行更新判断,需要进行递归调用该方法。
tree[root].minV=min(tree[root].minV,v);
tree[root].maxV=max(tree[root].maxV,v);
if(i<=tree[root].Mid()){
//如果i小于等于mid说明可能影响到了左树的max或min
//调用左树进行判断更新;
Insert(2*root+1,i,v);
}else{
//如果i大于mid说明可能影响到了右树的max或min
//调用右树进行判断更新;
Insert(2*root+2,i,v);
}
//需要说明:此时不会有第三种情况。
//因为这个值只可能插入这个节点的左树和右树这两种情况
}

void Query(int root,int s,int e){
//查询区间[s,e]中的最大值和最小值,如果更优就记在全局变量里
if(tree[root].minV >= minV && tree[root].maxV <=maxV )
return ;
if(tree[root].L==s && tree[root].R ==e){
//如果查询的区间s、e就是当前节点所包含的区间
//将所包含的最大值和最小值依次和 当前max和min比较
minV= min(minV,tree[root].minV);
maxV= max(maxV,tree[root].maxV);
return ;
}
//如果区间右边界小于mid,递归调用查询左节点,区间s,e ,此时root已经变成左节点
if(e<=tree[root].Mid())
Query(2*root+1,s,e);
//如果区间左边界大于mid,递归查询有节点区间s,e,注意此时root已经变成右节点
else if(s>tree[root].Mid())
Query(2*root+2,s,e);
else{
//说明所查询区间s,e横跨了该节点所对应的左右节点
//则依次调用左节点和右节点
Query(2*root+1,s,tree[root].Mid());
Query(2*root+2,tree[root].Mid()+1,e);
}
}
int main(){
int n,q,h;
int i,j,k;
scanf("%d%d",&n,&q);
//n代表一共有n个数需要建立这个线状树
//q代表一共有几次查询
BuildTree(0,1,n);
//初始化树节点:
//以0号为root,1和n为该节点表示区间范围[L,R]
for(i=1;i<=n;i++){
scanf("%d",&h);
Insert(0,i,h);
//从0为root开始,依次将第i个数,其值为h,插入初始化的线状树中
}
//这时候创建这棵线状树完毕
for(i=0;i<q;i++){
//依次进行每次查询
int s,e;
scanf("%d%d",&s,&e);
//输入要查询的区间[s,e]
minV=INF;
maxV=-INF;
Query(0,s,e);
//从0为root开始(树根)查询s、e区间
//将查询区间的max和min存放在全局变量的maxV和minV中
//然后输出二者的差即为所求
printf("%d\n",maxV-minV);
}
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: