【学习笔记】高级数据结构--线状树 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】的线段树。
![](http://img.blog.csdn.net/20140719211027316?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbW93ZWl5YW5nMDIxNA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
线段树的深度不超过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
源代码:
现在给大家简单说说我的一点点理解,以及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; }
相关文章推荐
- 高级数据结构设计--并查集及实现学习笔记(有趣篇)
- 高级数据结构设计--并查集及实现学习笔记(有趣篇)
- 高级数据结构设计--并查集及实现学习笔记(有趣篇)
- 高级数据结构设计--并查集及实现学习笔记(有趣篇)
- Oracle高级培训 第7课 学习笔记
- 严蔚敏数据结构学习笔记六.树和二叉树
- C#学习笔记(二十二):使用文件高级
- Pro visual c++/cli and .net 2.0 platform2 学习笔记(10 第四章 高级C++/CLI)
- Web服务高级编程学习笔记(一)
- Oracle专家高级编程学习笔记
- Oracle专家高级编程学习笔记(一)
- Oracle专家高级编程学习笔记( 二)
- Oracle专家高级编程学习笔记( 二)
- Oracle高级培训 第5课 学习笔记
- 数据结构学习笔记之一:链表
- Oracle高级培训 第2课 学习笔记
- 一篇不错的数据结构学习笔记!
- 学习数据结构的笔记(第一章)
- C#学习笔记(二十二):使用文件高级
- Oracle高级培训 第1课 学习笔记