可持久化並查集
前言
- 聽名字像是一個十分高端的東西,在今年NOI2018之前,我從未想過自己會用這個數據結構
- 然而,當發現Day1 T1用可持久化並查集可以暴力A的時候,心中無盡的無奈......
(畢竟不會) - 考完后了解了一下,發現似乎是一個挺好理解的數據結構。
- 所以就寫了這篇學習筆記!
前置技能
- 可持久化並查集,所需要知道的前置技能很顯然!
- 顧名思義,可持久化並查集=可持久化+並查集=可持久化數組+並查集=主席樹+並查集!
- 因此,我們首先要會主席樹和並查集。
- 可持久化數組這個沒什么好說的,就那幾個操作,詳情見洛谷可持久化數組模板
- 並查集倒是要提一下!
- 並查集中有幾種合並方式:
- 一種是直接暴力連父親
(這顯然用不上); - 一種是路徑壓縮的合並(這個在普通並查集中很常用,但是好像無法在可持久化並查集中用,聽說是可以構造數據使可持久化並查集的空間爆掉?);
- 還有一種是按秩合並,也就是可持久化並查集中常用的合並方式!其實也就是一種類似於啟發式合並的方式,每一次合並時選擇一個深度小的點向深度大的合並。這樣就可以保證並查集的高度不會增長的太快,保證高度盡量均衡。
步入正題——可持久化並查集
- 其實我們可以發現看懂了前置技能后,可持久化並查集已經不難實現。
- 可持久化並查集其實就是指的用可持久化數組維護並查集中的\(Fa\)與按秩合並所需要的\(dep\)
- 所謂可持久化並查集,可以進行的操作就只有幾個:
- 回到歷史版本
(不然怎么叫可持久化呢2333) - 合並兩個集合
(畢竟還是個並查集么) - 查詢節點所在集合的祖先,當然,因此也可以判斷是否在同一個集合中!
- 對於1操作,我們可以很輕松的利用可持久化數組實現。就直接把當前版本的根節點定為第k個版本的根節點就行了!
- 至於代碼實現?
root[i]=root[x];
//是不是很簡單呀!
- 對於2操作,其實也就是按照我在前置技能中所說的按秩合並!
- 對於3操作,也就是在可持久化數組中查詢!
- 這樣說肯定會有點懵圈,不如一個個函數的解釋!
#define Mid ((l+r)>>1)
#define lson L[rt],l,Mid
#define rson R[rt],Mid+1,r
// 整個代碼的三個宏定義
初始化建樹
void build(int &rt,int l,int r)
{
rt=++cnt;
if(l==r){fa[rt]=l;return ;}
build(lson);build(rson);
}
// 就是普通的可持久化數組構建法,不過維護的是Fa而已
合並
void merge(int last,int &rt,int l,int r,int pos,int Fa)
{
rt=++cnt;L[rt]=L[last],R[rt]=R[last];
if(l==r)
{
fa[rt]=Fa;
dep[rt]=dep[last];//繼承上個版本的值
return ;
}
if(pos<=Mid)merge(L[last],lson,pos,Fa);
else merge(R[last],rson,pos,Fa);
}
// 這個就是單純的將一個點合並到另一個點上的可持久化數組操作!
修改節點深度(方便按秩合並)
void update(int rt,int l,int r,int pos)
{
if(l==r){dep[rt]++;return ;}
if(pos<=Mid)update(lson,pos);
else update(rson,pos);
}
// 可持久化數組普通操作
// 可能有人會問為什么修改節點深度的時候不需要新開節點!
// 其實新開節點是根據我們的需要來的!
// 如果我們需要某個值在某個版本的信息,那么,每當這個值進行修改的時候,我們都需要新添加一個節點,使得我們可以查到各個版本的值
// 然而dep我們並不需要知道它以前的值是多少,我們只需要用它當前的值去合並就行了!
查詢某一個值所在可持久化數組中的下標
int query(int rt,int l,int r,int pos)
{
if(l==r)return rt;
if(pos<=Mid)return query(lson,pos);
else return query(rson,pos);
}
// 為了找祖先的操作
查找祖先
int find(int rt,int pos)
{
int now=query(rt,1,n,pos);
if(fa[now]==pos)return now;
return find(rt,fa[now]);
}
// 暴力找祖先
- 以上操作就是可持久化並查集的基礎函數 單次操作復雜度均為\(log\)級的,空間需要注意,也要開\(n*log\)級,一般就正常空間乘上\(40\)左右吧。
- 合並與查詢操作就和普通並查集差不多,只是需要注意當前查詢的版本是什么就可以了。
- 當然還需要注意一點,每一次操作都要先把上個版本給傳遞過來\(root[i]=root[i-1]\)
- 放個代碼看看吧!
- 按秩合並
posx=find(root[i],x);posy=find(root[i],y);
if(fa[posx]!=fa[posy])
{
if(dep[posx]>dep[posy])swap(posx,posy);
merge(root[i-1],root[i],1,n,fa[posx],fa[posy]);
if(dep[posx]==dep[posy])update(root[i],1,n,fa[posy]);
// 因為不可能出現深度相同的兩個點,所以要把其中一個點深度+1,由於是深度小的合到深度大的上,所以把深度小的增加深度
}
- 查找
posx=find(root[i],x);posy=find(root[i],y);
if(fa[posx]==fa[posy])puts("1");
else puts("0");
// 這個真和普通並查集沒區別,只是需要注意是什么版本的並查集...
- 至此,可持久化並查集的所有操作就已經解釋完了!
(相信聰明的你一定可以實現它)
其實,把上面的操作拼起來就是完整代碼,不過我還是粘一個完整版吧!
#include<bits/stdc++.h>
#define N 301000
using namespace std;
template<typename T>inline void read(T &x)
{
x=0;
static int p;p=1;
static char c;c=getchar();
while(!isdigit(c)){if(c=='-')p=-1;c=getchar();}
while(isdigit(c)) {x=(x<<1)+(x<<3)+(c-48);c=getchar();}
x*=p;
}
int n,m;
int L[N*30],R[N*30],fa[N*30],dep[N*30];
int root[N*30];
namespace Persistant_Union_Set
{
#define Mid ((l+r)>>1)
#define lson L[rt],l,Mid
#define rson R[rt],Mid+1,r
int cnt;
void build(int &rt,int l,int r)
{
rt=++cnt;
if(l==r){fa[rt]=l;return ;}
build(lson);build(rson);
}
void merge(int last,int &rt,int l,int r,int pos,int Fa)
{
rt=++cnt;L[rt]=L[last],R[rt]=R[last];
if(l==r)
{
fa[rt]=Fa;
dep[rt]=dep[last];
return ;
}
if(pos<=Mid)merge(L[last],lson,pos,Fa);
else merge(R[last],rson,pos,Fa);
}
void update(int rt,int l,int r,int pos)
{
if(l==r){dep[rt]++;return ;}
if(pos<=Mid)update(lson,pos);
else update(rson,pos);
}
int query(int rt,int l,int r,int pos)
{
if(l==r)return rt;
if(pos<=Mid)return query(lson,pos);
else return query(rson,pos);
}
int find(int rt,int pos)
{
int now=query(rt,1,n,pos);
if(fa[now]==pos)return now;
return find(rt,fa[now]);
}
#undef Mid
#undef lson
#undef rson
}
using namespace Persistant_Union_Set;
int main()
{
read(n);read(m);
build(root[0],1,n);
for(int i=1;i<=m;i++)
{
static int opt,x,y;
read(opt);read(x);
if(opt==1)
{
read(y);
static int posx,posy;
root[i]=root[i-1];
posx=find(root[i],x);posy=find(root[i],y);
if(fa[posx]!=fa[posy])
{
if(dep[posx]>dep[posy])swap(posx,posy);
merge(root[i-1],root[i],1,n,fa[posx],fa[posy]);
if(dep[posx]==dep[posy])update(root[i],1,n,fa[posy]);
}
}
else if(opt==2)root[i]=root[x];
else if(opt==3)
{
read(y);
root[i]=root[i-1];
static int posx,posy;
posx=find(root[i],x);posy=find(root[i],y);
if(fa[posx]==fa[posy])puts("1");
else puts("0");
}
}
return 0;
}
擴展——可持久化帶權並查集
- 感覺這個比普通的帶權並查集直接一些!
- 直接在可持久化數組里維護,即在合並父親的時候同時維護權值的信息就行了!
(是不是特別的簡單呢OVO )
題目
- 可持久化並查集的題目我還真沒做過幾個,畢竟這個東西只要板子會打,剩下的都是思維的事情了,代碼實現的難度並不高。題目好像也沒有幾個。
- 洛谷的模板題可以打一下,練一練板子。
(以后好復制) - 如果實在想練一下,那么就去把noi2018歸程用可持久化並查集給做掉233.