最小生成樹——Prim算法和Kruskal算法

洛谷P3366 最小生成樹板子題html

這篇博客介紹兩個算法:Prim算法和Kruskal算法,兩個算法各有優劣ios

通常來講當圖比較稀疏的時候,Kruskal算法比較快算法

而當圖很密集,Prim算法就大顯身手了數組

下面是這兩種算法的介紹函數

 


 

Prim算法spa

百度百科定義:傳送門code

好吧,其實當我第一眼看到這個東西的時候感受和Dijkstra好像,可是學了以後發現其實區別仍是很明顯(而且好記)的htm

Dijkstra是維護從到源點的最短長度,而Prim則是維護到最小生成樹的最短長度(其實就是到最小生成樹上全部點的最短長度)blog

那麼Prim究竟是什麼呢?排序

Prim的思想是將任意節點做爲根,再找出與之相鄰的全部邊(用一遍循環便可),再將新節點更新並以此節點做爲根繼續搜,維護一個數組:dis,做用爲已用點到未用點的最短距離。

Prim算法之因此是正確的,主要基於一個判斷:對於任意一個頂點v,鏈接到該頂點的全部邊中的一條最短邊(v, vj)必然屬於最小生成樹(即任意一個屬於最小生成樹的連通子圖,從外部鏈接到該連通子圖的全部邊中的一條最短邊必然屬於最小生成樹)

舉個栗子:

STEP 1

此爲原始的加權連通圖。每條邊一側的數字表明其權值。

STEP 2

頂點D被任意選爲起始點。頂點A、B、E和F經過單條邊與D相連。A是距離D最近的頂點,所以將A及對應邊AD以高亮表示。

STEP 3

下一個頂點爲距離D或A最近的頂點。B距D爲9,距A爲7,E爲15,F爲6。所以,F距D或A最近,所以將頂點F與相應邊DF以高亮表示。

STEP 4

算法繼續重複上面的步驟。距離A爲7的頂點B被高亮表示。

STEP 5

在當前狀況下,能夠在C、E與G間進行選擇。C距B爲8,E距B爲7,G距F爲11。點E最近,所以將頂點E與相應邊BE高亮表示。

STEP 6

這裏,可供選擇的頂點只有C和G。C距E爲5,G距E爲9,故選取C,並與邊EC一同高亮表示。

STEP 7

頂點G是惟一剩下的頂點,它距F爲11,距E爲9,E最近,故高亮表示G及相應邊EG。

STEP 8

如今,全部頂點均已被選取,圖中綠色部分即爲連通圖的最小生成樹。在此例中,最小生成樹的權值之和爲39。

 

複雜度:

這裏記頂點數v,邊數e
鄰接矩陣:O(v) 鄰接表:O(elog2v)

 

下面是代碼及註釋:

#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<iomanip>
#include<cmath>
#include<cstring>
#include<string>
#include<algorithm>
#include<time.h>
#include<queue>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pr;
const double pi=acos(-1);
#define rep(i,a,n) for(int i=a;i<=n;i++)
#define per(i,n,a) for(int i=n;i>=a;i--)
#define Rep(i,u) for(int i=head[u];i;i=Next[i])
#define clr(a) memset(a,0,sizeof a)
#define pb push_back
#define mp make_pair
#define fi first
#define sc second
ld eps=1e-9;
ll pp=1000000007;
ll inf=2147483647;
#define maxn 5005
#define maxm 200005
ll mo(ll a,ll pp){if(a>=0 && a<pp)return a;a%=pp;if(a<0)a+=pp;return a;}
ll powmod(ll a,ll b,ll pp){ll ans=1;for(;b;b>>=1,a=mo(a*a,pp))if(b&1)ans=mo(ans*a,pp);return ans;}
ll read(){
    ll ans=0;
    char last=' ',ch=getchar();
    while(ch<'0' || ch>'9')last=ch,ch=getchar();
    while(ch>='0' && ch<='9')ans=ans*10+ch-'0',ch=getchar();
    if(last=='-')ans=-ans;
    return ans;
}
//head 


