1. 程式人生 > >伸展樹的基本操作與應用

伸展樹的基本操作與應用

 

 

 

【總結】

由上面的分析介紹,我們可以發現伸展樹有以下幾個優點: (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