1. 程式人生 > >最小生成樹(MST)

最小生成樹(MST)

目錄

一、知識點

二、例題


一、知識點

1. 生成樹定義:在一個有n個點的無向連通圖中,取其n-1條邊並連線所有的頂點,所得到的子圖稱為原圖的一棵生成樹。

2. 樹的屬性:無環+連通+任意兩點之間只有唯一的簡單路徑+刪掉任意邊就不連通

3. 最小生成樹:各邊權和最小的一棵生成樹。

4. 最小邊原則:圖中權值最小的邊(如果唯一的話)一定在最小生成樹上

5. 唯一性定理:對於一個圖,如果各邊權值不等,則圖的MST一定是唯一的,反之不成立


計算無向圖的最小生成樹

1. Prime

演算法思路:貪心
(1)最初將無向連通圖分成兩個頂點集合A、B,任選一個頂點a先放到A,將B中與a有關並且權值最小的點加到A,知道n個頂點全部屬於A結束。

  • 注意這裡的d陣列,存的是到樹的最短路徑,不是到源點的最短路徑。這裡注意區別單源最短路徑。所以,每次更新d陣列時,比較的是d[i]與g[k][i]而不用加上ans。

(2)顯然出發點不同,最小生成樹的形態就不同,但邊權和的最小值是唯一的。

複雜度:O(N^2)

//prime演算法 
#include<bits/stdc++.h>
using namespace std;

const int inf=0x3f3f3f3f;
const int maxn=505;
int vis[maxn];//標記頂點i是否加入最小生成樹中
int d[maxn];//表示點i與當前生成樹中的點有連邊的邊長的最小值
int g[maxn][maxn];//存邊權
int n,m,ans;

void read()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)g[i][j]=inf;
	for(int i=1;i<=m;i++)
	{
		int x,y,w;
		scanf("%d%d%d",&x,&y,&w);
		g[x][y]=g[y][x]=w;
	}
}
void Prim(int v0)
{
	memset(vis,0,sizeof(vis));//初始化生成樹點集
	for(int i=1;i<=n;i++)d[i]=inf;
	d[v0]=0;ans=0;
	int minn,k;
	for(int i=1;i<=n;i++)//選擇n個點
	{
		minn=inf;
		for(int j=1;j<=n;j++)
			if(!vis[j] && minn>d[j])
			{ 
				minn=d[j];
				k=j; 
			}
		vis[k]=1;//標記;
                ans+=d[k];//算最小生成樹的邊權和
		for(int j=1;j<=n;j++)//修改d陣列
			if(!vis[j] && d[j]>g[k][j])//這裡注意區別單源最短路徑
				d[j]=g[k][j]; 
	 } 
} 
int main()
{
	read();
	Prim(1);
	cout<<ans<<endl;
	return 0;
}

2. Kruskal

演算法思路:貪心
(1)將圖中的所有邊都去掉。
(2)將邊按權值由小到大新增到圖中,並保證新增的過程中不會形成環
(3)重複上一步直到連線所有頂點

該方法用到了並查集來判斷是否會產生環

複雜度:O(mlogm+mα(n) )   //α(n)是一次並查集的複雜度

//kruskal演算法
#include<bits/stdc++.h>
using namespace std;

const int maxn=1e5+10;

struct edge{
	int x,y,z;
}a[maxn];

bool cmp(edge x,edge y)
{
	return x.z<y.z;
}

int n,m,pre[maxn],ans,flag;

int findd(int x)
{
	if(pre[x]==x)return x;
	pre[x]=findd(pre[x]);
	return pre[x];
}
void kruskal()
{
	for(int i=1;i<=n;i++)pre[i]=i;
	int k=0;
	for(int i=1;i<=m;i++)
	{
		int f1=findd(a[i].x);
		int f2=findd(a[i].y);
		if(f1!=f2)
		{
			ans+=a[i].z;
			pre[f1]=f2;
			k++;
			if(k==n-1)break;//最小生成樹的邊數為n-1 
		}
	}
	if(k<n-1)
	{
		puts("impossiable");
		flag=0;
		return;
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	ans=0;flag=1;
	for(int i=1;i<=m;i++)
		scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
	sort(a+1,a+1+m,cmp);
	kruskal();
	if(flag)printf("%d\n",ans);
	return 0;
}

二、例題

1. 【loj】#10064. 「一本通 3.1 例 1」黑暗城堡(最短路徑生成樹 dijkstra+Prim)

2. 【loj】#10066. 「一本通 3.1 練習 1」新的開始 (最小生成樹·Prim)

3. 【loj】#10067. 「一本通 3.1 練習 2」構造完全圖(最小生成樹 Kruskal)

題目描述:

對於完全圖 G,若有且僅有一棵最小生成樹為 T,則稱完全圖 G 是樹 T 擴展出的。

給你一棵樹 T,找出 T 能擴展出的邊權和最小的完全圖 G。

題目連結:https://loj.ac/problem/10067

【分析】給出最小生成樹,並且說明了該MST形態唯一。類比Kruskal演算法。並查集。

先把邊按權值由小到大排序,然後遍歷邊。注意,邊數是n-1;

把圖的頂點集分為兩個集合。集合中的點數size[x], size[y],因為是完全圖,所以任意兩點之間都是有邊直接相連的。

所以連通兩個集合使其變成完全圖一共需要cnt=size[x]*size[y]條邊

而遍歷邊的時候,已經存在一條,所以只需要再加cnt-1條邊即可。而邊權比新加入的這條邊的邊權+1。

所以核心式子就是(size[x]*size[y]-1)*(a[i].d+1)

注意計算的時候,要強制轉換為long long....不然!就一直wa....╥﹏╥

這裡講得很棒!

【程式碼】

#include<bits/stdc++.h>
using namespace std;

const int maxn=1e5+10;
typedef long long ll;

int pre[maxn],size[maxn];
ll ans,n;

struct node{
	int l,r,d;
}a[maxn];
bool cmp(node x,node y)
{
	return x.d<y.d;
}

int findd(int x)
{
	return x==pre[x]?x:pre[x]=findd(pre[x]);
}

int main()
{
	ans=0;
	scanf("%lld",&n);
	for(int i=1;i<n;i++)
	{
		scanf("%d%d%d",&a[i].l,&a[i].r,&a[i].d);
		pre[i]=i;size[i]=1;
		ans+=a[i].d;	
	}
	pre[n]=n;size[n]=1;
	sort(a+1,a+n,cmp);//n-1條邊
	for(int i=1;i<n;i++)
	{
		int x=findd(a[i].l);
		int y=findd(a[i].r);
		if(x!=y)
		{
			ans+=(ll)(size[x]*size[y]-1)*(a[i].d+1);//這裡!!!wa了好幾次的起源...
			pre[x]=y;
			size[y]+=size[x];
		}
	 } 
	printf("%lld\n",ans);
	return 0;
}