struct edge
{
    int to,_dis,next;////出邊的終點、出邊的長度、下一條出邊
}edge[maxm<<1];//由於是無向圖,因此開雙倍數組,雙倍快樂 

int head[maxn],dis[maxn],cnt,n,m,tot,now=1,ans;
//dis數組表示當前點到最小生成樹的最短路徑 
bool vis[maxn];

inline void add_edge(int from,int to,int value)
{
    edge[++cnt].to=to;
    edge[cnt]._dis=value;
    edge[cnt].next=head[from];
    head[from]=cnt;
}//添加邊 

inline int prim()
{
    rep(i,2,n)
        dis[i]=inf;//初始化 
    for(int i=head[1];i;i=edge[i].next)//遍歷當前節點的每一條出邊 
        dis[edge[i].to]=min(dis[edge[i].to],edge[i]._dis);//重邊の處理 
    while(++tot<n)//就是tot<=n-1,由於最小生成樹的邊數必定等於節點數-1 
    {
        int minn=inf;//初始化min 
        vis[now]=1;//已經到達 
        rep(i,1,n)
            if(!vis[i]&&minn>dis[i])//尋找到最小生成樹距離最短的節點 
                minn=dis[i],now=i;//更新 
        ans+=minn;//更新最小生成樹 
        for(int i=head[now];i;i=edge[i].next)//遍歷每一條出邊 
        {
            int to=edge[i].to;
            if(dis[to]>edge[i]._dis&&!vis[to])
                dis[to]=edge[i]._dis;//更新dis數組 
        }
        
    }
    return ans;
}
int main()
{
    n=read(),m=read();
    rep(i,1,m)
    {
        int from=read(),to=read(),value=read();
        add_edge(from,to,value);//由於是無向圖 
        add_edge(to,from,value);//雙倍存儲,雙倍快樂 
    }
    cout<<prim();
}

 


 

Kruskal算法

 

Kruskal算法的思想比Prim好理解一些。先把邊按照權值進行排序,用貪心的思想優先選取權值較小的邊,並依次鏈接,若出現環則跳過此邊(用並查集來判斷是否存在環)繼續搜,直到已經使用的邊的數量比總點數少一便可。

 

證實:剛剛有提到:若是某個連通圖屬於最小生成樹,那麼全部從外部鏈接到該連通圖的邊中的一條最短的邊必然屬於最小生成樹。因此不難發現,當最小生成樹被拆分紅彼此獨立的若干個連通份量的時候,全部可以鏈接任意兩個連通份量的邊中的一條最短邊必然屬於最小生成樹

 

上面提到:這個東西要用到「並查集」

不瞭解的:傳送門

代碼及註釋:

#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<iomanip>
#include<cmath>
#include<cstring>
#include<string>
#include<algorithm>
#include<time.h>
#include<queue>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pr;
const double pi=acos(-1);
#define rep(i,a,n) for(int i=a;i<=n;i++)
#define per(i,n,a) for(int i=n;i>=a;i--)
#define Rep(i,u) for(int i=head[u];i;i=Next[i])
#define clr(a) memset(a,0,sizeof a)
#define pb push_back
#define mp make_pair
#define fi first
#define sc second
ld eps=1e-9;
ll pp=1000000007;
ll inf=2147483647;
#define maxn 5005
#define maxm 200005
ll mo(ll a,ll pp){if(a>=0 && a<pp)return a;a%=pp;if(a<0)a+=pp;return a;}
ll powmod(ll a,ll b,ll pp){ll ans=1;for(;b;b>>=1,a=mo(a*a,pp))if(b&1)ans=mo(ans*a,pp);return ans;}
ll read(){
    ll ans=0;
    char last=' ',ch=getchar();
    while(ch<'0' || ch>'9')last=ch,ch=getchar();
    while(ch>='0' && ch<='9')ans=ans*10+ch-'0',ch=getchar();
    if(last=='-')ans=-ans;
    return ans;
}
//head 

