伸展树的基本操作与应用

 

【总结】

由上面的分析介绍,我们可以发现伸展树有以下几个优点: (1)时间复杂度低,伸展树的各种基本操作的平摊复杂度都是 O(log n)的。在树状数据结构中,无疑是非常优秀的。

(2)空间要求不高。与红黑树需要记录每个节点的颜色、AVL 树需要记录平衡因子不同,伸展树不需要记录任何信息以保持树的平衡。 (3)算法简单,编程容易。伸展树的基本操作都是以 Splay 操作为基础的,而Splay 操作中只需根据当前节点的位置进行旋转操作即可。

上题参考代码:

 1 /**************************************************************
 2     Problem: 1588
 3     User: SongHL
 4     Language: C++
 5     Result: Accepted
 6     Time:1284 ms
 7     Memory:2068 kb
 8 ****************************************************************/
 9  
10 #include<bits/stdc++.h>
11 const int INF=0x3f3f3f3f;
12 using namespace std;
13 int ans,n,t1,t2,rt,size;
14 int tr[50001][2],fa[50001],num[50001];
15 void rotate(int x,int &k)
16 {
17     int y=fa[x],z=fa[y],l,r;
18     if(tr[y][0]==x)l=0;else l=1;r=l^1;
19     if(y==k)k=x;
20     else{if(tr[z][0]==y)tr[z][0]=x;else tr[z][1]=x;}
21     fa[x]=z;fa[y]=x;fa[tr[x][r]]=y;
22     tr[y][l]=tr[x][r];tr[x][r]=y;
23  }
24 void splay(int x,int &k)
25 {
26     int y,z;
27     while(x!=k)
28     {
29         y=fa[x],z=fa[y];
30         if(y!=k)
31         {
32             if((tr[y][0]==x)^(tr[z][0]==y))rotate(x,k);
33             else rotate(y,k);
34         }
35         rotate(x,k);
36     }
37 }
38 void ins(int &k,int x,int last)
39 {
40      if(k==0){size++;k=size;num[k]=x;fa[k]=last;splay(k,rt);return;}
41      if(x<num[k])ins(tr[k][0],x,k);
42      else ins(tr[k][1],x,k);
43  }
44 void ask_before(int k,int x)
45 {
46      if(k==0)return;
47      if(num[k]<=x){t1=num[k];ask_before(tr[k][1],x);}
48      else ask_before(tr[k][0],x);
49  }
50 void ask_after(int k,int x)
51 {
52    if(k==0)return;
53    if(num[k]>=x){t2=num[k];ask_after(tr[k][0],x);}
54    else ask_after(tr[k][1],x);
55 }
56 int main()
57 {
58     scanf("%d",&n);
59     for(int i=1;i<=n;i++)
60     {
61         int x;if(scanf("%d",&x)==EOF) x=0;
62         t1=-INF;t2=INF;
63         ask_before(rt,x);
64         ask_after(rt,x);
65         if(i!=1)ans+=min(x-t1,t2-x);
66         else ans+=x;
67         ins(rt,x,0);
68     }
69     printf("%d",ans);
70     return 0;
71 }
View Code

 [NOI2005]维修数列(Splay的其他操作)

https://www.lydsy.com/JudgeOnline/problem.php?id=1500

算法过程:

初始化

首先,对于原序列,我们不应该一个一个读入,然后插入,那么效率就是O(nlogn),而splay的常数本身就很大,所以考虑一个优化,就是把原序列一次性读入后,直接类似线段树的build,搞一个整体建树,即不断的将当前点维护的区间进行二分,到达单元素区间后,就把对应的序列值插入进去,这样,我们一开始建的树就是一个非常平衡的树,可以使后续操作的常数更小,并且建树整个复杂度只是O(2n)的。

Insert操作

其次,我们来考虑一下如何维护一个insert操作。我们可以这么做,首先如上将需要insert的区间变成节点数目为tot的平衡树,然后把k+1(注意我们将需要操作的区间右移了一个单位,所以题目所给k就是我们需要操作的k+1)移到根节点的位置,把原树中的k+2移到根节点的右儿子的位置。然后把需要insert的区间,先build成一个平衡树,把需要insert的树的根直接挂到原树中k+1的左儿子上就行了。

Delete操作

再然后,我们来考虑一下delete操作,我们同样的,把需要delete的区间变成[k+1,k+tot](注意,是删去k后面的tot个数,那么可以发现我们需要操作的原区间是[k,k+tot-1]!),然后把k号节点移到根节点的位置,把k+tot+2移到根节点的右儿子位置,然后直接把k+tot+2的左儿子的指针清为0,就把这段区间删掉了。可以发现,比insert还简单一点。

