關於可持久化並查集的學習和思考
鑑於noip比賽前集訓時SAKER前輩教了我這個蒟蒻可持久化線段樹以來,我懂得了如何維護一個支援歷史查詢的線段樹。於是我就開始異想天開了:可不可以快速維護一個支援歷史查詢的陣列呢?
就在這時,我上網看到了一個新的演算法:可持久化並查集。先用例題來講吧:
BZOJ3674:可持久化並查集加強版
Description:
自從zkysb出了可持久化並查集後……
hzwer:亂寫能AC,暴力踩標程
KuribohG:我不路徑壓縮就過了!
ndsf:暴力就可以輕鬆虐!
zky:……
n個集合 m個操作
操作:
1 a b
合併a,b所在集合
2 k 回到第k次操作之後的狀態(查詢算作操作)
3 a b
詢問a,b是否屬於同一集合,是則輸出
請注意本題採用強制線上,所給的a,b,k均經過加密,加密方法為x
= x xor lastans,lastans的初始值為0
0<n,m<=2*10^5
題目分析:
本題要求強制線上操作,於是我們很容易想到暴力:維護m個大小為n的fa陣列,在每做完一步後都記錄一下每個點的fa,(可以使用路徑壓縮),然後該返回第k步的時候就返回那個陣列,時間為O(n*m)。
然而這樣會超時,炸空間。要在規定時間內求解我們有三種演算法:
一. 分塊(這不是本文要重點討論的演算法)
分塊演算法是一種比較常見的演算法。對於本題,我們只需要維護sqrt(m)個大小為n的fa陣列,分別記錄在完成sqrt(m),2*sqrt(m)……m步後fa陣列的狀態。然後要跳回去的時候只需要O(sqrt(m))的時間就行了,這樣可以使用路徑壓縮。時空複雜度均為O(m*sqrt(m))左右。
二. 可持久化線段樹,且不使用路徑壓縮。
我們來看一下,如果不使用路徑壓縮的話,對於每一次合併,我們只會改變fa陣列中的一個值(注意:每一次改的地方都不同),其他的值都和它們的歷史版本一樣:
也就是說,我們要快速地讓1到3,5到n快速地指向它的歷史版本。不使用路徑壓縮的可持久化並查集,本質上就是要維護一個支援單點修改和歷史版本查詢的陣列。
經過在下翻閱眾多網上大神的做法,他們基本上都開了一棵可持久化線段樹,這棵線段樹的非葉子節點i只負責維護lson和rson,不維護除此之外的任何資訊,它的作用是方便我們在修改的時候快速地將[Li,Ri]這一段O(1)地指向它的歷史版本。
我們可以使用啟發式合併,這樣找一個節點所在集合的根,就需要查詢log(n)次線段樹,而每一次時間都是log(n),故時間為O(m*log^2(n)),空間為m*log(n)。
接下來就是本人的一些思考:時間為O(m*log^2(n)),線段樹的常數又這麼大,時間和分塊沒什麼區別呀。該線段樹每一次查詢保證是log(n)的,每一次查詢只有查詢到葉子節點才能得出答案,能不能讓常數小一點呢?
我們先假設用連結串列儲存fa陣列,這樣當我們修改fa[x]的值的時候,我們需要O(x)的時間與和時間成正比的空間,如圖:
我們要修改fa[3],就要一路開新的節點,最後讓fa[3]的後繼指向4。
那麼問題很明顯了,我們要在不改變連結串列儲存方式的前提下,儘可能快速地到達任意一個節點,於是本人想到可以把這個連結串列變得“緊湊”一些,成為一棵樹:
注意,這和線段樹不同,它的每一個節點就是原連結串列中的一個元素。這樣我們要修改元素x的時候,時空均為O(floor(log(x))+1)。
那麼怎樣走才能到5號節點呢?我們發現(5)10=(101)2,故最開始是1,即為一號節點,第二位是0,故往左子樹走,接下來是1,走右子樹。我們可以發現,2的子樹轉化成二進位制位後都是10開頭的,因為2往下走一層,無非就是在2的二進位制位後面加了一個0或1,故這樣走正確性顯然。
然而這只是常數級別的優化罷了,相當於線段樹的常數變成樹狀陣列的常數而已。
三. 使用路徑壓縮。
理論時空是O(m*n*log(n)),然而實際執行只比啟發式合併慢那麼一點點……
附bzoj3674CODE(已AC):
#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;
const int maxn=200100;
const int maxl=20;
struct data
{
int lson,rson,id,val,_Size;
} tree[maxn+2*maxn*maxl];
int Root[maxn];
int cur;
int w[maxn];
int n,m,lt,nt,lans;
void Build(int x)
{
tree[x].id=x;
tree[x].val=x;
tree[x]._Size=1;
int left=x<<1;
if (left<=n)
{
tree[x].lson=left;
Build(left);
}
int right=left|1;
if (right<=n)
{
tree[x].rson=right;
Build(right);
}
}
data Query(int root,int L,int x)
{
if (x==L) return tree[root];
if (x&(1<<(w[x]-w[L]-1))) return Query(tree[root].rson,L*2+1,x);
else return Query(tree[root].lson,L*2,x);
}
data Find_fa(int x)
{
data y=Query(Root[lt],1,x);
while (y.id!=y.val) y=Query(Root[lt],1,y.val);
return y;
}
void Update(int root,int L,int x,int nid,int v)
{
if (L==x)
{
if (nid==0) tree[root].val=v;
else tree[root]._Size=v;
return;
}
if (x&(1<<(w[x]-w[L]-1)))
{
data temp=tree[ tree[root].rson ];
tree[root].rson=++cur;
tree[cur]=temp;
Update(cur,L*2+1,x,nid,v);
}
else
{
data temp=tree[ tree[root].lson ];
tree[root].lson=++cur;
tree[cur]=temp;
Update(cur,L*2,x,nid,v);
}
}
void Add(int x,int y)
{
data fx=Find_fa(x);
data fy=Find_fa(y);
if (fx.val==fy.val)
{
Root[nt]=Root[lt];
return;
}
if (fx._Size>fy._Size) swap(fx,fy);
Root[nt]=++cur;
tree[cur]=tree[ Root[lt] ];
Update(cur,1,fx.id,0,fy.id);
tree[++cur]=tree[ Root[nt] ];
Root[nt]=cur;
Update(cur,1,fy.id,1,fx._Size+fy._Size);
}
int main()
{
freopen("c.in","r",stdin);
freopen("c.out","w",stdout);
scanf("%d%d",&n,&m);
Build(1);
Root[1]=1;
cur=n;
lt=1,nt=1;
lans=0;
w[1]=1;
for (int i=2; i<maxn; i++) w[i]=w[i/2]+1;
/*for (int i=1; i<=n; i++)
{
data temp=Find_fa(i);
printf("%d ",temp.val);
}
printf("\n");*/
for (int i=1; i<=m; i++)
{
int op;
scanf("%d",&op);
if (op==1)
{
int a,b;
scanf("%d%d",&a,&b);
a=a^lans;
b=b^lans;
nt++;
Add(a,b);
lt=nt;
}
if (op==2)
{
int k;
scanf("%d",&k);
k=k^lans;
lt=k+1;
nt++;
Root[nt]=Root[lt];
lt=nt;
}
if (op==3)
{
int a,b;
scanf("%d%d",&a,&b);
a=a^lans;
b=b^lans;
//printf("%d %d\n",a,b);
data fa=Find_fa(a);
data fb=Find_fa(b);
bool f=(fa.val==fb.val);
printf("%d\n",f);
lans=f;
nt++;
Root[nt]=Root[lt];
lt=nt;
}
/*for (int i=1; i<=n; i++)
{
data temp=Find_fa(i);
printf("%d ",temp.val);
}
printf("\n");*/
}
return 0;
}