struct Edge
{
    int from,to,_dis;
}edge[maxm];
int fa[maxn],n,m,ans,eu,ev,cnt;
//father數組用來存儲父親節點 
bool cmp(Edge a,Edge b)
{
    return a._dis<b._dis;//比較函數 
}
inline int find_die(int x)
{
    while(x!=fa[x]) x=fa[x]=fa[fa[x]];
    return x;//找爹 
} 
inline int kruskal()
{
    sort(edge+1,edge+1+m,cmp);//先將全部的邊按權值排序 
    rep(i,1,m)
    {
        eu=find_die(edge[i].from);
        ev=find_die(edge[i].to);//分別找始點和終點的祖宗節點 
        if(eu==ev)//若是是一個祖宗,就說明他們在一個聯通圖中 
            continue;
        ans+=edge[i]._dis;//更新最小生成樹長度 
        fa[ev]=eu;//順便標記父親 
        if(++cnt==n-1)//知道生成最小生成樹 
            break;
    }
    return ans;
}

int main()
{
    n=read(),m=read();
    rep(i,1,n)
        fa[i]=i;//初始化本身是本身的父親 
    rep(i,1,m)
        edge[i].from=read(),edge[i].to=read(),edge[i]._dis=read();
    cout<<kruskal();
}

 加上判斷無解的狀況:

#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<iomanip>
#include<cmath>
#include<cstring>
#include<string>
#include<algorithm>
#include<time.h>
#include<queue>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pr;
const double pi=acos(-1);
#define rep(i,a,n) for(int i=a;i<=n;i++)
#define per(i,n,a) for(int i=n;i>=a;i--)
#define Rep(i,u) for(int i=head[u];i;i=Next[i])
#define clr(a) memset(a,0,sizeof a)
#define pb push_back
#define mp make_pair
#define fi first
#define sc second
ld eps=1e-9;
ll pp=1000000007;
ll inf=2147483647;
#define maxn 500001
#define maxm 1000001
ll mo(ll a,ll pp){if(a>=0 && a<pp)return a;a%=pp;if(a<0)a+=pp;return a;}
ll powmod(ll a,ll b,ll pp){ll ans=1;for(;b;b>>=1,a=mo(a*a,pp))if(b&1)ans=mo(ans*a,pp);return ans;}
ll read(){
    ll ans=0;
    char last=' ',ch=getchar();
    while(ch<'0' || ch>'9')last=ch,ch=getchar();
    while(ch>='0' && ch<='9')ans=ans*10+ch-'0',ch=getchar();
    if(last=='-')ans=-ans;
    return ans;
}
//head 

struct Edge
{
    int from,to,_dis;
}edge[maxm];
int fa[maxn],n,m,ans,eu,ev,cnt;
//father數組用來存儲父親節點 
bool cmp(Edge a,Edge b)
{
    return a._dis<b._dis;//比較函數 
}
inline int find_die(int x)
{
    while(x!=fa[x]) x=fa[x]=fa[fa[x]];
    return x;//找爹 
}
int main()
{
    n=read(),m=read();
    rep(i,1,n)
        fa[i]=i;//初始化本身是本身的父親 
    rep(i,1,m)
        edge[i].from=read(),edge[i].to=read(),edge[i]._dis=read();
    sort(edge+1,edge+1+m,cmp);//先將全部的邊按權值排序 
    rep(i,1,m&&cnt<=n-1)
    {
        eu=find_die(edge[i].from);
        ev=find_die(edge[i].to);//分別找始點和終點的祖宗節點 
        if(eu==ev)//若是是一個祖宗,就說明他們在一個聯通圖中 
            continue;
        ans+=edge[i]._dis;//更新最小生成樹長度 
        fa[ev]=eu;//順便標記父親 
        cnt++;
    }
    if(cnt!=n-1)
        cout<<"orz";
    else
        cout<<ans;
}

 

 碼字不易,若是看的滿意請點個推薦哦~

相關文章
相關標籤/搜索