1. 程式人生 > >並查集:擒賊先擒王

並查集:擒賊先擒王

name 節點 單元 spa pan 人的 find 子集 還要

定義

並查集,在一些有\(N\)個元素的集合應用問題中,我們通常是在開始時讓每個元素構成一個單元素的集合,然後按一定順序將屬於同一組的元素所在的集合合並,其間要反復查找一個元素在哪個集合中。這一類問題近幾年來反復出現在信息學的國際國內賽題中,其特點是看似並不復雜,但數據量極大,若用正常的數據結構來描述的話,往往在空間上過大,計算機無法承受;即使在空間上勉強通過,運行的時間復雜度也極高,根本就不可能在比賽規定的運行時間(\(1\)~\(3\)秒)內計算出試題需要的結果,只能用並查集來描述。摘自百度百科。

實現

\(A,B,C,D\)四個人,他們各自為王,一天:

\(A\)\(B\)打架了,然後\(A\)

戰勝了\(B\),於是\(B\)成了\(A\)的小弟,於是數組\(F(B)=A\),代表\(B\)的老大是\(A\)

然後\(C\)\(D\)打架了,\(C\)打敗了\(D\)\(D\)成了\(C\)的手下,於是數組\(F(D)=C\),代表\(D\)的老大是\(C\)

下面,\(C\)的手下D與A打架了,\(A\)戰勝了\(D\),於是\(D\)要歸順\(A\)

\(C\)一看就不幹了,這是我的人啊,不過後來為王,於是\(A\)不但收服\(D\),還然\(C\)成為了他的小弟,於是數組\(F(C)=A\),代表\(C\)的老大是\(A\)

大概是如上的,這個過程我們用語言描述為:

並查集通過一個一維數組來實現,本質上是維護一個森林。剛開始的時候,森林裏的每一個結點都是一個集合(也就是只有一個結點的樹,是孤立的),之後根據題意,逐漸將一個個集合合並(也就是合並成一棵大樹)。之後尋找時不斷查找父節點,當查找到父結點為本身的結點時,這個結點就是祖宗結點。合並則是尋找這兩個結點的祖宗結點,如果這兩個結點不相同,則將其中右邊的集合作為左邊集合的子集(即靠左,靠右也是同一原理)。

所以說我們要一個找老大的函數,通過遞歸實現

int find(int k){
    if(f[k]==k)return k;//如果找到了老大
    return find(f[k]);//否則繼續找
}

上面的代碼不是很靠譜,可能會\(TLE\)

,所以要用到路徑壓縮。路徑壓縮就是當\(B\)歸順A前,\(C\)還是\(B\)的老大,那麽我們不僅要\(F(D)=A\),還要\(F(F)=A\),把所有節點的祖先直接定為最高的祖先,就是路徑壓縮。代碼非常的簡單:

int find(int k){
    if(f[k]==k)return k;
    return f[k]=find(f[k]);
}

下面就簡單了,把每個人的老大設置為自己:

 for(i=1;i<=n;i++)
        f[i]=i;

判斷兩個人是否是一個老大:

if(find(a)==find(b))

讓一個人臣服另一個人:

f[find(B)]=find(A);//B打贏了A,於是讓A的一切關系歸屬B的老大

現在你基本學會了並查集了,那麽讓我們做一道板子題目:

例題

洛谷 P3367 【模板】並查集

Code

#include<bits/stdc++.h>
using namespace std;
int n,m,f[10010];
int find(int k)
{
    if(f[k]==k)return k;
    return f[k]=find(f[k]);
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        f[i]=i;
    for(int i=1;i<=m;i++)
    {
        int x,A,B;
        cin>>x>>A>>B;
        if(x==1)
            f[find(B)]=find(A);
        else
            if(find(A)==find(B))
                printf("Y\n");
            else
                printf("N\n");
    }
    return 0;
}

並查集:擒賊先擒王