PTA 朋友圈 (25 分) 代碼詳解 (並查集)


1、題目要求:

某學校有N個學生,形成M個俱樂部。每個俱樂部里的學生有着一定相似的興趣愛好,形成一個朋友圈。一個學生可以同時屬於若干個不同的俱樂部。根據“我的朋友的朋友也是我的朋友”這個推論可以得出,如果A和B是朋友,且B和C是朋友,則A和C也是朋友。請編寫程序計算最大朋友圈中有多少人。

輸入格式:

輸入的第一行包含兩個正整數N(≤30000)和M(≤1000),分別代表學校的學生總數和俱樂部的個數。后面的M行每行按以下格式給出1個俱樂部的信息,其中學生從1~N編號:

第i個俱樂部的人數Mi(空格)學生1(空格)學生2 … 學生Mi

輸出格式:

輸出給出一個整數,表示在最大朋友圈中有多少人。

輸入樣例:

7 4
3 1 2 3
2 1 4
3 5 6 7
1 6
 
結尾無空行

輸出樣例:

4
 
結尾無空行
 
2、題目解析:
首先我們來理清一下題目意思,A和B是朋友,B和C是朋友,那么A和C也是朋友,如果此時C又和D是朋友,那么A和D也是朋友,則A此時與B,C,D都是朋友,那么這個以A為起點
的朋友圈就有4個人。又如測試用例,第一行表示第一個圈子,有1,2,3這3個人,第二個圈子有1,4這兩個人,因為1和4是好朋友,所以4和2、3都是朋友,因此以1為中心的這個
朋友圈共有4個人。這是一個以朋友為傳遞關系建立成的朋友圈,是一種連通性的問題。簡單來說就是求這幾個連通分支里點最多的連通分支,朋友圈代表連通分支,朋友圈里
的人代表連通分支上的點。所以題目就是求以朋友關系連起來的最大的朋友圈(連通分支)里有幾個人(點)。
 
3、代碼:
#include <bits/stdc++.h>
using namespace std;
#define MAXn 30001
int fa[MAXn];
int num[30001];

int Find(int x){
    int r=x;
    while(r!=fa[r])
        r=fa[r];
    int i=x,temp;
    while(r!=fa[i]){
        temp=fa[i];
        fa[i]=r;
        i=temp;
    }
    return r;
}

void Union(int x,int y){
    int fx=Find(x),fy=Find(y);
    if(fx!=fy){
        fa[fx]=fy;
        num[fy]+=num[fx];
    }
}

int main(){
    int n,m,people,first,p;
    cin>>n>>m;
    for(int i=0;i<=n;i++)
        fa[i]=i;
    for(int i=0;i<=n;i++)
        num[i]=1;
    while(m--){
        cin>>people;
        cin>>first;
        while(--people){
            cin>>p;
            Union(first,p);
        }
    }
    int max=0;
    for(int i=0;i<=n;i++){
        if(num[i] > max)
            max=num[i];
    }
    cout<<max<<endl;
    return 0;
}

4、“並查集” 說明:

首先本題是屬於數據結構中的 “並查集“ 類型的題目,所以要理解本題的代碼,我們就要先弄懂什么是並查集。

所以現在,我們先來搞懂 ”並查集“是什么?

顧名思義,”並“就是合並,”查“就是查找,”集“就是集合(元素唯一)。”並查集"主要代碼結構有兩個函數,一個主函數“,一個函數是 Find(x) ,一個是 Union(x,y)。

將每個集合看成每棵樹,用數組 fa[]  來存儲各個節點的父節點,fa[i] 表示 i 的父節點。如下圖1,fa[2] = 1,fa[3] = 1,fa[4] = 2......  (圖論基礎)

 

若每棵樹只有一個節點,那么它的父節點就是它自己,即 fa[i] = i,誰和誰都沒有關系,如圖2。

 合並:Union(x,y)

 那么我們要怎么把它們建立聯系呢,就是把單獨的樹合並成一顆大的樹?

 可以令每棵樹的根節點都一樣,那么它們就合並了,比如要將 圖1中的1 和2 合並成一顆樹,可以讓1成為2的根節點(當然也可以讓2成為1的根節點),

即 fa[2] = 1,fa[1] = 1,(fa[1] = 2,fa[2] = 2)這樣就變成了圖3.

若要將3也合並到1那顆樹上,如圖.可以知道fa[3] = 2, fa[2] = 1,fa[1] = 1,則把3合並過來的表達式:fa[3] = fa[1],

假設x=2,y=3 ,將2和3合並,則 fa[x] = y ;

這就是將集合合並起來,從上面兩個圖中我們可以發現若1都是根節點,那么fa[1] = 1,若2是根節點,那么fa[2] = 2,即fa[i] = i 表示此時 i 是這顆樹的根節點,

”查“就是查找根節點。

