伸展树应用初步——解决区间问题
2014-05-09 21:44
357 查看
伸展树的基本操作就是伸展,也就是将指定节点旋转至树根(同时不改变排序二叉树的性质)。在这个操作的基础上,配合节点中保存额外的数据域,伸展树可以完成多种任务,包括各种区间问题。
伸展树的节点除了保存必要的指针信息和键值对之外,经常使用的额外的数据域包括size域、sum域、极值域等等。size域用于记录该节点所代表的子树的节点总数,可以用于解决区间kth数问题;sum域用于记录该节点所代表子树的所有节点的数值之和,可以解决区间和问题;极值域用于记录该子树所有节点的极值,可以解决RMQ问题。而且伸展树能够解决问题的范围非常广。不但可以解决静态区间问题,数值单点修改的区间问题,数值成段修改的区间问题(这需要延迟标记,本文暂不讨论),而且可以解决区间本身发生变动时问题。相比之下,ST算法只能解决静态RMQ,树状数组可以解决单点修改的区间和问题,线段树还能解决成段修改的区间问题,但是它们都不如伸展树覆盖的范围大,都不能解决最后一类问题。当然,伸展树适用范围虽然广,但是在解决特定问题时效率比较低,除非是特定的访问模式。
此外,伸展树还是其他高级算法和结构的基础,例如树链剖分和link-cut tree。所以其一伸展树是非常有用的结构,其二伸展树一招鲜是不行的,树状数组、线段树也都少不了。
伸展树中加入额外域的核心问题就是维护问题,也就是如何在伸展过程中维护额外域的正确性。首先需要加入一个操作称之为pushUp(t)函数。该函数的涵义是利用t及其子节点计算t的额外域(无论是size、sum还是极值,这一点都非常容易完成)。而本文以极值域解决单点修改RMQ问题为例进行说明,至于其他类型的域只有少许细节不同。
令旋转的节点为t,其父节点为P,无论左旋还是右旋,只有这两个节点的极值域会发生变化,其他相关节点均不变。所以在旋转操作中需要加入维护P和t的操作。
但是旋转操作从来只会在伸展操作中调用,绝不会被其他东西调用。而伸展操作将会重复旋转t,这也就意味着t将被维护多次,而实际上是不需要的。因为在伸展过程中,t的额外信息如果不对,对上对下均不会造成不利影响。所以只需在伸展结束之后把t维护一次即可,而旋转操作中只维护P即可。
查询操作完成很简单,假设要区间[s, e]的极值,则将节点s-1伸展成树根,再将节点e+1伸展为根节点的右儿子,那么节点e+1的左子树就代表了区间[s, e],节点e+1的左儿子的极值域就是结果。此处必须保证节点s-1和e+1存在,这是一个很简单的技巧,很容易实现。
单点修改操作也很容易完成,假设要修改的键值为key,则将key所在的节点旋转至树根,修改数值域,再更新这一个节点的极值域即可。
另外,初始时n个节点的伸展树的建树操作可以直接递归实现,而不必一个节点一个节点的插入,后者显然效率极低。
下列源代码是hdu1754的AC代码,典型的单点修改RMQ应使用线段树解决。此处故意使用伸展树解决(毫无疑问的,如无意外,伸展树的运行时间比线段树长)。
伸展树的节点除了保存必要的指针信息和键值对之外,经常使用的额外的数据域包括size域、sum域、极值域等等。size域用于记录该节点所代表的子树的节点总数,可以用于解决区间kth数问题;sum域用于记录该节点所代表子树的所有节点的数值之和,可以解决区间和问题;极值域用于记录该子树所有节点的极值,可以解决RMQ问题。而且伸展树能够解决问题的范围非常广。不但可以解决静态区间问题,数值单点修改的区间问题,数值成段修改的区间问题(这需要延迟标记,本文暂不讨论),而且可以解决区间本身发生变动时问题。相比之下,ST算法只能解决静态RMQ,树状数组可以解决单点修改的区间和问题,线段树还能解决成段修改的区间问题,但是它们都不如伸展树覆盖的范围大,都不能解决最后一类问题。当然,伸展树适用范围虽然广,但是在解决特定问题时效率比较低,除非是特定的访问模式。
此外,伸展树还是其他高级算法和结构的基础,例如树链剖分和link-cut tree。所以其一伸展树是非常有用的结构,其二伸展树一招鲜是不行的,树状数组、线段树也都少不了。
伸展树中加入额外域的核心问题就是维护问题,也就是如何在伸展过程中维护额外域的正确性。首先需要加入一个操作称之为pushUp(t)函数。该函数的涵义是利用t及其子节点计算t的额外域(无论是size、sum还是极值,这一点都非常容易完成)。而本文以极值域解决单点修改RMQ问题为例进行说明,至于其他类型的域只有少许细节不同。
令旋转的节点为t,其父节点为P,无论左旋还是右旋,只有这两个节点的极值域会发生变化,其他相关节点均不变。所以在旋转操作中需要加入维护P和t的操作。
但是旋转操作从来只会在伸展操作中调用,绝不会被其他东西调用。而伸展操作将会重复旋转t,这也就意味着t将被维护多次,而实际上是不需要的。因为在伸展过程中,t的额外信息如果不对,对上对下均不会造成不利影响。所以只需在伸展结束之后把t维护一次即可,而旋转操作中只维护P即可。
查询操作完成很简单,假设要区间[s, e]的极值,则将节点s-1伸展成树根,再将节点e+1伸展为根节点的右儿子,那么节点e+1的左子树就代表了区间[s, e],节点e+1的左儿子的极值域就是结果。此处必须保证节点s-1和e+1存在,这是一个很简单的技巧,很容易实现。
单点修改操作也很容易完成,假设要修改的键值为key,则将key所在的节点旋转至树根,修改数值域,再更新这一个节点的极值域即可。
另外,初始时n个节点的伸展树的建树操作可以直接递归实现,而不必一个节点一个节点的插入,后者显然效率极低。
下列源代码是hdu1754的AC代码,典型的单点修改RMQ应使用线段树解决。此处故意使用伸展树解决(毫无疑问的,如无意外,伸展树的运行时间比线段树长)。
//RMQ with Splay Tree #include <cstdio> #include <cstring> #define SIZE 200003 #define LEFT 0 #define RIGHT 1 typedef int key_t; typedef int data_t; struct _t{ int parent; int child[2]; int sn; key_t key; data_t data; data_t peak; }Node[SIZE]; int toUsed = 0; int Root = 0; //初始化 inline void init(){ toUsed = Root = 0; } //信息上传,指根据儿子节点计算父节点的附加值,此处即区间最大值 inline void _pushUp(int t){ Node[t].peak = Node[t].data; int son = Node[t].child[LEFT]; if ( son && Node[t].peak < Node[son].peak ) Node[t].peak = Node[son].peak; son = Node[t].child[RIGHT]; if ( son && Node[t].peak < Node[son].peak ) Node[t].peak = Node[son].peak; } //分配一个新节点 inline int _newNode(){ ++toUsed; memset(Node+toUsed,0,sizeof(_t)); return toUsed; } //link操作 inline void _link(int p,int sn,int t){ Node[p].child[sn] = t; if ( t ) Node[t].parent = p, Node[t].sn = sn; } //旋转操作,t非根节点 inline void _rotate(int t){ int p = Node[t].parent; int sn = Node[t].sn; int osn = sn ^ 1; //确定三对父子关系 _link(p,sn,Node[t].child[osn]); _link(Node[p].parent,Node[p].sn,t); _link(t,osn,p); //只维持p,t在伸展的最后维持 _pushUp(p); } //将t伸展至p下,p为0则伸展至树根 void _splay(int t,int p,int& root){ while( Node[t].parent != p ){ int pp = Node[t].parent; if ( Node[pp].parent != p ) Node[pp].sn == Node[t].sn ? _rotate(pp) : _rotate(t); _rotate(t); } _pushUp(t); if ( 0 == p ) root = t; } //在root树上查找键值为key的节点,其父亲放在参数3中返回 int _advance(int root,key_t key,int&parent){ if ( 0 == root ) return parent = 0; parent = 0; int t = root; while( t && Node[t].key != key ){ parent = t; t = key < Node[t].key ? Node[t].child[LEFT] : Node[t].child[RIGHT]; } return t; } //递归建树,因为key值是连续的 void build(int&t,int parent,int sn,key_t s,key_t e,data_t const a[]){ key_t mid = ( s + e ) >> 1; t = _newNode(); Node[t].parent = parent; Node[t].data = a[mid]; Node[t].key = mid; Node[t].sn = sn; if ( mid > s ) build(Node[t].child[LEFT],t,LEFT,s,mid-1,a); if ( mid < e ) build(Node[t].child[RIGHT],t,RIGHT,mid+1,e,a); _pushUp(t); } //查询操作,保证s-1和e+1存在 int query(int& root,key_t s,key_t e){ //将s-1伸展至树根 int p; int t = _advance(root,s-1,p); _splay(t,0,root); //将e+1变成树根的右儿子 t = _advance(root,e+1,p); _splay(t,root,root); return Node[Node[t].child[LEFT]].peak; } //将键值为key的节点的数据域修改为v void modify(int &root,key_t key,data_t v){ //将key节点伸展至树根 int p; int t = _advance(root,key,p); _splay(t,0,root); //修改 Node[t].data = v; if ( Node[t].peak < v ) Node[t].peak = v; } int N,M; int A[SIZE] = {0}; bool read(){ if ( EOF == scanf("%d%d",&N,&M) ) return false; for(int i=1;i<=N;++i)scanf("%d",A+i); A[N+1] = 0; return true; } int main(){ while ( read() ){ init(); build(Root,0,0,0,N+1,A); while(M--){ char cmd[3]; int a,b; scanf("%s%d%d",cmd,&a,&b); if ( 'Q' == *cmd ){ printf("%d\n",query(Root,a,b)); }else{ modify(Root,a,b); } } } return 0; }
相关文章推荐
- JSP应用解决中文乱码问题(初步解决)
- 伸展树解决区间问题
- shareSDK的初步使用(shareSDK中微信、qq等兼容问题,以及cocoapods支持架构冲突问题的解决)
- 检测和解决Android应用的性能问题
- redis在应用中使用连接不释放问题解决
- 应用主题后FCKeditor上传问题的解决及相应的改进
- 灵活应用js调试技巧解决样式问题的步骤分享
- taskctl能解决的问题和应用场景
- 怎样解决“在禁用UAC时,无法激活此应用”问题
- CLAPACK安装问题解决及应用举例
- Filter(四)常见应用一----解决乱码问题
- 用它解决大问题啦,STRACE应用
- 在编程的世界中,如何高效地学习理论知识,应用理论知识来解决实际生产中的问题
- C语言基础知识应用问题解决!
- UNIX 共享内存应用中的问题及解决方法
- HihoCoder 1079(线段树,改变递归区间解决问题)
- PKI/CA解决什么问题?例举几个典型的应用?
- [原]用Eclipse开发Android应用,用svn管理源码时遇到的问题及解决方法
- cdq分治解决区间问题
- Android 使用android-support-multidex解决Dex超出方法数的限制问题,让你的应用不再爆棚