圖的最小生成樹


圖的最小生成樹

對於一張圖,我們有一個定理:n個點用n-1條邊連接,形成的圖形只可能是樹。我們可以這樣理解:樹的每一個結點都有一個唯一的父親,也就是至少有n條邊,但是根節點要除外,所以就是n-1條邊。還有一種理解:樹里不存在環,那么既要連接n個點又不能形成環,只能用n-1條邊。

那么,對於一張n個點帶權圖,它的生成樹就是用其中的n-1條邊來連接這n個點,那么最小生成樹就是n-1條邊的邊權之和最小的一種方案,簡單的理解,就是用讓這張圖只剩下n-1條邊,同時這n-1條邊的邊權總和最小。如下圖所示:

紅邊即為此圖的最小生成樹,注意我們目前只討論無向圖的最小生成樹。


求最小生成樹的過程,我們可以理解為建一棵樹。要使邊權總和最小,我們不難想到可以用貪心的思想:讓最小生成樹里的每一條邊都盡可能小,那么我們有兩種思路,分別對應着兩種算法:

(1)Kruskal:

我們不難想到一種貪心的策略:每一條邊的邊權都是小的,那這些邊連接起來的邊權總和一定也是小的。所以,我們不難想到可以先挑選最小的,再挑選次小的、第三小的......直到我們挑了n-1條邊。因此,我們可以將這些邊按照邊權排序,然后開始挑選邊。為什么是挑選邊呢?不是越小的邊越好嗎,為什么還要挑?

邊權小固然好,但是不要忘記我們有一個大前提:我們要建的是樹,它里面不能存在環。也就是說,假如我看到一條邊,但是這條邊連着的兩個點在我建的樹里已經連通了,那這條邊還需要再加進來嗎?很顯然不用。這樣說可能有點無法理解,我們拿一張圖來具體操作一下:

我們的邊排序后從小到大依次為(結點 結點 邊權):

①1 2 1②1 3 1③4 6 3④5 6 4⑤2 3 6⑥4 5 7⑦3 4 9⑧2 4 11⑨3 5 13

要建的最小生成樹一開始長這樣:

我們的操作就是往這棵樹里加邊,其操作過程如下圖所示:

直到目前為止,一切都很順利。輪到我們的第五條邊2 3 6了。我們發現,2和3已經連通,通過1作為中轉,這時候我們就不能將2 3 6這條邊加入到我們的樹中了。同理,下一條⑥號邊同樣也要跳過,因為4和5已經連通了。接下來,我們加入3 4 9這條邊,如圖所示:

我們發現我們建的樹里每個結點都已經連接了,剛好用了5條邊,也就是我們說的n-1條邊,算法結束。

算法的實現並不難,最為難的是如何判斷兩點是否已經連通。我們可以用深搜或廣搜來解決,但顯然效率極低,因此,我們需要借助一種強大的數據結構:並查集。並查集的最強大的功能就是可以快速地判斷兩個元素是否在同一集合內(祖先是否相同),所以我們借助它來判斷兩點是否連通。

主要代碼:

struct edge
{
	int u,v,w;
}a[100001];
//邊集數組 
int boss[10001];//並查集,boss[i]表示i的祖先 
int find(int x)
{
	if(boss[x]==x)return x;//找到祖先
	else
	{
		boss[x]=find(boss[x]);//路徑壓縮 
		return boss[x];	
	} 
}
void Kurscal()
{
	int i;
	for(i=1;i<=n;i++)boss[i]=i;//初始化
//n個結點,每個結點的祖先默認為它自己,也就是每個結點自己一個集合
	stable_sort(a+1,a+1+m,cmp);//m條邊,將邊按照邊權從小到大排序
	int cnt=0;//當前最小生成樹里邊的數量 
	int len=0;//當前最小生成樹邊權總和 
	for(i=1;i<=m;i++)
	{);
		int x=find(a[i].u),y=find(a[i].v
		//x表示a[i].u的祖先,y表示a[i].v的祖先
		if(x!=y)
		//說明兩點不在同一集合內,即這兩點不連通
		{
			boss[x]=y;//標記祖先 
			cnt++;//邊數增加 
			len+=a[i].w;//邊權和增加 
		} 
		if(cnt==n-1)break;
		//如果已經選了n-1條邊,那最小生成樹就建好了 
	}
}

(2)prim:

除了通過加入邊來建樹,我們還有沒有其它的方法了呢?Kurscal中,我們加入邊,是在我們固定了結點的情況下完成的。也就是,我們這時候不在乎這些結點,我們只在乎連接它們的邊。那,我們可以不可以在乎一下這些結點呢(雨露均沾)?這時候,我們建樹的過程就不是添加邊了,而是添加點。那一開始,我們的樹就應該長這樣(有點尷尬):

