最小生成樹(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。
【分析】給出最小生成樹,並且說明了該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;
}