zoukankan      html  css  js  c++  java
  • ●小集训之旅 二

    有志者自有千计万计,无志者只感千难万难。

    ●2017.3.28-29

    学习内容:伸展树 Splay Tree

    引:二叉查找树(Binary Search Tree) 可以被用来表示有序集合、建立索引或优先队列等。最坏情况下,作用于二叉查找树上的基本操作的时间复杂度,可能达到O(n)。

    ●伸展树(Splay Tree)是二叉查找树的改进。

    优点:对伸展树的操作的平摊复杂度是O(log2n)。伸展树的空间要求较低。

    在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的操作,为了使整个操作时间更小,被操作频率高的那些节点(子树)就应当经常处于靠近树根的位置。于是在每次操作之后对树进行重构,把被操作的节点(子树)搬移到离树根近一些的地方(并保证不破坏二叉树中各节点之间的关系)。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。

    (我们的Y老师曰:这是一种“玄学”算法,学了以后我感觉很有道理。)

    (我们的Y老师还曰:Splay Tree是“区间王”,学了以后我也感觉很有道理。)

    算法相关内容(基础与支持操作)

      • 单旋与双旋 rotate()(通过旋转操作使得x成为root)
        • 单旋:在操作完位于节点x之后,对x进行旋转操作,使得x的父亲节点成为x的儿子节点 下面是两种情况:(x的父亲节点y是root,即fa[x]==y==root)
    • 单旋图 
      • 所以:
    • 单旋图结论
      • 双旋:当x的父节点y的父节点z是根时,即fa[ fa[x] ]==z==root,则为了将x变为root,要进行两次旋转,那么便要分为两种情况来操作:(初学者当结论记吧,Y老师说是“玄学”)
        • 同侧情况(即 x和y都为其父亲的左儿子或右儿子)

                               则先旋转y,再旋转x:(右旋—右旋 or 左旋—左旋)

        • 双旋一图(直接由上面的两次单旋构成,每次关系变化的边只有两条,只是每次对象不同)
        • 异侧情况(即x和y分别为其父亲的左儿子和右儿子)

                              则对x进行两次旋转:(左旋—右旋 or 右旋—左旋)

          • 双旋二图(也是由上面的两次单旋过程,且每次的对象相同,只是第一次关系变化的边有三条。第二次关系变化的边任只有两条。
          • 有了上面的单旋和双旋的操作,便可把任意位置的x点移动到root的位置 (如何移动见下文; 为什么双旋要这样旋,额,我还不太明白具体原因,我先当结论记吧,Y老师说是“玄学”,不好证明。)
        • Splay()函数(“伸展运动”)
          • 该函数的目的便是通过调用rotate()旋转函数,实现把x旋转到root位置(~也可以到其他位置)代码如下(学习hzwer的)
    void rotate(int x,int &k)											//旋转(单) 
    {
    	int y=fa[x],z=fa[y];
    	int l=(x!=c[y][0]),r=l^1;
    	if(y==k) k=x;
    	else c[z][y!=c[z][0]]=x;
    	fa[x]=z; fa[y]=x; fa[c[x][r]]=y;
    	c[y][l]=c[x][r]; c[x][r]=y;
    	update(y); update(x);
    }
    
    void splay(int x,int &k)		//伸展运动 ,e 
    {
    	int y,z;
    	while(x!=k)
    	{
    		y=fa[x],z=fa[y];
    		if(y!=k)
    		{
    			if(c[y][0]==x^c[z][0]==y) rotate(x,k);
    			else rotate(y,k);
    		}
    		rotate(x,k);
    	}
    }
          • 有了以上的两个函数,那么伸展树基础便有了;下面说说其为“区间王”的原由
        • Split()函数
          • 看图:
          • Split图
          • 有了这一函数,需要操作的区间便被单独放入了root的右儿子的左儿子所在的子树中,我们就可以对该子树的根进行操作,添加lazy标记之类的,就可以解决很多区间问题。(比线段树更强)
        • find()函数
          • 上图中,我们把区间形成一棵单独的子树的前提是要找到区间两侧的外端点(的编号)才能对其进行Splay()操作,移到根节点或根的右儿子节点。
          • 如何在树中找到我们要的点(的编号)呢(即原序列中的第几个点),代码如下,(不难。)
    int find(int k,int x)//找树中的目标点 
    {
    	pushdown(k);
    	if(siz[c[k][0]]+1==x) return k; 
    	if(siz[c[k][0]]>=x) return find(c[k][0],x);
    	else return find(c[k][1],x-siz[c[k][0]]-1);
    }
    int split(int k,int len)//裂(把需要的区间弄到根的右儿子的左儿子所在的子树上c[c[rt][1]][0]) 
    {					                        
    	int x=find(rt,k),y=find(rt,k+len+1);
    	splay(x,rt);splay(y,c[x][1]);
    	return c[y][0];
    }
          • (Ps:代码中的pushdown()是为了将lazy标记传下去。)
        • 其它函数(build(),insert(),rever(),erase(),rec(),modify()……)
          • 有了之前的那些函数,结合lazy标记,那么区间操作就方便极了
          • (只是要注意在区间操作时的,每次树的形态改变之前,一定要把lazy标记传下去)
        • 下面以一个题来”奉上“众多函数
          • (NOI 2005 维修数列(bzoj 1500 ))
          • 题图:
          • NOI 2005 维修数列
          • 此题涵盖了不少函数,是一个很好的”裸题”,直接上代码了;(学习hzwer的
    #include<queue>
    #include<cmath>
    #include<cstdio>
    #include<cstring>
    #include<cstdlib>
    #include<iostream>
    #include<algorithm>
    #define inf 1000000000
    #define N 1000005
    using namespace std;
    int read()//读入优化 
    {
    	int x=0,f=1;char ch=getchar();
    	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    	return x*f;
    }
    int n,m,rt,cnt,k,len,val; char ch[10];
    int a[N],id[N],fa[N],c[N][2];
    int sum[N],siz[N],v[N],mx[N],lx[N],rx[N];
    bool tag[N],rev[N];
    queue<int> q;
    void update(int x)                                            
    {
    	int l=c[x][0],r=c[x][1];
    	sum[x]=sum[l]+sum[r]+v[x];
    	siz[x]=siz[l]+siz[r]+1;
    	mx[x]=max(mx[l],mx[r]);
    	mx[x]=max(mx[x],rx[l]+v[x]+lx[r]);
    	lx[x]=max(lx[l],sum[l]+v[x]+lx[r]);
    	rx[x]=max(rx[r],sum[r]+v[x]+rx[l]);
    }
    void pushdown(int x)                                          
    {
    	int l=c[x][0],r=c[x][1];
    	if(tag[x])
    	{
    		rev[x]=tag[x]=0;                         //有tag 就不 rev 
    		if(l) tag[l]=1,v[l]=v[x],sum[l]=v[l]*siz[l];
    		if(r) tag[r]=1,v[r]=v[x],sum[r]=v[r]*siz[r];
    		if(v[x]>=0)
    		{
    			if(l) lx[l]=rx[l]=mx[l]=sum[l];
    			if(r) lx[r]=rx[r]=mx[r]=sum[r];
    		}
    		else 
    		{
    			if(l)lx[l]=rx[l]=0,mx[l]=v[x];
    			if(r)lx[r]=rx[r]=0,mx[r]=v[x];
    		}
    	}
    	if(rev[x])
    	{
    		rev[x]^=1;rev[l]^=1;rev[r]^=1;          //(^ 的妙处 )
     		swap(lx[l],rx[l]);swap(lx[r],rx[r]); 
    		swap(c[l][0],c[l][1]);swap(c[r][0],c[r][1]);
    	}
    }
    void rotate(int x,int &k)//旋转(单) 
    {
    	int y=fa[x],z=fa[y];
    	int l=(x!=c[y][0]),r=l^1;
    	if(y==k) k=x;
    	else c[z][y!=c[z][0]]=x;
    	fa[x]=z; fa[y]=x; fa[c[x][r]]=y;
    	c[y][l]=c[x][r]; c[x][r]=y;
    	update(y); update(x);
    }
    void splay(int x,int &k)//伸展运动 ,e 
    {
    	int y,z;
    	while(x!=k)
    	{
    		y=fa[x],z=fa[y];
    		if(y!=k)
    		{
    			if(c[y][0]==x^c[z][0]==y) rotate(x,k);
    			else rotate(y,k);
    		}
    		rotate(x,k);
    	}
    }
    void build(int l,int r,int f)//建树 
    {
    	if(l>r) return;
    	int mid=l+r>>1,now=id[mid],last=id[f];
    	if(l==r)
    	{
    		sum[now]=a[l];
    		siz[now]=1;
    		tag[now]=rev[now]=0;
    		if(a[l]>=0)lx[now]=rx[now]=mx[now]=a[l];
    		else lx[now]=rx[now]=0,mx[now]=a[l];
    	}
    	build(l,mid-1,mid);build(mid+1,r,mid);
    	v[now]=a[mid];
    	fa[now]=last;
    	c[last][mid>=f]=now;
    	update(now);
    }
    int find(int k,int x)//找树中的目标点 
    {
    	pushdown(k);
    	if(siz[c[k][0]]+1==x) return k; 
    	if(siz[c[k][0]]>=x) return find(c[k][0],x);
    	else return find(c[k][1],x-siz[c[k][0]]-1);
    }
    int split(int k,int len)//裂(把需要的区间弄到根的右儿子的左儿子所在的子树上c[c[rt][1]][0]) 
    {																									
    	int x=find(rt,k),y=find(rt,k+len+1);
    	splay(x,rt);splay(y,c[x][1]);
    	return c[y][0];
    }
    void insert(int k,int len)//插入 
    {
    	for(int i=1;i<=len;i++)
    	if(!q.empty()) id[i]=q.front(),q.pop();
    	else id[i]=++cnt;
    	for(int i=1;i<=len;i++) a[i]=read();
    	build(1,len,0);
    	int x=id[1+len>>1];
    	int z=find(rt,k+1),y=find(rt,k+2);//第一位为-inf 
    	splay(z,rt); splay(y,c[z][1]);
    	fa[x]=y; c[y][0]=x;
    	update(y);update(fa[y]);
    }
    void rec(int x)//删除时“回收空间”	(把不要的点的编号放进队列,下次要加新点时,直接用队列里的编号) 
    {															
    	if(!x) return;
    	int l=c[x][0],r=c[x][1];
    	rec(l); rec(r);
    	q.push(x);
    	fa[x]=c[x][0]=c[x][1]=0;
    	tag[x]=rev[x]=0;
    }
    void erase(int k,int len)//删除区间 
    {
    	int x=split(k,len),y=fa[x];
    	rec(x); c[y][0]=0;
    	update(y);update(fa[y]);
    }
    void query(int k,int len)//询问区间和 
    {
    	int x=split(k,len);
    	printf("%d
    ",sum[x]);
    }
    void rever(int k,int len)//区间翻转 
    {
    	int x=split(k,len),y=fa[x];
    	if(!tag[x])
    	{
    		rev[x]^=1;						// ^ 的妙处 
    		swap(lx[x],rx[x]);
    		swap(c[x][0],c[x][1]);
    		update(y);update(fa[y]);
    	}
    }
    void modify(int k,int len,int val)//区间修改 
    {
    	int x=split(k,len),y=fa[x];
    	tag[x]=1; v[x]=val;
    	sum[x]=v[x]*siz[x];
    	if(v[x]>=0) lx[x]=rx[x]=mx[x]=sum[x];
    	else lx[x]=rx[x]=0,mx[x]=v[x];
    	update(y);update(fa[y]);
    }
    int main()
    {
    	n=read();m=read();
    	mx[0]=a[1]=a[n+2]=-inf; id[1]=1; id[n+2]=n+2;
    	for(int i=2;i<=n+1;i++) a[i]=read(),id[i]=i;
    	build(1,n+2,0);
    	rt=n+3>>1; cnt=n+2;
    	while(m-->0)
    	{
    		scanf("%s",ch);
    		if(ch[0]!='M'||ch[2]!='X') k=read(),len=read();
    		if(ch[0]=='I') insert(k,len);
    		if(ch[0]=='D') erase(k,len);
    		if(ch[0]=='R') rever(k,len);
    		if(ch[0]=='G') query(k,len);
    		if(ch[0]=='M')
    		{
    			if(ch[2]=='X')printf("%d
    ",mx[rt]);
    			else val=read(),modify(k,len,val);
    		}
    	}
    	return 0;
    }
    /* 
    	●需要 update() 的地方:                                  
    	  build(), rotate(), modify(), rever(), erase(), insert();
    	●需要 pushdown() 的地方:
    	  程序中似乎只有 find() 中调用了pushdown(),
    	  但众多操作中都通过  split()-->find()-->pushdown()来间接调用了pushdown()
    	  ○调用pushdown()的原则:在树的形态发生变化前要把lazy标记传下去;
    	    (split()中会调用splay(),使树的形态发生改变,所以split()中要先通过find()把lazy标记传下去) 
    	 */

    ●总结:该算法的区间操作能力强大,时间空间也都比较优秀,无愧于“区间王”,但仍然有小小一点缺陷:1.常数过大,容易被卡。2.代码长,函数多,容易打错,所以要多多练习,把这些函数打熟练。



    Do not go gentle into that good night.
    Rage, rage against the dying of the light.
    ————Dylan Thomas
  • 相关阅读:
    windows C++ 网络编程
    typedef void (*Fun) (void) 的理解——函数指针——typedef函数指针
    获取屏幕分辨率(C/C++)
    随手记
    【SQLite】可视化工具SQLite studio
    C++宽窄字符串转换
    并发编程1——一个简单的多线程程序
    css中:after和:before的作用及使用方法
    uniapp—App—自定义导航栏
    vue—封装无数据时默认展示组件
  • 原文地址:https://www.cnblogs.com/zj75211/p/6640147.html
Copyright © 2011-2022 走看看