並查集中的啟發式合併
演算法原理
並查集一般有兩種方法來保持複雜度不退化,一種是路徑壓縮,另一種則是按照秩來做啟發式合併。
一般情況下我們都是用第一種,壓縮路徑通過遞推找到祖先節點後,在回溯時將它的子孫節點都直接指向祖先,這樣以後每次呼叫Find( )函式找父親時複雜度就變成了O(1)。但是路徑壓縮時直接將節點的父親修改成最終的祖先節點,在破壞原先的樹結構的同時,在有些題目中也會損失資訊。而不使用壓縮路徑,直接用暴力並查集又容易超時。
所以我們考慮用啟發式合併的方法來保持樹的形態,那麼如何控制並查集的複雜度呢?
因為並查集是一種樹型結構,對於以每個節點為根節點的子樹都有一個深度,如果把一棵深度大的樹的根節點接在了一棵深度小的樹上,因為是直接把根節點接在另一個的根節點上,所以整棵樹的深度為那一棵深度大的樹的深度加一。而如果把一棵深度小的樹的根節點接在了一棵深度大的樹上,可直接接上,不影響深度。如果兩個數深度一樣,則將接完後的樹的深度加一即可。所以考慮每次都將深度小的樹接在深度大的樹上,這就是啟發式合併的原理。雖然沒有壓縮路徑,但是按秩合併可以保證樹高是O(logn),這樣找到樹根是O(logn),路徑查詢也是O(logn)。
實現程式碼
int fu[maxn]//存放父節點 int deep[maxn];//記錄深度 int findx(int x)//啟發式合併不壓縮路徑,保持樹結構 { if(fu[x] == x) return x; return findx(fu[x]); } void join(int x, int y,int k) //按照秩來做啟發式合併 { int fx = findx(x); int fy = findx(y); if(fx==fy) return; if(deep[fx]>deep[fy])//深度小的樹接在深度大的樹上 swap(fx,fy); fu[fx] = fy; if(deep[fx]==deep[fy]) deep[fy]++; }
記錄一道例題
思路:
考慮到並查集實際上是一棵樹,所以可以在邊上維護一些資訊,假設k時刻(第k次操作)將u和v連通,我們記錄下該路徑,路徑上的權值則為k,由於如果兩個頂點聯通, 則兩個頂點間只有唯一路徑,這樣查詢邊時可以獲取連通時間。題中由於邊較難儲存,這裡用點儲存,如果一個點被接到另一個點上時,則記錄該點的連通的時刻k,一個點可能被其他點接多次,但只可能接在其他點上一次。
查詢時使u和v都向上找到LCA(最近公共祖先),然後取LCA到u和LCA到v這兩條鏈上的合併時間的最大值,因為越靠近根節點的邊越晚合併,所以只要看LCA連著的兩條邊的時間戳的最大值即可。
AC程式碼如下:
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int maxn=100010;
int fu[maxn],n,m,lastans,sum;//sum記錄當前連通塊
int path[maxn];//記錄聯通路權k,表示在k時刻連通
int deep[maxn];//按照秩來做啟發式合併
int vis[maxn];//每次查詢記錄到該節點路權,即時間
void init()
{
for(int i = 0; i <= n; i++ )
{
fu[i] = i;
deep[i]=path[i]=0;
vis[i]=-1;
}
lastans=0;
sum=n;
}
int findx(int x)//不壓縮路徑
{
if(fu[x] == x) return x;
return findx(fu[x]);
}
void join(int x, int y,int k) {
int fx = findx(x);
int fy = findx(y);
if(fx==fy) return;
if(deep[fx]>deep[fy])//按照秩來做啟發式合併,小樹接在大樹上
swap(fx,fy);
fu[fx] = fy;
path[fx]=k;
if(deep[fx]==deep[fy]) deep[fy]++;
sum--;//合併後連通塊減一
}
int same(int x, int y) {
if(findx(x) != findx(y))
return 0;
int nowmax=0,tx=x,ty=y,ans;
while(1)//先用vis記錄其中一個節點到其祖先路徑上各節點的時刻
{
vis[tx]=nowmax;
if(tx==fu[tx]) break;
nowmax=max(nowmax,path[tx]);
tx=fu[tx];
}
nowmax=0;
while(1)//從另一個節點開始向其祖先遍歷,知道找到有效的vis節點
{
if(vis[ty]!=-1) {//非初始值,說明該節點已與另一個節點連通,即找到了連通時刻
nowmax=max(nowmax,vis[ty]);
break;
}
nowmax=max(nowmax,path[ty]);
ty=fu[ty];
}
tx=x;
while(1)//重新初始化
{
vis[tx]=-1;
if(tx==fu[tx]) break;
tx=fu[tx];
}
return nowmax;
}
int main()
{
int t,op,u,v;
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
init();
for(int k=1;k<=m;k++)
{
scanf("%d%d%d",&op,&u,&v);
u^=lastans;
v^=lastans;
//printf("u=%d v=%d\n",u,v);
if(op==0)
{
join(u,v,k);
printf("%d\n",lastans=sum);
}
else
{
printf("%d\n",lastans=same(u,v));
}
}
}
}