1. 程式人生 > 實用技巧 >最小生成樹的常用演算法模板

最小生成樹的常用演算法模板

關於最小生成樹的話,其實很早之前就接觸了,當時也寫了一篇關於最小生成樹的文章,但一直沒有好好刷題。

最小生成樹的演算法沒有其他演算法那麼複雜,演算法思想比較簡單,程式碼也比較容易。

常見的最小生成樹演算法由Kruskal演算法和Prim演算法。

1.Kruskal演算法 -- 時間複雜度\(O(m * log m)\)

演算法思想:

  1. 建立一個並查集,每個點構成一個集合;
  2. 將邊進行從小到大進行排序,依次掃描邊edge(u,v,w);
  3. 如果u和v 屬於同一個集合,那麼跳過這輪迴圈;
  4. 如果不屬於同一個集合,則把u、v合併為同一個集合;
  5. 當集合中的頂點數為n或者掃遍所有邊edge,則構成最小生成樹。
#include<iostream> 
#include<algorithm>
#include<cstdio>
using namespace std;
const int manx=1e5+5; //對應頂點數目
const int mamx=1e5+5; //對應邊的數目
int n,m,u,v,total=1;
struct edge{
    int start,to;
    long long val;
}bian[mamx];
int a[manx];
long long ans;
int find(int x) //並查集
{
    if(a[x]==x) return x;
    else return a[x]=find(a[x]);
}
bool cmp(edge x,edge y)
{
    return x.val<y.val;
}
inline void kruskal()
{
    for(int i=1;i<=m;i++)
    {
        u=find(bian[i].start);
        v=find(bian[i].to);
        if(u==v) continue; //如果兩個點存在於同一個集合則跳過迴圈
        ans+=bian[i].val;
        a[u]=v;
        total++;
        if(total==n-1) break;
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) //並查集的初始化,祖先為自己
        a[i]=i; 
    for(int i=1;i<=m;i++)
        scanf("%d%d%d",&bian[i].start,&bian[i].to,&bian[i].val);
    sort(bian+1,bian+1+m,cmp);
    kruskal();
    cout<<ans;
    return 0;
}

2.Prim演算法

Prim演算法跟最短路中的Dijkstra 演算法思想相近,有興趣的可以瞭解一下:
https://www.cnblogs.com/RioTian/p/12597634.html

演算法思想:
1.把1作為起點加入最小生成樹集合S;
2.在未訪問過的集合T中找出一點距離集合S最近的點,並在T中將其剔除,移入S中;
3.重複2步驟,直到所有點加入S。

樸素演算法 時間複雜度 \(O(n^2)\)

const int inf=2147483647;
int a[manx][manx], d[manx], n, m, ans;
bool vis[manx];
void prim(){
	for(int i=1;i<=n;i++) d[i]=inf,vis[i]=0,a[i][i]=0; //初始化各陣列
	d[1]=0; //1作為起點
	for(int i=1;i<n;i++) //重複n-1次
	{
		int x=0;
		for(int j=1;j<=n;j++)
			if( !vis[j] && (x==0||d[j]<d[x]) ) //尋找未訪問過且離集合S最近的點
				x=j;
		vis[x]=1;
		for(int y=1;y<=n;y++)  //第一輪d[y]中由起點可到達的點得到更新
			d[y]=min( d[y ], a[x][y]) //這裡與Dij不同,因為Dij求的是兩點間的距離,而這裡是點到集合距離
		}
}

Prim演算法使用堆優化可達到與Kruscal一樣的複雜度 \(O(m * log m)\)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int manx=5e3+5;
const int mamx=1e5+5;
int k,n,m,cnt,sum,a,b,c;
int head[manx],dis[manx],vis[manx];
struct node{
    int v,w,next;
}e[mamx];
typedef pair<int,int> p;
priority_queue<p,vector<p>,greater<p> >q; //堆優化
void add(int u, int v, int w) //鏈式前向星建圖
{
    e[++k].v=v;
    e[k].w=w;
    e[k].next=head[u];
    head[u]=k;
}
int main()
{
    memset(dis,127,sizeof(dis));
    memset(head,-1,sizeof(head));
    scanf("%d%d",&n,&m);
    for(R i=1;i<=m;i++)
    {
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
      //無向圖   add(b,a,c);
    }
    dis[1]=0; //一般將1作為最小生成樹擴充套件的起點
    q.push(make_pair(0,1));
    while(!q.empty() && cnt<n)  //仔細觀察可以發現堆優化的Prim也和堆優化的Dij的程式碼實現有很多相似之處,這是因為兩者都是基於貪心的演算法
    {
        int d=q.top().first,u=q.top().second;
        q.pop();
        if(vis[u]) continue;
        cnt++;  //計算S集合中頂點的個數
        sum+=d;  //計算最小權值和
        vis[u]=1; 
        for(R i=head[u];i;i=e[i].next)
            if(e[i].w<dis[e[i].v])
                dis[e[i].v]=e[i].w,q.push(make_pair(dis[e[i].v],e[i].v));
    }
    printf("%d",sum);
}	

儘管堆優化,但不如直接使用Kruskal演算法更加方便,因此,稀疏圖用Kruskal,稠密圖用Prim 。