最小生成樹兩種算法詳解


最小生成樹

眾所周知, 樹是一種特殊的圖, 是由n-1條邊連通n個節點的圖.

如果在一個有n個節點的無向圖中, 選擇n-1條邊, 將n個點連成一棵樹, 那么這棵樹就是這個圖的一個生成樹.

如果保證樹的邊權和最小, 那么這棵樹就是圖的最小生成樹.

為了求一棵樹的最小生成樹, 有兩種算法, 一種是選擇點加入樹的Prim算法, 另一種是選擇邊加入樹的Kruskal算法.

Prim算法

這個算法的過程和Dijkstra類似, 但有所不同.

首先選擇任意一點作為樹的第一個節點0, 枚舉與它相連的所有點i, 將兩點之間的邊權記為這個點到生成樹的距離b[i], 選擇距離最近的點加入生成樹, 然后枚舉與之相鄰的節點j, 用邊權a[i,j]更新b[j], 使其等於min(b[j],a[i,j]), 這樣再繼續加入當前離生成樹最近的點, 在更新它相鄰的點, 以此類推, 直到所有點全部加入生成樹. 這樣, 便求出了最小生成樹.

關於正確性

我自己的思路是這樣的: 如果用Prim算法求出了一棵最小生成樹, 將一條邊u換成另一條更小的v, 就得到一棵邊權和更小的生成樹. 首先保證樹連通, 所以去掉u和v, 生成樹被分成兩個連通塊是一模一樣的. 在當時連接u的時候, 已經決策完的生成樹一定也和v相連, 這時v連接的節點一定會比u連接的節點更早加入, 所以一開始的假設不成立, 算法正確.

具體代碼實現

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
int n,m,l,r,x,a[5005][5005]/*鄰接矩陣*/,b[5005]/*點到生成樹的最短邊權*/,now/*當前加入的點*/,k=1/*生成樹節點數*/,ans=0/*生成樹總邊權和*/;
bool vsd[5005]={0};
void update(int at){//用節點at更新其他點的b[]值
	for(int i=1;i<=n;i++) {
		b[i]=min(a[at][i],b[i]);
	}
	vsd[at]=true;
	return;
}
int find(){//尋找當前離生成樹最近的點
	int ft=0;
	for(int i=1;i<=n;i++){
		if(!vsd[i]){//不在樹中
			if(b[i]<=b[ft]){
				ft=i;
			}
		}
	}
	return ft;
}
int main(){
	cin>>n>>m;
	memset(a,0x3f,sizeof(a));
	for(int i=1;i<=n;i++){
		a[i][i]=0;
	}
	for(int i=1;i<=m;i++){
		cin>>l>>r>>x;
		a[l][r]=min(a[l][r],x);//防止有兩個點之間出現邊權不同的幾條邊
		a[r][l]=min(a[r][l],x);
	}
	memset(b,0x3f,sizeof(b));
	update(1);
	while(k<n){//加入n-1個點后返回(第一個點本來就在樹中, 無需加入)
		now=find();//加入最近的點now
		ans+=b[now];//統計答案
		update(now);//更新其他點
		k++;//統計點數
	}
	cout<<ans<<endl;
	return 0;
}

Kruskal算法

這個算法和Prim相反, 它是將邊記為樹上的邊, 最終得到一棵最小生成樹.

將所有邊按邊權排序, 然后將它們從小到大討論是否加入生成樹. 如果該邊的兩個端點屬於同一個連通塊, 這時加入該邊就會形成環, 不符合樹的定義, 所以舍棄. 如果該邊兩個端點不屬於同一個連通塊, 那么連接該邊, 將兩個端點所在連通塊連成一個.

當共加入n-1條邊的時候, 就得到了一棵最小生成樹.

對於查找兩點是否在同一個連通塊中的方法, 我們可以使用並查集來維護點之間的連通關系.

正確性簡易說明

Kruskal相對來說更好理解, 因為從小到大排序后, 使用被舍棄的邊連成環是非法的, 使用排在后面的合法的邊替換已經選擇的邊, 得到的答案不是最優的. 所以Kruskal算法正確.

代碼實現

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n,m,fa[10005],s,e,l,k=0,ans=0;
struct side{
	int le,ri,len;//起點, 終點, 邊權
}a[200005];
bool cmp(side x,side y){//結構體sort規則
	return(x.len<y.len);
}
int find(int x){//並查集尋找最老祖先
	if(fa[x]==x){//自己就是當前連通塊最老祖先
		return x;
	}
	fa[x]=find(fa[x]);//自己祖先的最老祖先
	return fa[x];
}
int main(){
	cin>>n>>m;
	memset(a,0x3f,sizeof(a));
	for(int i=1;i<=m;i++){
		cin>>s>>e>>l;
		a[i].le=s;//結構體存儲邊
		a[i].ri=e;
		a[i].len=l;
	}
	sort(a+1,a+m+1,cmp);//按邊權升序排列
	for(int i=1;i<=n;i++){
		fa[i]=i;//初始化並查集
	}
	int i=0;
	while((k<n-1/*加入了n-1個點跳出*/)&&(i<=m/*枚舉完了所有的邊跳出*/)){
		i++;
		int fa1=find(a[i].le),fa2=find(a[i].ri);//兩個端點的最老祖先
		if(fa1!=fa2){//不在同一連通塊
			ans+=a[i].len;//記錄答案
			fa[fa1]=fa2;//連接連通塊
			k++;//記錄邊數
		}
	}
	cout<<ans<<endl;
	return 0;
}

之前發的是筆記, 現在發的是實戰總結


免責聲明!

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



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