1. 程式人生 > >【貪心演算法(三)】並查集和克魯斯卡爾演算法

【貪心演算法(三)】並查集和克魯斯卡爾演算法

1介紹

本節將記錄兩個問題,(1)並查集;(2)克魯斯卡爾演算法;。

這是貪心演算法最後一節,可能不是所有的問題都與貪心演算法有關,但是都是我認為有趣且比較重要的東西,有必要統一學習記錄一下。可能我舉例不太文雅,但絕對沒有歧視和嘲諷任何群體的意思,只是為了讓人印象深刻一些。

2並查集

2.1原理

2.1.1基礎

我們在使用QQ或者其他社交軟體的時候,會發現有一個好友推送的功能,也就是向你推薦你朋友的朋友。如果我們認定你朋友的朋友就是你的朋友,那麼你的朋友圈就足夠大了。倘若現在出現一個美女——如花,你非常想認識她/他,首先你不認識她,其次你朋友圈裡也沒有人認識如花和如花朋友圈裡的人。


當然最直接的方法是直接去搭訕如花,這是最簡單的方法。但是你想做的更高效一些,不能只考慮自己的幸福,因為你所處的是悶騷型屌絲群體,都是單身狗,而如花所處的朋友圈都是女神(原諒老郭),所以,有沒有什麼辦法讓這兩個朋友圈都互相認識呢?

很簡單,讓你朋友圈的老大謙哥去跟對方朋友圈的老大老郭談一談,談妥以後就可以聯誼了,至於怎麼聯誼,從此以後,是讓謙哥認老郭做朋友圈老大,還是讓老郭認謙哥做朋友圈老大,這個也有講究,稍後討論。

可以總結一下這個過程:(1)你要認識如花;(2)你去找你的老大謙哥;(3)如花去找她的老大老郭;(4)發現你們兩個的老大不是同一個人(謙哥和老郭),於是聯合。

程式的一般結構,三個函式,一個數組:

(1) 一個數組:用來存貯當前資料的前驅結點,如:Arry[i]=j表示i的前驅是j,當Arry[i]=i時表示i是根結點。

(2) Find函式:尋找老大;

int Find(int num){
	while(Arry[num]!=num)
		num=Arry[num];
	return num;
}

(3) Connect函式:判斷兩人是否認識,是返回true,否返回false

bool Connect(int a,int b){
	if(Arry[a]==Arry[b])
		return true ;
	return false;
}

(4) Union函式:聯合

void Union(int a ,int b){
	int aRoot=Find(a);
	int bRoot=Find(b);
	if(aRoot!=bRoot)
		Arry[aRoot]=bRoot;
}

2.1.2路徑壓縮

現在來優化一下,假如你的朋友圈是下面這種結構的,也就是說,你可能太宅了,朋友圈中很多牛逼的人物你接觸不到。




如果是這樣的話,你想促成這次聯誼,就要一層一層往上去找老大,一個個問“咱們這個圈子誰比較有號召力啊?”,這樣勞心費力,操碎了心,還不如直接去找謙哥來的快。


我們要想辦法把路徑壓縮成上圖這樣比較好。我們在Find函式裡壓縮,修改前驅就可以了。當然還有很多高明的壓縮手法此處就不拓展了。

路徑壓縮:

int Find(int num){
	int root=num,temp;
	while(Arry[root]!=root)
		root=Arry[root];
	//路徑壓縮
	while(num!=root){
		temp=Arry[num];
		Arry[num]=root;
		num=temp;
	}
	return num;
}

2.1.3畸形樹

回到之前的一個問題,“從此以後,是讓謙哥認老郭做朋友圈老大,還是讓老郭認謙哥做朋友圈老大”。



之前預設的是Arry[aRoot]=bRoot,為什麼不寫成Arry[bRoot]=aRoot?這樣一味跪舔女神有意義嗎?畢竟不是所有的單身狗都是哈士奇,也有很多獨狼,就像人生苦短,有人選擇及時行樂,有人選擇創造價值,無所謂對錯,只是個人選擇而已。

並查集告訴我們,一味地,盲目地妥協只能造成畸形,從樹的角度而言,下列哪種樹最合適一目瞭然。



給出一張正規的比較圖。


優化程式碼,小樹附在大樹旁,要給每一個節點一個Size,那麼所有結點的Size可以存進一個數組裡。

Size陣列的初始化,size[i]=j意思是以i為根結點的樹中,有j個節點,初始化,每個結點是相互獨立的,只包括自己:

for (int i = 0; i < max; i++)  
		 size[i] = 1;  

優化一下union函式:

