這里介紹最小生成樹的兩種方法:Prim和Kruskal。
各種Bug於2018-9-27日修復
兩者區別:Prim在稠密圖中比Kruskal優,在稀疏圖中比Kruskal劣。Prim是以更新過的節點的連邊找最小值,Kruskal是直接將邊排序。
兩者其實都是運用貪心的思路
洛谷數據:
Prim:
個人覺得Prim和最短路中的dijkstra很像,由於速度問題,所以這里我用鏈式前向星存圖。Prim的思想是將任意節點作為根,再找出與之相鄰的所有邊(用一遍循環即可),再將新節點更新並以此節點作為根繼續搜,維護一個數組:dis,作用為已用點到未用點的最短距離。
證明:Prim算法之所以是正確的,主要基於一個判斷:對於任意一個頂點v,連接到該頂點的所有邊中的一條最短邊(v, vj)必然屬於最小生成樹(即任意一個屬於最小生成樹的連通子圖,從外部連接到該連通子圖的所有邊中的一條最短邊必然屬於最小生成樹)
具體算法流程圖解如下:
注意:inline和register為一點點常數優化,不要的話也可以過,不理解的同學刪掉即可
#include<bits/stdc++.h>
using namespace std;
#define re register
#define il inline
il int read()
{
re int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*f;
}//快讀,不理解的同學用cin代替即可
#define inf 123456789
#define maxn 5005
#define maxm 200005
struct edge
{
int v,w,next;
}e[maxm<<1];
//注意是無向圖,開兩倍數組
int head[maxn],dis[maxn],cnt,n,m,tot,now=1,ans;
//已經加入最小生成樹的的點到沒有加入的點的最短距離,比如說1和2號節點已經加入了最小生成樹,那么dis[3]就等於min(1->3,2->3)
bool vis[maxn];
//鏈式前向星加邊
il void add(int u,int v,int w)
{
e[++cnt].v=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt;
}
//讀入數據
il void init()
{
n=read(),m=read();
for(re int i=1,u,v,w;i<=m;++i)
{
u=read(),v=read(),w=read();
add(u,v,w),add(v,u,w);
}
}
il int prim()
{
//先把dis數組附為極大值
for(re int i=2;i<=n;++i)
{
dis[i]=inf;
}
//這里要注意重邊,所以要用到min
for(re int i=head[1];i;i=e[i].next)
{
dis[e[i].v]=min(dis[e[i].v],e[i].w);
}
while(++tot<n)//最小生成樹邊數等於點數-1
{
re int minn=inf;//把minn置為極大值
vis[now]=1;//標記點已經走過
//枚舉每一個沒有使用的點
//找出最小值作為新邊
//注意這里不是枚舉now點的所有連邊,而是1~n
for(re int i=1;i<=n;++i)
{
if(!vis[i]&&minn>dis[i])
{
minn=dis[i];
now=i;
}
}
ans+=minn;
//枚舉now的所有連邊,更新dis數組
for(re int i=head[now];i;i=e[i].next)
{
re int v=e[i].v;
if(dis[v]>e[i].w&&!vis[v])
{
dis[v]=e[i].w;
}
}
}
return ans;
}
int main()
{
init();
printf("%d",prim());
return 0;
}
Kruskal:
Kruskal算法的思想比Prin好理解一些。先把邊按照權值進行排序,用貪心的思想優先選取權值較小的邊,並依次連接,若出現環則跳過此邊(用並查集來判斷是否存在環)繼續搜,直到已經使用的邊的數量比總點數少一即可。
證明:剛剛有提到:如果某個連通圖屬於最小生成樹,那么所有從外部連接到該連通圖的邊中的一條最短的邊必然屬於最小生成樹。所以不難發現,當最小生成樹被拆分成彼此獨立的若干個連通分量的時候,所有能夠連接任意兩個連通分量的邊中的一條最短邊必然屬於最小生成樹
具體算法流程圖解如下:
並查集詳解
#include<bits/stdc++.h>
using namespace std;
#define re register
#define il inline
il int read()
{
re int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*f;
}
struct Edge
{
int u,v,w;
}edge[200005];
int fa[5005],n,m,ans,eu,ev,cnt;
il bool cmp(Edge a,Edge b)
{
return a.w<b.w;
}
//快排的依據(按邊權排序)
il int find(int x)
{
while(x!=fa[x]) x=fa[x]=fa[fa[x]];
return x;
}
//並查集循環實現模板,及路徑壓縮,不懂並查集的同學可以戳一戳代碼上方的“並查集詳解”
il void kruskal()
{
sort(edge,edge+m,cmp);
//將邊的權值排序
for(re int i=0;i<m;i++)
{
eu=find(edge[i].u), ev=find(edge[i].v);
if(eu==ev)
{
continue;
}
//若出現兩個點已經聯通了,則說明這一條邊不需要了
ans+=edge[i].w;
//將此邊權計入答案
fa[ev]=eu;
//將eu、ev合並
if(++cnt==n-1)
{
break;
}
//循環結束條件,及邊數為點數減一時
}
}
int main()
{
n=read(),m=read();
for(re int i=1;i<=n;i++)
{
fa[i]=i;
}
//初始化並查集
for(re int i=0;i<m;i++)
{
edge[i].u=read(),edge[i].v=read(),edge[i].w=read();
}
kruskal();
printf("%d",ans);
return 0;
}