[總結]最小生成樹之Kruskal算法



一、最小生成樹的相關知識

1. 樹的性質

樹實際上是圖的特殊形態,
對於任意一張無向連通圖\(G=(V,E)\),其中\(n=|V|,m=|E|\)
該圖為樹時的性質:

  1. |E|=|V|-1
  2. 圖中無環
  3. 圖連通
  4. 任意兩點有且僅有一條簡單路徑
  5. 刪除任意一條邊后該圖不連通

2. 生成樹

在一個有\(|V|\)個點的無向連通圖中,取其中\(|V|-1\)條邊,並連接所有的頂點,所得到的子圖為原圖的生成樹(Spanning Tree)。

3. 最小生成樹

在一個帶權無向連通圖中,各邊權和最小的一棵生成樹即為原圖的最小生成樹(Minimum Spanning Tree,MST)。

4. 最小生成樹的性質

1. 最小邊原則:圖中權值最小的邊(唯一)一定在最小生成樹上。
2.唯一性定理:對於一個圖\(G\),如果圖中邊權都不相同,那么該圖的最小生成樹唯一(形態不一定唯一)。


二、Kruskal算法求最小生成樹

求出一個圖的最小生成樹共有兩種算法:Prim算法Kruskal算法
由於Prim算法的實用性較低,因此這里不介紹該算法。

1. 核心思想

Kruskal算法是一種貪心思想,將邊權按權值由小到大排序,並從剩下的邊集中選擇權值最小且兩個端點不在同一集合的邊加入生成樹中,重復該操作直到加入了\(n-1\)條邊。

2. 具體流程

  1. 將邊權由小到大排序。
  2. 建立並查集,每個點都構成一個集合。
  3. 掃描每一條邊(x,y,z),若發現x,y在同一集合,那么舍棄這條邊;否則累計z到答案中並合並x,y到同一集合。
  4. 重復操作3直到生成樹中包含n-1條邊。若所有邊掃描完畢且生成樹的邊數不到n-1,那么該圖不存在最小生成樹。

總時間復雜度為:\(O(mlogm+m\alpha (n))\),其中\(\alpha (n)\)是一次並查集的復雜度。

3. 圖示

以下組圖具體描述了Step:0中的一個圖求出最小生成樹的過程。
圖片20.png

圖片21.png
Step:1初始每個點都是一個集合。

圖片22.png
Step:2當前最小邊權為2,因此將點1,2合並到同一個集合。

圖片23.png
Step:3當前最小邊權為3,因此將點3,5合並到同一個集合。

圖片24.png
Step:4當前最小邊權為6,點3,4不在同一集合,因此將點3,4合並到同一個集合。

圖片25.png
Step:5當前最小邊權為7,由於此時點4,5已經在同一集合,因此將點3,4合並到同一個集合。

圖片26.png
Step:6當前最小邊權為8,此時點2,3不在同一集合,因此將點2,3合並到同一個集合。又由於此時(m==n-1),故算法結束並返回答案:Ans=19。

以上,求圖中最小生成樹的問題得以解決。

4. 代碼實施

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
int n,m,ans,cnt,fa[500100];
struct node{
	int u,v,w;
	bool friend operator <(node a,node b){
		return a.w<b.w;
	}
}edge[500010];
inline int Find(int x){return fa[x]==x? x:fa[x]=Find(fa[x]);}
void Kruskal(){
	sort(edge+1,edge+m+1);//邊權排序
	for(int i=1;i<=m;i++){
		int fu=Find(edge[i].u);
		int fv=Find(edge[i].v);
		if(fu==fv) continue;//已經在同一集合,由於再加入這條邊會形成環,因此不考慮這條邊
		cnt++;//累計生成樹中邊的條數
		ans+=edge[i].w;//累計答案
		fa[fu]=fv;//合並到同一集合
		if(cnt==n-1) break;//已經得出最小生成樹
	}
	if(cnt<n-1){//無法形成生成樹
		cnt=0;return;
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++) scanf("%d%d%d",&edge[i].u,&edge[i].v,&edge[i].w);
	for(int i=1;i<=n;i++) fa[i]=i;//並查集初始化
	Kruskal();
	if(cnt) printf("%d",ans);
	else printf("No Answer");
	return 0;
}

三、例題

例1:P2212 [USACO14MAR]澆地Watering the Fields

P1546 最短網絡 Agri-NetP1111 修復公路P1195 口袋的天空
模板題,具體解釋見代碼。
Code:

