朋友圈--並查集應用
小引
並查集是求解等價關係的得力助手,具體應用如求無向圖連通分支數,至少還需幾條路才能將一個城市串通,迷宮生成,克魯斯卡爾演算法求解最小生成樹。它的聽起來高大上,實際上卻是極為簡單的資料結構–森林
並查集顧名思義是並、查集合的操作的實現關鍵在於一個數組father,該陣列下標表示相應的點,值表示該點對應的雙親,初始化全為-1,表示每一個點都構成僅有一個結點且該結點為根的樹,每次得到兩個點的關係,利用查詢函式找到兩個點的祖先,並將他們合併構建為一顆二叉樹,以此反覆,直到所有關係均處理完
最終father狀態含義:
- 負數的個數表示連通分支個數(集合個數)
- 負數的絕對值表示相應連通分支的節點個數(相應集合的大小)
朋友圈求解
[問題描述]
某學校有N個學生,形成M個俱樂部。每個俱樂部裡的學生有著相似的興趣愛好,形成一個朋友圈。一個學生可以同時屬於若干個不同的俱樂部。根據“我的朋友的朋友也是我的朋友”這個推論可以得出,如果A和B是朋友,且B和C是朋友,則A和C也是朋友。請編寫程式計算最大朋友圈中有多少人。
[基本要求]
(1)輸入說明:輸入的第一行包含兩個正整數N (N<=30 000)和M (M<=1000),分別代表學校的學生總數和俱樂部的個數。隨後的M行每行按以下格式給出一個俱樂部的資訊,其中學生從1-N編號:
第i個俱樂部的人數Mi(空格)學生1(空格)學生2… 學生Mi
(2)輸出說明:輸出一個整數,表示在最大朋友圈中有多少人。
(3)測試用例:
輸入 7 4
3 1 2 3
2 1 4
3 5 6 7
1 6
輸出 4
資料結構
father陣列最終狀態:負數的個數表示朋友圈個數;陣列值的絕對值表示該朋友圈人數
int father[30005];//下標表示人的標號,值表示雙親節點
查詢
- 查詢並返回當前點所在樹的根節點
- 優化:壓縮路徑法,即將一棵樹中所有的點均直接指向根,可以提高查詢效率
//查詢當前點所在樹的根節點
int find(int child)
{
int f = child;
while(father[f] > 0){
f = father[f];
}
//優化:壓縮路徑,即將一棵樹中所有的點均直接指向根,可以提高查詢效率
int j = child;`在這裡插入程式碼片`
while(j != f){
father[j] = f;
j = father[j];
}
return f;
}
合併
將兩個根合併,等價於將兩棵樹合併,但二叉樹只能有一個根,所以必須抉擇,本來選擇誰都是可行的,但為了避免樹退化,高度過大,每次選擇高度小的根接入高度大的根
//合併兩個點
void Union(int fa,int fb)
{
//優化:為了避免樹的退化,每次將高度小的根接到高度大的根
if(father[fa] > father[fb]){
father[fa] += father[fb];//個數相加
father[fb] = fa;//fb為fa父親
}
else{
father[fb] += father[fa];
father[fa] = fb;
}
}
核心處理
從檔案讀取資料
檔案朋友圈.txt內容
7 4
3 1 2 3
2 1 4
3 5 6 7
1 6
依次處理每一條邊即可
//計算
void Cal()
{
//檔案讀取資料
fstream inFile("朋友圈.txt",ios::in);
if(!inFile)cout<<"fail to open file!"<<endl;
int n,m;//總人數,社團個數
inFile>>n>>m;//cout<<"n:"<<n<<" m:"<<m<<endl;
for(int i = 1; i <= n; i++){//全初始化為-1
father[i] = -1;
}
int sum,a,b,fa,fb;
for(int i = 0; i < m; i++){//處理m條資訊
inFile>>sum>>a;
if(sum != 1){
for(int j = 0; j < sum-1; j++){
inFile>>b;
fa = Find(a);//一次合併
fb = Find(b);
Union(fa,fb);
}
}
}
inFile.close();
int min = 999999;//由於初始值為-1,疊加後為負數,所以尋找最小值
for(int i = 0; i < n; i++){
if(father[i] < 0){
if(min > father[i]){
min = father[i];
}
}
}
cout<<-min<<" ";
}
完整Code
#include<iostream>
using namespace std;
#include<fstream>
//並查集應用
//可用於求關聯集合個數及其總個數
//father陣列最終狀態:負數的個數表示朋友圈個數;陣列值的絕對值表示該朋友圈人數
int father[30005];//下標表示人的標號,值表示雙親節點
//查詢當前點的祖先,根節點
int Find(int child)
{
int f = child;
while(father[f] > 0){
f = father[f];
}
//壓縮路徑,可有可無,不過可以提高效率
int j = child;
while(j != f){
father[j] = f;
j = father[j];
}
return f;
}
//合併兩個點
void Union(int fa,int fb)
{
//優化:為了避免樹的退化,每次將高度小的根接到高度大的根
if(father[fa] > father[fb]){
father[fa] += father[fb];//個數相加
father[fb] = fa;//fb為fa父親
}
else{
father[fb] += father[fa];
father[fa] = fb;
}
}
//計算
void Cal()
{
//檔案讀取資料
fstream inFile("朋友圈.txt",ios::in);
if(!inFile)cout<<"fail to open file!"<<endl;
int n,m;//總人數,社團個數
inFile>>n>>m;//cout<<"n:"<<n<<" m:"<<m<<endl;
for(int i = 1; i <= n; i++){//全初始化為-1
father[i] = -1;
}
int sum,a,b,fa,fb;
for(int i = 0; i < m; i++){//處理m條資訊
inFile>>sum>>a;
if(sum != 1){
for(int j = 0; j < sum-1; j++){
inFile>>b;
fa = Find(a);//一次合併
fb = Find(b);
Union(fa,fb);
}
}
}
inFile.close();
int min = 999999;//由於初始值為-1,疊加後為負數,所以尋找最小值
for(int i = 0; i < n; i++){
if(father[i] < 0){
if(min > father[i]){
min = father[i];
}
}
}
cout<<-min<<" ";
}
int main()
{
Cal();
return 0;
}
收穫
- 不得不佩服前人的精妙思維,如此簡單的結構卻如此強大,果真大道至簡
- 必須鍛鍊自己明白演算法思想就寫出程式的能力
- 學習演算法時得明白他的來龍去脈,如何被創造而成,而不是僅僅學會如何使用
- 同時也該多想想演算法思想之間的關聯性,異同之處,才可能融匯貫通。如哈夫曼樹利用陣列思想,和堆排序類似;二叉排序樹先查詢,後插入,與並查集先查詢,再確定是否合併有些類似