我們一開始應該先找一個根結點,這個根結點可以是任何一個結點,因為最后每一個結點都會兩兩連通,哪一個作為根就無所謂了。那么,每一次我們都要選一個點加入我們的最小生成樹,這個點必須滿足什么條件呢?它距離當前樹上的與它最近的結點的距離必須是每一個結點距離樹上離它們各自最近的結點的距離中最小的,實際上說的就是每一次要找到一個距離“最小生成樹”最近的結點。我們仍舊以上面的例子來模擬:

首先,1號點為根,這時候距離這棵樹(1號點)最近的是2號點,我們將2號點加入樹:

接下來,距離這棵樹最近的結點是3號點,我們將3號點加入樹:

接下來的操作請讀者自己手動模擬,算法只有手動模擬了才能身臨其境的感受其思想的真諦(跑題了

加入點的順序應該是:1->2->3->4->5->6。注意我們每次選點的時候要選的是樹以外的結點,否則一開始就會出現一種非常尷尬的局面:第一輪,找結點,發現根結點到根結點距離為0,選擇根結點;第二輪,發現根結點到根結點距離為0,又選根結點......這就陷入了死循環。對於已經在樹里的結點,我們是沒有必要再去動它了,因為我們的目的就是將所有點插入到樹里,那你已經在樹里的我還管你干什么?所以我們需要有一個數組來記錄結點是否已經在樹里。

在選點的過程中,我們需要按照上面說的一大串話那樣,比較樹外的每一個結點到樹上的每一個結點的距離嗎?我們在那下面說了:“實際上說的就是每一次要找到一個距離“最小生成樹”最近的結點。”我們有沒有辦法來記錄每一個結點到最小生成樹的距離呢?當然有!我們可以開一個數組dis,有沒有發現,和我們的最短路算法中的Dijkstra算法有異曲同工之妙?不懂得可以看一下最短路算法分析。這兩種算法都是不斷加入點進行拓展,從而得出整張圖的最短路或最小生成樹。Dijkstra中dis表示的是結點到源點的距離,這里就是把源點擴大成了一棵樹,其思想並沒有任何改變,我們仍舊可以把那棵樹當作一個點來看待。那么,在加入點后,我們需要用這個點來刷新一下其它非樹結點(不在樹上的結點)到樹的距離,這和Dijkstra的松弛是一模一樣的!令人贊嘆的是,這兩種算法解決的問題不同,它們的過程竟然完全一樣!

主要代碼:

struct edge
{
	int last,to,len;
}a[100001];
int first[10001],len=0;
//鄰接表 
bool f[10001];//記錄是否在樹上
int dis[10001];//記錄結點到樹的距離 
void add......//存邊
void prim()
{
	int i;
   for(i=1;i<=n;i++)dis[i]=999999;//初始化
	int cnt=0;//樹內點的數量
	int sum=0;//樹內邊權總和 
	dis[1]=0;
	f[1]=1;
	cnt=1;
	//先確定根結點,一般以1作為根結點
	while(cnt<n)//直到n個結點均在樹上 
	{
		int id,minn=1000001;
		//id記錄找到的結點的編號,minn是它到樹的距離 
		for(i=1;i<=n;i++)
			if(f[i]==0&&dis[i]<minn)
			{
				id=i;
				minn=dis[i];
			}
		f[id]=1;
		cnt++;
		//將這個點加入樹
		sum+=dis[id];
		//刷新邊權總和 
		for(i=first[id];i;i=a[i].last)
		//刷新結點到樹的距離 
			if(f[a[i].to]==0&&a[i].len<dis[a[i].to])
				dis[a[i].to]=a[i].len;
	} 
}

總結一下兩種算法:Kurscal算法是將森林里的樹逐漸合並,prim算法是在根結點的基礎上建起一棵樹。

可能有的同學會誤解:dis代表結點到樹的距離,那這個距離一定只包含一條邊嗎?在這里,距離只能有一條邊。為什么呢?我們每一次是要往樹里加一個點的,那如果這個距離經過了不止一條邊,那就不滿足我們的需求了。這一點要和Dijkstra區別開,Dijkstra是單純的距離,而prim是只經過一條邊的距離。這樣的話,即使在存在負邊權,求得的dis不是真正意義上的最短距離,也不會影響我們最終的結果。

我們的過程實際上是每一次添加一個點,然后逐漸建起一棵樹,我們並不是真的希望這個點到我們的樹是最近的,我們只希望這個點加入我們的最小生成樹后可以滿足我們貪心的要求:局部最優導致整體最優,這個局部指的是我們最小生成樹的邊權,而並不是真正意義上的距離。這一點一定要好好理解!

同樣,prim算法也可以堆優化,那么堆里存的就是結點的編號和它到樹的距離,和Dijkstra的堆優化基本一樣,希望讀者自己嘗試去實現。


因為Kurscal涉及大量對邊的操作,所以它適用於稀疏圖;普通的prim算法適用於稠密圖,但堆優化的prim算法更適用於稀疏圖,因為其時間復雜度是由邊的數量決定的。

讀者如有疑問或發現錯誤歡迎在評論區留言!

By wxn


免責聲明!

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



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