【貪心演算法(三)】並查集和克魯斯卡爾演算法
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知道B,B知道C,意思是A,B,C互相瞭解,所以他們可以呆在一桌中。
例如:如果我告訴你A知道B,B知道C,D知道E,那麼A,B,C可以呆在一桌,D,E必須留在另一桌。所以伊格至少需要2桌。
2.3解析
詳細的就不用解釋了,只是最後輸出的是樹的數量,最初Count=M,每次合併(Union)Count就要減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