找到根節點有什么用?

查找:Find(x)

找到一顆樹的根節點就相當於找到了這顆樹的編號,換個說法說就是某個集合的編號,可以知道這個人在哪個集合里。

如下圖

 

 

我們想找5屬於哪顆樹,就得一個一個父節點網上找,直到找到根節點 Find(5) = 1才知道5屬於上面那顆樹。如果我們想找8,那么我們必須找到8的根節點 Find(8) = 6

才知道8屬於下面那棵樹。

因此函數 Find(x) 就是查找 x的根節點,返回值是一個數(根節點)。

 如果我想讓5和6成為好朋友呢?只需要在5和6之間畫一條線連起來,或者之間讓6的父節點之接變成1,這樣6就屬於5的那顆樹啦,如下圖

或者這樣也行,

 

即讓5的根節點等於6的根節點,因此合並的原理就是:查找x和y 的根節點,fx = Find(x) , fy = Find(y) ;令y的根節點等於x的根節點,Find(fx) = fy。

說到這,想必都清楚”並“和”查“的意思了吧?不知道我說得清楚了沒?文案和圖有點亂。。。。

還有一點要注意的是,還有個"路徑壓縮",這個 “路徑壓縮” 是啥捏?

舉個例子:如下圖

 

假設1是根節點,(不分是否二叉樹), 如果我們要找3的根節點是誰,那么上面圖的做法是 fa[3] = 1,查找一次就找到了。而下面圖的做法是 fa[3] = 2,不是根節點(怎么判斷是不是根節點上文有說),

再查找 fa[2] = 1 找到了,是不是上面圖的做法查詢的速度會更快一點?如果有n個節點,那么下面圖要查找n-1次了,此時效率比較低。所以“路徑壓縮” 就是要把樹(集合)上的

節點都搞成上面圖的樣子,這樣就縮短了查詢根節點的路徑了。

 

5、代碼解析:

了解了“並查集 " ,那么我們現在就來看看本題的代碼怎么實現吧!

 

#include <bits/stdc++.h>
using namespace std;
#define MAX 30001
int fa[MAX];//存儲父節點
int num[30001];//存儲節點數

/*查找根節點*/
int Find(int x){
    //找x的根節點
    int r=x;//x賦給r,以免中間發生變化后x改變
    while(r!=fa[r])//循環直到找到根節點,上文第4點有說例子
        r=fa[r];//讓r的父節點等於r,直到找到x的根節點,此時就可返回r了,下面是為了壓縮路徑寫的代碼
    //路徑壓縮
    int i=x,temp;//temp為中間變量
    while(r!=fa[i]){//假設鏈式樹,“1---2---3---4” 根節點r=1,x=3,即要把3直接套在1后面,這里上文“路徑壓縮”有說到。
        temp=fa[i];//把3的父節點2賦給temp
        fa[i]=r;//讓3的父節點等於1
        i=temp;//把3的父節點2賦給i,繼續循環把2的根節點變成1
    }
    return r;//最后返回根節點r
}

/*合並兩個集合*/
void Union(int x,int y){
    int fx=Find(x),fy=Find(y);//分別找到x和y的根節點
    if(fx!=fy){//如果x和y的根節點不一樣,說明兩個人不在一棵樹上,此時就要合並兩個人
        fa[fx]=fy;//讓y的根節點fy成為x的根節點,兩棵樹(集合)就合並了
        num[fy]+=num[fx];//fy這顆樹多加num[fx]個人,num[fx]代表以fx為根節點所在的樹上有幾個節點
    }
}

int main(){
    int n,m,people,first,p;
    cin>>n>>m;
    //給每個人編號,並讓每個人的父節點都是自己
    for(int i=0;i<=n;i++)
        fa[i]=i;
    //定義一個num[]數組來存儲第i棵樹(朋友圈)上有幾個節點(人),剛開始都是一個人,所以初始化為1
    for(int i=0;i<=n;i++)
        num[i]=1;
    //輸入數據
    while(m--){
        cin>>people;//每一行中第一個數是這個圈子有幾個人
        cin>>first;//第二個數在這里輸入,是為了將第一個數作為每一行(每一個圈子)的根,用來標識朋友圈加以區別
        while(--people){
            cin>>p;//依次輸入每個人的編號
            Union(first,p);//每輸入一個人就合並到第一個人的樹(圈子)上,這樣每一行的人都代表一個圈子里的人
        }
    }
    //求出最大的樹(圈子)有幾個節點(人)
    int max=0;
    for(int i=0;i<=n;i++){//人的編號從1~N
        if(num[i] > max)
            max=num[i];
    }
    cout<<max<<endl;
    return 0;
}

 

不知道我講的明白了沒,如果大家還沒理解,可以看看這個博客,暢通工程並查集詳解

 

 

 

 

 

 

 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM