最小生成樹——Minimum Spanning Tree,是圖論中比較重要的模型,通常用於解決實際生活中的路徑代價最小一類的問題。我們首先用通俗的語言解釋它的定義:
對於有n個節點的有權無向連通圖,尋找n-1條邊,恰好將這n個節點相連,並且這n-1條邊的權值之和最小。
對於MST問題,通常常見的解法有兩種:Prim算法 或者 Kruskal算法+並查集
對於最小生成樹,一定要注意其定義是在無向連通圖的基礎上,如果在有向圖中,那么就需要另外的分析,單純用無向圖中的方法是不能得出正確解的,這一點我在比賽中確實吃過虧
好了,進入正題:
Prim算法:(基於點的貪心思路)由於是基於點的算法,因此適合於稠密圖,一下給出代碼沒有經過堆優化,時間復雜度為O(N^2)
記原圖為G,生成樹圖為MST,其中G的節點個數為n個
算法描述如下:
- 任取G中的一點,加入MST中——這一步的作用是選擇一個節點作為整個算法的起點
- 采用貪心策略,將剛剛加入的節點記為u,以u為中心,檢查與u相連且沒有加入MST的節點(未訪問過的節點),選擇權值最小的邊,如果有多條邊的權值均最小,則任取一條邊。——貪心策略,選擇局部最優
- 將所選擇的邊中,不在MST中的那個節點,加入MST——舉例來說,比如(u,v)是當前與u相連,v不再MST中,且權值最小的邊,則邊(u,v)被選中,並將v加入MST。
- 如果步驟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+並查集適用於稀疏圖
- 將所有的邊按權值由小到大排序——准備工作,可借助sort()完成,但是在工程中,如果不知道邊和點的數量關系,還是應該用最小值堆,而不是sort來保證效率,但在競賽中,sort足夠了
- 從非MST中的邊中尋找一條,在不會與現有的MST構成環的前提下,權值最小的邊,加入MST
- 如果已經加入了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還沒有實踐過,所以,有時間我再更新一些相關習題吧~