#include<bits/stdc++.h>
#define N 2500
using namespace std;
int n,c,m,px[N],py[N];
int pre[N],ans,cnt;
struct node{
	int u,v,dis;
	bool operator <(const node &other) const{
		return dis<other.dis;
	}
}q[20000000];
inline int find(int x){
	return pre[x]==x ? x:pre[x]=find(pre[x]);
}
inline void Kruskal(){
	for(int i=1;i<=n;i++) pre[i]=i;
	for(int i=1;i<=m;i++){
		int fu=find(q[i].u);
		int fv=find(q[i].v);
		if(fu!=fv){
			pre[fu]=fv;
			ans+=q[i].dis;
			cnt++;
		}
		if(cnt==n-1) break;
	}
	if(cnt==n-1) printf("%d",ans);
	else printf("-1");
}
int main()
{
	scanf("%d%d",&n,&c);
	for(int i=1;i<=n;i++){
		scanf("%d%d",&px[i],&py[i]);
		for(int j=1;j<i;j++){
			int dis=(px[i]-px[j])*(px[i]-px[j])+(py[i]-py[j])*(py[i]-py[j]);//計算費用
			if(dis<c) continue;//費用小於C的水管不會被安裝,因此需要不建邊
			q[++m].u=i,q[m].v=j,q[m].dis=dis;
		}
	}
	sort(q+1,q+m+1);
	Kruskal();
		return 0;
}

例2:P1550 [USACO08OCT]打井Watering Hole

我們唯一需要解決的問題在於第一口井打在哪里。不妨設一個超級原點,並把每口井與該點相連,邊權就是打井的費用。在新圖上跑最小生成樹,這樣就能巧妙地解決問題。
P1194 買禮物
Code:

#include<bits/stdc++.h>
using namespace std;
int cnt,pre[200000],num,ans,n;
struct node{
	int u,v,w;
	bool operator <(const node &other) const{
		return w<other.w;
	}
}e[200000];
inline void add_edge(int u,int v,int w){
	cnt++;
	e[cnt].u=u;e[cnt].v=v;e[cnt].w=w;
}
inline int Find(int x){
	return x==pre[x]? x:pre[x]=Find(pre[x]);
}
void Kruskal(){
	for(int i=1;i<=cnt;i++){
		int fu=Find(e[i].u);
		int fv=Find(e[i].v);
		if(fu==fv) continue;
		pre[fu]=fv;
		num++;
		ans+=e[i].w;
		if(num==n) break;//由於包含了超級原點的這一條邊,因此最后有n條邊 
	}
}
int main()
{
	int u,v,w;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){//與超級原點建邊 
		scanf("%d",&w);
		add_edge(0,i,w);
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++){
			scanf("%d",&w);
			if(i==j) continue;
			add_edge(i,j,w);
		}
	sort(e+1,e+cnt+1);
	for(int i=0;i<=n;i++) pre[i]=i;
	Kruskal();
	printf("%d",ans);
	return 0;
}

例3:P1547 Out of Hay

求最小生成樹中最大的一條邊,每次把邊加入生成樹中不斷取最大值即可。
或者,根據其貪心思想,越晚加進來的邊權值越大,因此只要讓邊權不斷賦值給答案就能求出最大值。

例4:P1340 獸徑管理

每次只增加一條邊,因此用Kruskal算法十分方便。
正常排序后標記產生道路的時間,循環m次最小生成樹求的答案。
Code:

#include<bits/stdc++.h>
using namespace std;
int pre[1000000],n,m,ans,num;
struct node
{
	int u,v,w,p;
	bool operator <(const node &other)const{
		return w<other.w;
	}
}e[1000000];
inline int Find(int x)
{
	if(pre[x]==x) return x;
	pre[x]=Find(pre[x]);
	return pre[x];
}
void Kruskal(int p)
{
	ans=0;num=0;
	for(int i=1;i<=n;i++) pre[i]=i;
    for(int i=1;i<=m;i++)
    {
    	int fu=Find(e[i].u);
    	int fv=Find(e[i].v);
    	if(fu==fv||e[i].p>p) continue;
    	pre[fu]=fv;
    	ans+=e[i].w;
    	num++;
    	if(num==n-1) {
    		printf("%d\n",ans);break;
		}
	}
	if(num<n-1){
		printf("-1\n");
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		e[i].u=u;e[i].v=v;e[i].w=w;
		e[i].p=i;
	}
	sort(e+1,e+m+1);
	for(int i=1;i<=n;i++) pre[i]=i;
	for(int i=1;i<=m;i++) Kruskal(i);
	return 0;
}

pic.png


免責聲明!

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



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