BZOJ 3673 可持久化並查集 by zky 可持續化線段樹+並查集啟發式合併

~hsm~發表於2019-04-04

題目

BZOJ 3673
LUOGU 3402
Description

n個集合 m個操作
操作:
1 a b 合併a,b所在集合
2 k 回到第k次操作之後的狀態(查詢算作操作)
3 a b 詢問a,b是否屬於同一集合,是則輸出1否則輸出0
0<n,m<=2*10^4

Input

Output

Sample Input

5 6
1 1 2
3 1 2
2 0
3 1 2
2 1
3 1 2

Sample Output

1
0
1

HINT

Source

出題人大SB

題解

  • 這題不知道出題人什麼做法,但是程式碼很短的樣子
    UPD:出題人用的是rope,即stl中的可持久化平衡樹
    KuribohG神犇告訴了我可以用可持久化線段樹實現可持久化陣列T T
    既然都有可持久化陣列了,只要用個再並查集的啟發式合併就能妥妥的水過了(這樣每次只要修改一個fa)。

  • 並查集的啟發式合併就是按秩合併,初始所有集合秩為0
    合併把秩小的樹根的父親設為秩大的樹根
    如果秩相同,則隨便選取一個作為父節點並將它的秩+1
    秩和fa一樣維護。

  • 但是其實這題資料隨機的話隨便合併就行了,根本不用按秩合併什麼的
    UPD:秩其實有的時候很不好用,維護子樹大小比較贊。。。
    另外,ndsf發現只要直接暴力就能虐了T T。

引用自hzwer

程式碼

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
template<typename T>inline void read(T &x)
{
	x=0;
	T f=1,ch=getchar();
	while (!isdigit(ch) && ch^'-') ch=getchar();
	if (ch=='-') f=-1, ch=getchar();
	while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
	x*=f;
}
struct rec
{
	int l,r;
}tree[maxn*30];
int Ed[maxn],tot;//Ed[]是版本號,tot是節點總數(這些就是主席樹啦)
int n,m,fa[maxn*30],deep[maxn*30];//deep[]存最大深度,fa[]存一個點在某個版本的父親
inline void build(int &now,int l,int r)
{
	now=++tot;
	if (l==r)
	{
		fa[now]=l;//初始版本:父親是自己,就像並查集初始化每個點的父親是它自己
		return ;
	}
	int mid=(l+r)>>1;
	build(tree[now].l,l,mid);
	build(tree[now].r,mid+1,r);
}
//主席樹維護的是:每一個版本,每一個點的父親是誰
inline void update(int &now,int last,int l,int r,int x,int f)//把x的父親改成f
{
	now=++tot;
	if (l==r)
	{
		fa[now]=f;
		deep[now]=deep[last];
		return ;
	}
	tree[now]=tree[last];//deep[]用於啟發式合併
	int mid=(l+r)>>1;
	if (x<=mid)
		update(tree[now].l,tree[last].l,l,mid,x,f);
	else
		update(tree[now].r,tree[last].r,mid+1,r,x,f);
}

inline int query(int now,int l,int r,int x)//詢問某一個版本的一個點的父親
{
	if (l==r)
		return now;
	int mid=(l+r)>>1;
	if (x<=mid)
		return query(tree[now].l,l,mid,x);
	else
		return query(tree[now].r,mid+1,r,x);
}

inline void add(int now,int l,int r,int x)//把某一個並查集聯通塊中每一個點的深度加一
{
	if (l==r)
	{
		++deep[now];
		return ;
	}
	int mid=(l+r)>>1;
	if (x<=mid)
		add(tree[now].l,l,mid,x);
	else
		add(tree[now].r,mid+1,r,x);
}
inline int get(int ed,int x)//ed 版本編號
{
	register int f=query(ed,1,n,x);//查詢在這一版本里 一個點的父親
	if (x==fa[f]) return f;
	return get(ed,fa[f]);//不帶路徑壓縮的並查集
}
int main()
{
	read(n);read(m);
	build(Ed[0],1,n);
	for (register int opt,k,a,b,i=1;i<=m;++i)
	{
		read(opt);
		if (opt==1)
		{
			Ed[i]=Ed[i-1];
			read(a);read(b);
			register int f1=get(Ed[i],a);
			register int f2=get(Ed[i],b);
			if (fa[f1]==fa[f2]) continue;
			if (deep[f1] > deep[f2])
				swap(f1,f2);//把大的往小的並,保證f1兒子節點數一定是小於等於f2
			update(Ed[i],Ed[i-1],1,n,fa[f1],fa[f2]);
			if (deep[f1]==deep[f2])
				add(Ed[i],1,n,fa[f2]);//因為f2併到了f1,所以f1的深度要加1
          //我們用啟發式合併來保證並查集合並的複雜度
		}
		else if (opt==2)
		{
			read(k);
			Ed[i]=Ed[k];
		}
		else
		{
			Ed[i]=Ed[i-1];
			read(a);read(b);
			register int f1=get(Ed[i],a);
			register int f2=get(Ed[i],b);
			if (fa[f1]==fa[f2]) puts("1");
			else puts("0");
		}
	}
	return 0;
}

相關文章