Reverse操作

接下来,这道题的重头戏就要开始了。splay的区间操作基本原理还类似于线段树的区间操作,即延迟修改,又称打懒标记。

对于翻转(reverse)操作,我们依旧是将操作区间变成[k+1,k+tot],然后把k和k+tot+1分别移到对应根的右儿子的位置,然后对这个右儿子的左儿子打上翻转标记即可。

Make-Same操作

对于Make-Same操作,我们同样需要先将需要操作的区间变成[k+1,k+tot],然后把k和k+tot+1分别移到根和右儿子的位置,然后对这个右儿子的左儿子打上修改标记即可。

Get-Sum操作

对于Get-Sum操作,我们还是将操作区间变成[k+1,k+tot],然后把k和k+tot+1分别移到根和右儿子的位置,然后直接输出这个右儿子的左儿子上的sum记录的和。

Max-Sum操作

对于这个求最大子序列的操作,即Max-Sum操作,我们不能局限于最开始学最大子序列的线性dp方法,而是要注意刚开始,基本很多书都会介绍一个分治的O(nlogn)的方法,但是由于存在O(n)的方法,导致这个方法并不受重视,但是这个方法确实很巧妙,当数列存在修改操作时,线性的算法就不再适用了。

这种带修改的最大子序列的问题,最开始是由线段树来维护,具体来说就是,对于线段树上的每个节点所代表的区间,维护3个量:lx表示从区间左端点l开始的连续的前缀最大子序列。rx表示从区间右端点r开始的连续的后缀最大子序列。mx表示这个区间中的最大子序列。

那么在合并[l,mid]和[mid+1,r]时,就类似一个dp的过程了!其中

lx[l,r]=max(lx[l,mid],sum[l,mid]+lx[mid+1,r])lx[l,r]=max(lx[l,mid],sum[l,mid]+lx[mid+1,r])

rx[l,r]=max(rx[mid+1,r],sum[mid+1,r]+rx[l,mid])rx[l,r]=max(rx[mid+1,r],sum[mid+1,r]+rx[l,mid])

mx[l,r]=max(mx[l,mid],mx[mid+1,r],lx[mid+1,r]+rx[l,mid+1])mx[l,r]=max(mx[l,mid],mx[mid+1,r],lx[mid+1,r]+rx[l,mid+1])

这个还是很好理解的。就是选不选mid的两个决策。但是其实在实现的时候,我们并不用[l,r]的二维方式来记录这三个标记,而是用对应的节点编号来表示区间,这个可以看程序,其实是个很简单的东西。

那么最大子序列这个询问操作就可以很简单的解决了,还是类比前面的方法,就是把k和k+tot+1移到对应的根和右儿子的位置,然后直接输出右儿子的左儿子上的mx标记即可

懒标记的处理

最后,相信认真看了的童鞋会有疑问,这个标记怎么下传呢?首先,我们在每次将k和k+tot+1移到对应的位置时,需要一个类似查找k大值的find操作,即找出在平衡树中,实际编号为k在树中中序遍历的编号,这个才是我们真正需要处理的区间端点编号,那么就好了,我们只需在查找的过程中下传标记就好了!(其实线段树中也是这么做的),因为我们所有的操作都需要先find一下,所以我们可以保证才每次操作的结果计算出来时,对应的节点的标记都已经传好了。而我们在修改时,直接修改对应节点的记录标记和懒标记,因为我们的懒标记记录的都是已经对当前节点产生贡献,但是还没有当前节点的子树区间产生贡献!然后就是每处有修改的地方都要pushup一下就好了。

一些细节

另外,由于本题数据空间卡的非常紧,我们就需要用时间换空间,直接开4000000*logm的数据是不现实的,但是由于题目保证了同一时间在序列中的数字的个数最多是500000,所以我们考虑一个回收机制,把用过但是已经删掉的节点编号记录到一个队列或栈中,在新建节点时直接把队列中的冗余编号搞过来就好了。

