圖論——最小生成樹:Prim算法及優化、Kruskal算法,及時間複雜度比較

最小生成樹:ios

  一個有 n 個結點的連通圖的生成樹是原圖的極小連通子圖,且包含原圖中的全部 n 個結點,而且有保持圖連通的最少的邊。簡單來講就是有且僅有n個點n-1條邊的連通圖。算法

  而最小生成樹就是最小權重生成樹的簡稱,即全部邊的權值之和最小的生成樹。網絡

  最小生成樹問題通常有如下兩種求解方式。優化

1、Prim算法spa

  參考了Feynman的博客 .net

  Prim算法一般以鄰接矩陣做爲儲存結構。code

  算法思路:以頂點爲主導地位,從起始頂點出發,經過選擇當前可用的最小權值邊把頂點加入到生成樹當中來:blog

  1.從連通網絡N={V,E}中的某一頂點U0出發,選擇與它關聯的具備最小權值的邊(U0,V),將其頂點加入到生成樹的頂點集合U中。排序

  2.之後每一步從一個頂點在U中,而另外一個頂點不在U中的各條邊中選擇權值最小的邊(U,V),把它的頂點加入到集合U中。如此繼續下去,直到網絡中的全部頂點都加入到生成樹頂點集合U中爲止。 隊列

  模板題連接:Prim算法求最小生成樹

  樸素版時間複雜度O(n²)算法模板:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 500+10;
int n,m;
int g[N][N],dis[N],vis[N];

void prim()
{
    memset(dis,0x1f,sizeof dis);
    dis[1]=0;
    for(int j=1;j<=n;j++)
    {
        int min_len=2e+9,k;
        for(int i=1;i<=n;i++)
        {
            if(!vis[i]&&dis[i]<min_len)
            {
                min_len=dis[i];
                k=i;
            }
        }
        vis[k]=1;
        for(int i=1;i<=n;i++)
        {
            if(!vis[i]&&dis[i]>g[k][i])
                dis[i]=g[k][i];
        }
    }

}

int main()
{
    scanf("%d%d",&n,&m);
    memset(g,0x1f,sizeof g);
    for(int i=1;i<=m;i++)
    {
        int u,v,w;scanf("%d%d%d",&u,&v,&w);
        g[u][v]=g[v][u]=min(g[u][v],w);  //由於有重邊,因此取min
    }
    prim();
    int ans=0;
    for(int i=1;i<=n;i++)ans+=dis[i];
    if(ans>1e7)printf("impossible\n");
    else printf("%d\n",ans);
    return 0;
}

  與Dijkstra相似,Prim算法也能夠用堆優化,優先隊列代替堆,優化的Prim算法時間複雜度O(mlogn)模板(圖的存儲方式爲前向星):

void Prim_heap(int point)
{
    memset(dis,0x1f,sizeof(dis));
    priority_queue<pair<int,int> > q;
    
    dis[point]=0;
    q.push(make_pair(0,1));
    while(!q.empty())
    {
        int k=q.top().second;
        q.pop();
        v[k]=1;
        for(int i=h[k];i!=-1;i=edge[i].next)
        {
            int to=edge[i].to,w=edge[i].w;
            if(!v[to]&&dis[to]>w)
            {
                dis[to]=w;
                q.push(make_pair(-dis[to],to));  //優先隊列大根堆變小根堆小騷操做:只需一個‘-’號; 
            }
        }
    }
    for(int i=1;i<=n;i++)if(dis[i]==0x1f1f1f1f)flag=false;  //判斷是否不存在最小生成樹 
    return ;
}

 

2、Kruskal算法

  相比於Prim算法,更經常使用的仍是Kruskal,其緣由在於Kruskal算法模板的代碼量小並且思路易理解。

  算法思路:先構造一個只含 n 個頂點、而邊集爲空的子圖,把子圖中各個頂點當作各棵樹上的根結點,以後,從網的邊集 E 中選取一條權值最小的邊,若該條邊的兩個頂點分屬不一樣的樹,則將其加入子圖,即把兩棵樹合成一棵樹,反之,若該條邊的兩個頂點已落在同一棵樹上,則不可取,而應該取下一條權值最小的邊再試之。依次類推,直到森林中只有一棵樹,也即子圖中含有 n-1 條邊爲止。

  步驟:

  1. 新建圖G,G中擁有原圖中相同的節點,但沒有邊;
  2. 將原圖中全部的邊按權值從小到大排序;
  3. 從權值最小的邊開始,若是這條邊鏈接的兩個節點於圖G中不在同一個連通份量中,則添加這條邊到圖G中;
  4. 重複3,直至圖G中全部的節點都在同一個連通份量中。

  簡單來講就是以邊爲主導地位,每次選擇權值最小的邊,判斷該邊鏈接的兩點是否連通,若不連通,則合併兩點(合併操做以並查集實現)。記錄合併的次數,當次數等於n-1時結束。

  模板題連接:Kruskal算法求最小生成樹

  代碼以下:時間複雜度O(mlogm)

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 100000+10, M = 200000+10; 

struct Edge{
    int u,v,w;
    bool operator < (const Edge &E)const
    {
        return w<E.w;
    }
}edge[M];
int fa[N];
int n,m,cnt,ans;

int find(int x)
{
    if(fa[x]==x)return x;
    else return fa[x]=find(fa[x]);
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)fa[i]=i;
    for(int i=1;i<=m;i++)
    {
        int a,b,c;scanf("%d%d%d",&a,&b,&c);
        edge[i].u=a;edge[i].v=b;edge[i].w=c;
    }
    sort(edge+1,edge+m+1);
    for(int i=1;i<=m;i++)
    {
        int u=find(edge[i].u),v=find(edge[i].v),w=edge[i].w;
        if(u!=v)
        {
            cnt++;
            fa[u]=v;
            ans+=w;
        }
    }
    if(cnt==n-1)printf("%d\n",ans);
    else printf("impossible\n");
    return 0;
}

 

3、Prim,Prim_heap,Kruskal算法時間複雜度比較

  參考了G機器貓的博客

結論:

  1.Prim在稠密圖中比Kruskal優,在稀疏圖中比Kruskal劣。

  2.Prim_heap在任什麼時候候都有使人滿意的的時間複雜度,可是代價是空間消耗極大。(以及代碼很複雜>_<)

  但值得說一下的是,時間複雜度並不能反映出一個算法的實際優劣。

  競賽題通常給的都是稀疏圖,選擇Prim_heap便可;若是以爲代碼量太大,想要在Prim與Kruskal算法中選一個,那就選擇Kruskal算法。

相關文章
相關標籤/搜索