最小生成樹(Minimum Spanning Tree)——Prim算法與Kruskal算法+並查集


最小生成樹——Minimum Spanning Tree,是圖論中比較重要的模型,通常用於解決實際生活中的路徑代價最小一類的問題。我們首先用通俗的語言解釋它的定義:

對於有n個節點的有權無向連通圖,尋找n-1條邊,恰好將這n個節點相連,並且這n-1條邊的權值之和最小。

對於MST問題,通常常見的解法有兩種:Prim算法   或者  Kruskal算法+並查集

對於最小生成樹,一定要注意其定義是在無向連通圖的基礎上,如果在有向圖中,那么就需要另外的分析,單純用無向圖中的方法是不能得出正確解的,這一點我在比賽中確實吃過虧

好了,進入正題:

Prim算法:(基於點的貪心思路)由於是基於點的算法,因此適合於稠密圖,一下給出代碼沒有經過堆優化,時間復雜度為O(N^2)

  記原圖為G,生成樹圖為MST,其中G的節點個數為n個

  算法描述如下:

  1. 任取G中的一點,加入MST中——這一步的作用是選擇一個節點作為整個算法的起點
  2. 采用貪心策略,將剛剛加入的節點記為u,以u為中心,檢查與u相連且沒有加入MST的節點(未訪問過的節點),選擇權值最小的邊,如果有多條邊的權值均最小,則任取一條邊。——貪心策略,選擇局部最優
  3. 將所選擇的邊中,不在MST中的那個節點,加入MST——舉例來說,比如(u,v)是當前與u相連,v不再MST中,且權值最小的邊,則邊(u,v)被選中,並將v加入MST。
  4. 如果步驟2-3被執行了n-1次,則退出,反之則返回到步驟2。——由於Prim算法初始化時加入了起點,而步驟2-3每執行一次都會加入一個新的節點,所以只需判斷執行次數。

關於算法的正確性證明網上都有證明,這里就不再贅述。

 1 //inf為路徑權上界,maxn為圖的臨接矩陣的點數
 2 //vis是記錄是否訪問過,cost[i]記錄到達第i個節點的最小代價 
 3 const int inf=0x7fffffff,maxn=101;
 4 int G[maxn][maxn],vis[maxn],cost[maxn],n;
 5 //len為MST長度
 6 int prim(){
 7     memset(vis,0,sizeof(vis));
 8    //加入起始節點
 9     int pos=1,min=inf,len=0,cnt=n;
10     vis[1]=1;
11     for(int v=2;v<=n;v++)cost[v]=G[pos][v];
12    //加入剩余n-1個節點
13     while(--cnt){
14         for(int i=1;i<=n;i++)if(!vis[i]&&cost[i]<min){
15             pos=i;min=cost[i];
16         }
17         len+=min;vis[pos]=1;
18         //以新加入的節點為中心,更新權值信息
19         for(int i=1;i<=n;i++)if(!vis[i]&&G[pos][i]<cost[i])
20             cost[i]=G[pos][i];
21         min=inf;
22     }
23     return len;
24 }

結合poj上的一道水題來驗證一下Prim的威力吧~親測156k內存0ms過(C++編譯器)

poj1258:http://poj.org/problem?id=1258

 

Kruskal算法:(基於邊的貪心算法)基於邊的貪心,由圖的性質不難知道,當圖為稠密圖時,邊的數目遠大於點的數目,因此Kruskal+並查集適用於稀疏圖

  1. 將所有的邊按權值由小到大排序——准備工作,可借助sort()完成,但是在工程中,如果不知道邊和點的數量關系,還是應該用最小值堆,而不是sort來保證效率,但在競賽中,sort足夠了
  2. 從非MST中的邊中尋找一條,在不會與現有的MST構成環的前提下,權值最小的邊,加入MST
  3. 如果已經加入了n-1條邊,則結束,否則返回步驟2

那么從算法描述,我們不難看到,整個算法中的核心部分是,判斷當前權值最小的邊是否會與MST構成環。

那么如何實現這個判斷呢?一種思路是我們通過BFS或者DFS,用遍歷圖的辦法來判斷——然而這個編程復雜度和時間復雜度都很高╮(╯-╰)╭

我們可以從另一個角度進行考量。如果說我們給每個MST一個代表元素(representative),或者說,是一個標記,那么,對於一個不連通的無向圖,每個MST就可以看作一個連通支,而每個連通支其實可以看作一個集合,連通支中的節點就是集合中的元素,而我們只關心一個新的元素是否在原先的集合中。

那么判定元素是否在集合中,我們是不是馬上想到了一種樹形結構——並查集(Union-Find Set)

並查集的數組實現如下:p[x]表示第x元素的父元素,我們規定當p[x]==x時,表示找到了這一組元素的代表元(representative),

則可以遞歸的進行查找,並同時進行路徑壓縮,因此,不難看出,在均攤意義下,並查集的時間復雜度為O(1)。

1 int find(int x){ return p[x]== x ? x : p[x] = find(p[x]); }

為什么return語句可以這樣和賦值語句連用?

大家想想諸如a=b=c=1;這樣的連續賦值,不難理解,其實賦值語句是有返回值的,並且返回值為左值的值,即先返回c的值1,賦給b,返回b的值1,賦給a,最后返回a的值。

這樣,我們就可以給出kruskal的完整實現了:

 1 const int maxn=100;
 2 //n為節點個數,m為邊個數,r存儲第i+1小的邊的序號,w存儲第i條邊的權值,u和v存儲第i條邊的節點序號 
 3 int p[maxn],n,u[maxn],v[maxn],w[maxn],r[maxn],m;
 4 //並查集find
 5 int find(int x){ return p[x]==x?x:p[x]=find(p[x]); }
 6 //間接排序函數
 7 int cmp(const int i,const int j){ return w[i]<w[j]; } 
 8 int kruskal(){
 9     int len=0;
10     for(int i=0;i<n;i++)p[i]=i;//初始化並查集
11     for(int i=0;i<m;i++)r[i]=i;//初始化邊的序號 
12     sort(r,r+m,cmp);//<algorithm>中的優化的快排
13     for(int i=0;i<m;i++){
14         int e=r[i],x=find(u[e]),y=find(v[e]);
15         if(x!=y){ len+=w[e];p[y]=x; }//並查集Union 
16     }
17     return len;
18 }

不難看出,Kruskal算法的復雜度為O(ElogE),基本上都集中在排序了,所以,工程上還可以用優先隊列或者斐波那契堆來減小復雜度

這樣,無向圖中的MST模型就介紹的差不多了,通常這個模型會用於解決資源最省之類的問題,不過,kruskal還沒有實踐過,所以,有時間我再更新一些相關習題吧~


免責聲明!

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



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