参考代码:

  1 #include<bits/stdc++.h>
  2 #define RI register int
  3 #define For(i,a,b) for (RI i=a;i<=b;++i)
  4 using namespace std;
  5 const int inf=0x3f3f3f3f;
  6 const int N=1e6+17;
  7 int n,m,rt,cnt;
  8 int a[N],id[N],fa[N],c[N][2];
  9 int sum[N],sz[N],v[N],mx[N],lx[N],rx[N];
 10 bool tag[N],rev[N];
 11 //tag表示是否有统一修改的标记,rev表示是否有统一翻转的标记
 12 //sum表示这个点的子树中的权值和,v表示这个点的权值
 13 queue<int> q;
 14 inline int read()
 15 {
 16     RI x=0,f=1;char ch=getchar();
 17     while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();}
 18     while('0'<=ch&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
 19     return x*f;
 20 }
 21 inline void pushup(RI x)
 22 {
 23     RI l=c[x][0],r=c[x][1];
 24     sum[x]=sum[l]+sum[r]+v[x];
 25     sz[x]=sz[l]+sz[r]+1;
 26     mx[x]=max(mx[l],max(mx[r],rx[l]+v[x]+lx[r]));
 27     lx[x]=max(lx[l],sum[l]+v[x]+lx[r]);
 28     rx[x]=max(rx[r],sum[r]+v[x]+rx[l]);
 29 }
 30 //上传记录标记
 31 inline void pushdown(RI x)
 32 {
 33     RI l=c[x][0],r=c[x][1];
 34     if(tag[x])
 35     {
 36         rev[x]=tag[x]=0;//我们有了一个统一修改的标记,再翻转就没有什么意义了
 37         if(l) tag[l]=1,v[l]=v[x],sum[l]=v[x]*sz[l];
 38         if(r) tag[r]=1,v[r]=v[x],sum[r]=v[x]*sz[r];
 39         if(v[x]>=0) 
 40         {
 41             if(l) lx[l]=rx[l]=mx[l]=sum[l];
 42             if(r) lx[r]=rx[r]=mx[r]=sum[r];
 43         }
 44         else
 45         {
 46             if(l) lx[l]=rx[l]=0,mx[l]=v[x];
 47             if(r) lx[r]=rx[r]=0,mx[r]=v[x];
 48         }
 49     }
 50     if(rev[x])
 51     {
 52         rev[x]=0;rev[l]^=1;rev[r]^=1;
 53         swap(lx[l],rx[l]);swap(lx[r],rx[r]);
 54         //注意,在翻转操作中,前后缀的最长上升子序列都反过来了,很容易错
 55         swap(c[l][0],c[l][1]);swap(c[r][0],c[r][1]);
 56     }
 57 }
 58 inline void rotate(RI x,RI &k)
 59 {
 60     RI y=fa[x],z=fa[y],l=(c[y][1]==x),r=l^1;
 61     if (y==k)k=x;else c[z][c[z][1]==y]=x;
 62     fa[c[x][r]]=y;fa[y]=x;fa[x]=z;
 63     c[y][l]=c[x][r];c[x][r]=y;
 64     pushup(y);pushup(x);
 65     //旋转操作,一定要上传标记且顺序不能变 
 66 }
 67 inline void splay(RI x,RI &k)
 68 {
 69     while(x!=k)
 70     {
 71         int y=fa[x],z=fa[y];
 72         if(y!=k)
 73         {
 74             if((c[z][0]==y)^(c[y][0]==x)) rotate(x,k);
 75             else rotate(y,k);
 76         }
 77         rotate(x,k);
 78     }
 79 }
 80 //这是整个程序的核心之一,毕竟是伸展操作嘛
 81 inline int find(RI x,RI rk)
 82 {//返回当前序列第rk个数的标号 
 83     pushdown(x);
 84     RI l=c[x][0],r=c[x][1];
 85     if(sz[l]+1==rk) return x;
 86     if(sz[l]>=rk) return find(l,rk);
 87     else return find(r,rk-sz[l]-1);
 88 }
 89 inline void recycle(RI x)
 90 {//这就是用时间换空间的回收冗余编号机制,很好理解
 91     RI &l=c[x][0],&r=c[x][1];
 92     if(l) recycle(l);
 93     if(r) recycle(r);
 94     q.push(x);
 95     fa[x]=l=r=tag[x]=rev[x]=0;
 96 }
 97 inline int split(RI k,RI tot)//找到[k+1,k+tot]
 98 {
 99     RI x=find(rt,k),y=find(rt,k+tot+1);
100     splay(x,rt);splay(y,c[x][1]);
101     return c[y][0];
102 }
103 //这个split操作是整个程序的核心之三
104 //我们通过这个split操作,找到[k+1,k+tot],并把k,和k+tot+1移到根和右儿子的位置
105 //然后我们返回了这个右儿子的左儿子,这就是我们需要操作的区间
106 inline void query(RI k,RI tot)
107 {
108     RI x=split(k,tot);
109     printf("%d\n",sum[x]);
110 }
111 inline void modify(RI k,RI tot,RI val)//MAKE-SAME
112 {
113     RI x=split(k,tot),y=fa[x];
114     v[x]=val;tag[x]=1;sum[x]=sz[x]*val;
115     if(val>=0) lx[x]=rx[x]=mx[x]=sum[x];
116         else lx[x]=rx[x]=0,mx[x]=val;
117     pushup(y);pushup(fa[y]);
118     //每一步的修改操作,由于父子关系发生改变
119     //及记录标记发生改变,我们需要及时上传记录标记
120 }
121 inline void rever(RI k,RI tot)//翻转 
122 {
123     RI x=split(k,tot),y=fa[x];
124     if(!tag[x])
125     {
126         rev[x]^=1;
127         swap(c[x][0],c[x][1]);
128         swap(lx[x],rx[x]);
129         pushup(y);pushup(fa[y]);
130     }
131     //同上
132 }
133 inline void erase(RI k,RI tot)//DELETE
134 {
135     RI x=split(k,tot),y=fa[x];
136     recycle(x);c[y][0]=0;
137     pushup(y);pushup(fa[y]);
138     //同上
139 }
140 inline void build(RI l,RI r,RI f)
141 {
142     RI mid=(l+r)>>1,now=id[mid],pre=id[f];
143     if(l==r)
144     {
145         mx[now]=sum[now]=a[l];
146         tag[now]=rev[now]=0;
147         //这里这个tag和rev的清0是必要,因为这个编号可能是之前冗余了
148         lx[now]=rx[now]=max(a[l],0);
149         sz[now]=1;
150     }
151     if(l<mid) build(l,mid-1,mid);
152     if(mid<r) build(mid+1,r,mid);
153     v[now]=a[mid]; fa[now]=pre;
154     pushup(now); //上传记录标记
155     c[pre][mid>=f]=now;
156     //当mid>=f时,now是插入到又区间取了,所以c[pre][1]=now,当mid<f时同理
157 }
158 inline void insert(RI k,RI tot)
159 {
160     for(int i=1;i<=tot;++i) a[i]=read();
161     for(int i=1;i<=tot;++i)
162     {
163         if(!q.empty()) id[i]=q.front(),q.pop();
164         else id[i]=++cnt;//利用队列中记录的冗余节点编号
165     }
166     build(1,tot,0);
167     RI z=id[(1+tot)>>1];
168     RI x=find(rt,k+1),y=find(rt,k+2);
169      //首先,依据中序遍历,找到我们需要操作的区间的实际编号
170     splay(x,rt);splay(y,c[x][1]);
171     //把k+1(注意我们已经右移了一个单位)和(k+1)+1移到根和右儿子
172     fa[z]=y;c[y][0]=z;
173     //直接把需要插入的这个平衡树挂到右儿子的左儿子上去就好了
174     pushup(y);pushup(x);
175     //上传记录标记
176 }
177 //可以这么记,只要用了split就要重新上传标记
178 //只有find中需要下传标记
179 int main()
180 {
181     n=read(),m=read();
182     mx[0]=a[1]=a[n+2]=-inf;
183     For(i,1,n) a[i+1]=read();
184     For(i,1,n+2) id[i]=i;//虚拟了两个节点1和n+2,然后把需要操作区间整体右移一个单位
185     build(1,n+2,0);//建树
186     rt=(n+3)>>1;cnt=n+2;//取最中间的为根
187     RI k,tot,val;char ch[10];
188     while(m--)
189     {
190         scanf("%s",ch);
191         if(ch[0]!='M' || ch[2]!='X') k=read(),tot=read();
192         if(ch[0]=='I') insert(k,tot);
193         if(ch[0]=='D') erase(k,tot);//DELETE
194         if(ch[0]=='M')
195         {
196             if(ch[2]=='X') printf("%d\n",mx[rt]);//MAX-SUM
197             else val=read(),modify(k,tot,val);//MAKE-SAME
198         }
199         if(ch[0]=='R') rever(k,tot);//翻转 
200         if(ch[0]=='G') query(k,tot);//GET-SUM
201     }
202     return 0;
203 }
204 
View Code

猜你喜欢

转载自www.cnblogs.com/songorz/p/10122047.html