1. 程式人生 > >朋友圈--並查集應用

朋友圈--並查集應用


小引

並查集是求解等價關係的得力助手,具體應用如求無向圖連通分支數,至少還需幾條路才能將一個城市串通迷宮生成,克魯斯卡爾演算法求解最小生成樹。它的聽起來高大上,實際上卻是極為簡單的資料結構–森林

並查集顧名思義是並、查集合的操作的實現關鍵在於一個數組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;
}

收穫

  • 不得不佩服前人的精妙思維,如此簡單的結構卻如此強大,果真大道至簡
  • 必須鍛鍊自己明白演算法思想就寫出程式的能力
  • 學習演算法時得明白他的來龍去脈,如何被創造而成,而不是僅僅學會如何使用
  • 同時也該多想想演算法思想之間的關聯性,異同之處,才可能融匯貫通。如哈夫曼樹利用陣列思想,和堆排序類似;二叉排序樹先查詢,後插入,與並查集先查詢,再確定是否合併有些類似