並查集中的啓發式合併

算法原理

並查集一般有兩種方法來保持複雜度不退化,一種是路徑壓縮,另一種則是按照秩來做啓發式合併。

一般情況下我們都是用第一種,壓縮路徑通過遞推找到祖先節點後,在回溯時將它的子孫節點都直接指向祖先,這樣以後每次調用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]++;
}

 

記錄一道例題

BNUOJ 51275 道路修建 Large && FJUT1961

思路:

考慮到並查集實際上是一棵樹,所以可以在邊上維護一些信息,假設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));
            }
        }
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章