void Union(int a ,int b){
	int aRoot=Find(a);
	int bRoot=Find(b);
	if(aRoot<bRoot){
		Arry[aRoot]=bRoot;
		size[bRoot]+=size[aRoot];
	}
	else {
		Arry[bRoot]=aRoot;
		size[aRoot]+=size[bRoot];
	}
}

給出一張正規的優化前(上)和優化後(下)的比較圖,很明顯優化後的查詢速率更快。接下來,用並查集解決一個實際問題。


2.2問題

問題:今天是Ignatius的生日。他邀請了許多朋友。現在是吃晚飯的時間了。Ignatius想知道他至少需要多少桌。你必須注意到並不是所有的朋友都認識對方,而且所有的朋友都不想和陌生人呆在一起。

這個問題的一個重要規則是,如果我告訴你A知道BB知道C,意思是ABC互相瞭解,所以他們可以呆在一桌中。

例如:如果我告訴你A知道BB知道CD知道E,那麼ABC可以呆在一桌,DE必須留在另一桌。所以伊格至少需要2桌。

2.3解析

詳細的就不用解釋了,只是最後輸出的是樹的數量,最初Count=M,每次合併(UnionCount就要減1,或者找根結點的個數,迴圈陣列序號與值相等的即為根結點。

AC原始碼:

#include<iostream>
using namespace std;
#define max 25
//陣列
int Arry[1003];
int size[1003];
//find
int Find(int num){
	int root=num,temp;
	while(Arry[root]!=root)
		root=Arry[root];
	//路徑壓縮
	while(num!=root){
		temp=Arry[num];
		Arry[num]=root;
		num=temp;
	}
	return num;
}
//connect
bool Connect(int a,int b){
	if(Arry[a]==Arry[b])
		return true ;
	return false;
}
//union
void Union(int a ,int b){
	int aRoot=Find(a);
	int bRoot=Find(b);
	if(aRoot==bRoot)
		return ;
	if(aRoot<bRoot){
		Arry[aRoot]=bRoot;
		size[aRoot]+=size[bRoot];
	}
	else {
		Arry[bRoot]=aRoot;
		size[bRoot]+=size[aRoot];
	}

}
int main (){
	int n,m;//m為資料組數,n為結點個數
	int i,j,k;//迴圈用
	int x,y;//輸入的兩個引數
	int count;
	cin>>k;
	while(k--){
		cin>>n>>m;
		count=0;
		//初始化size陣列
		for (i = 0; i <n; i++)  
			 size[i] = 1;
		//初始化Arry陣列
		for(i=0;i<n;i++)
			Arry[i]=i;
		for(i=0;i<m;i++){
			cin>>x>>y;
			Union(x-1,y-1);
		}
		for(j=0;j<n;j++){
			if(Arry[j]==j)
				count++;
		}
		cout<<count<<endl;
	}
	return 0;
}

2.3結果


3克魯斯卡爾演算法

3.1問題

問題:求上圖的最小生成樹。

3.2分析

這個問題,我之前在寫【動態規劃(二)】時用普里姆演算法解析過了,這次用克魯斯卡爾演算法詳解一次。這跟並查集有很重要的聯絡。

(1)依據貪心演算法思想,要求最小生成樹,那麼每次需要尋找最小權值的邊,然後依據邊確定相應的結點

(2)找最小權值的邊很容易,只需要升序排序即可,問題在於一個結點對應著多個邊,各種交錯,很容易形成環路,那樣求得的肯定就不是最小生成樹了。

(3)我們回到並查集,去掉所有的邊(其實是把他們放進一個數組裡按升序排序),把他們看成一個個獨立的結點,


(4)把每個結點看成一個獨立的樹,遍歷邊權值陣列,每次尋找最小的,如果邊兩端的結點所在樹的根節點不通,則說明二者為兩棵樹,這樣合併之後就不會出現環路的情況了。

(5)那麼我們來給出克魯斯卡爾演算法的框架(可能會有些細微不同,因為我在儘量把它跟並查集的框架保持一致,)

#define Maxvex 9//最多點數
#define Maxedge 81//最多邊數
#define INFINITY 65536
typedef struct 
{
	int begin;
	int end;
	int weight;
}Edge;
int Parent[Maxvex];//記錄每個根結點的前驅結點
//不影響樹的結構只起記錄作用:不考慮畸形樹的問題
int Find(int num){
	int root = num, temp;
	while (Parent[root]!= root)
		root = Parent[root];
	return num;
}
bool Union(int s, int e){
	int sRoot = Find(s);
	int eRoot = Find(e);
	if (sRoot != eRoot){
		Parent[eRoot] = sRoot;//不需要考慮畸形樹的問題
		return true;
	}
	return false;
}
void KrusKal(int M[Maxvex][Maxvex]){
	 Edge e[Maxedge],temp;
	 int i, j,t=0;

	 //初始化Parent陣列,在大話資料結構中此處全初始化為0,原理是一樣的,本人此處是為確保和上述所講並查集保持一致
	 for (i = 0; i < Maxvex;i++){
		 Parent[i] = i;
	 }
	 //開始選邊
	 for (i = 0; i < t;i++){
		 if (Union(e[i].begin,e[i].end))
			 cout << "(" << e[i].begin<< "," << e[i].end << "),"<<e[i].weight << endl;
	 }
}

(6)《大話資料結構》關於克魯斯卡爾演算法,跟我在此處的程式碼是有許多不同的。首先,它把union併入了void KrusKal(int M[Maxvex][Maxvex]),其次, Parent[i]它記錄的是下一個結點,我們記錄的是前驅Parent[eRoot] = sRoot;,從而該陣列初始化可能不同,if (sRoot != eRoot)不需變動。再次,路徑壓縮與畸形樹的優化可有可無,因為Parent不影響樹的結構只起記錄作用,最後給出原始碼。

原始碼:

#include<iostream>
using namespace std;
#define Maxvex 9//最多點數
#define Maxedge 81//最多邊數
#define INFINITY 65536
typedef struct 
{
	int begin;
	int end;
	int weight;
}Edge;
int Parent[Maxvex];//記錄每個根結點的前驅結點
//不影響樹的結構只起記錄作用:不考慮畸形樹的問題
int Find(int num){
	int root = num, temp;
	while (Parent[root]!= root)
		root = Parent[root];
	//路徑壓縮
	while (num != root){
		temp = Parent[num];
		Parent[num] = root;
		num = temp;
	}
	return num;
}
bool Union(int s, int e){
	int sRoot = Find(s);
	int eRoot = Find(e);
	if (sRoot != eRoot){
		Parent[eRoot] = sRoot;//不需要考慮畸形樹的問題
		return true;
	}
	return false;
}
void KrusKal(int M[Maxvex][Maxvex]){
	 Edge e[Maxedge],temp;
	 int i, j,t=0;

	 //初始化Parent陣列,在大話資料結構中此處全初始化為0,原理是一樣的,本人此處是為確保和上述所講並查集保持一致
	 for (i = 0; i < Maxvex;i++){
		 Parent[i] = i;
	 }
	 //處理矩陣,化成Edge
	 for (i = 0; i < Maxvex;i++){
		 for (j = i+1; j < Maxvex; j++){
			 if (M[i][j] < INFINITY){
				 e[t].begin = i;
				 e[t].end = j;
				 e[t].weight = M[i][j];
				 t++;
			 }
		 }
	 }
	 //根據貪心思想,每次選取最小的邊,先對Edge按升序排序
	 for (i = 0; i <t; i++){
		 for (j = i + 1; j <t; j++){
			 if (e[i].weight> e[j].weight){
				 temp = e[i];
				 e[i] = e[j];
				 e[j] = temp;
			 }
		 }
	 }
	 //開始選邊
	 for (i = 0; i < t;i++){
		 if (Union(e[i].begin,e[i].end))
			 cout << "(" << e[i].begin<< "," << e[i].end << "),"<<e[i].weight << endl;
	 }
}
int main(){
	//構建圖矩陣
	int MyGraph[Maxvex][Maxvex] = {
		{ 0, 10, INFINITY, INFINITY, INFINITY, 11, INFINITY, INFINITY, INFINITY },
		{ 10, 0, 18, INFINITY, INFINITY, INFINITY, 16, INFINITY, 12 },
		{ INFINITY, INFINITY, 0, 22, INFINITY, INFINITY, INFINITY, INFINITY, 8 },
		{ INFINITY, INFINITY, 22, 0, 20, INFINITY, 24, 16, 21 },
		{ INFINITY, INFINITY, INFINITY, 20, 0, 26, INFINITY, 7, INFINITY },
		{ 11, INFINITY, INFINITY, INFINITY, 20, 0, 17, INFINITY, INFINITY },
		{ INFINITY, 16, INFINITY, INFINITY, INFINITY, 17, 0, 19, INFINITY },
		{ INFINITY, INFINITY, INFINITY, 16, 7, INFINITY, 19, 0, INFINITY },
		{ INFINITY, 12, 8, 21, INFINITY, INFINITY, INFINITY, INFINITY, 0 } };
	KrusKal(MyGraph);
	return 0;
}

3.3結果

藍色框為邊出現順序。




八個問題的原始碼專案(19):https://github.com/AngryCaveman/C-Struct.git