圖論--最小生成樹總結(Prim&&Kruskal)

今天才寫了prim的堆優化,發現kruskal竟然比prim跑得快。。。css


迴歸正題:
如下是我我的對最小生成樹各類算法的理解,以及個人代碼。
如下我將點數稱爲n,邊數稱爲m;c++


Prim
算法過程(來自百度百科):
1. 輸入:一個加權連通圖,其中頂點集合爲V,邊集合爲E;
2. 初始化:Vnew = {x},其中x爲集合V中的任一節點(起始點),Enew = {},爲空;
3. 重複下列操做,直到Vnew = V:
- a.在集合E中選取權值最小的邊< u, v >,其中u爲集合Vnew中的元素,而v不在Vnew集合當中,而且v∈V(若是存在有多條知足前述條件即具備相同權值的邊,則可任意選取其中之一);
- b.將v加入集合Vnew中,將< u, v >邊加入集合Enew中;
4. 輸出:使用集合Vnew和Enew來描述所獲得的最小生成樹。
而後如下是我本身的解釋:
初始時任選一個起點,並加入點集;
不斷將離訪問過的點集最近的未訪問的點連邊(或者說選最短的邊),加入點集中,並將連邊加入邊集(或累加答案);
這樣進行n-1次,每次加入一個點和一條邊,結束後就會造成一棵n個節點,n-1條邊的樹;
正確性我也懶得證了,本身百度看吧。
時間複雜度:O(nm)
代碼就不用看了,這個複雜度徹底沒法接受,必須優化;算法


Prim–堆優化
對於上面的過程,最有優化餘地的一步是什麼呢?編程

不斷將離訪問過的點集最近的未訪問的點連邊(或者說選最短的邊markdown

很明顯,粗體字部分能夠優化(通常來講,將求最小值或最大值從n優化至logn級別都是常見的優化);
具體方法就是將有可能擴展的邊都放入一個小根堆中,每次取最小的,這樣能夠將每次取最小值的過程優化至log級別的;(不知道堆的同窗能夠自行百度)
時間複雜度:低於O(nlogm)
手寫堆又會增長編程複雜度,咱們可使用c++自帶的優先隊列(priority_queue),這就是一個堆;less

priority_queue< T > (T爲類型)(後面括號內爲時間複雜度)
1. T top(void):返回堆頂(即最值);(1)
2. void pop(void):彈出堆頂;(logn)
3. void push(T):壓入新元素;(logn)
4. bool empty(void):若是堆爲空返回true,不然返回false;(1)
5. int size(void):返回堆的元素數量;(1)
另外一種定義方式:(建議)
priority_queue< T , vector< T > ,less< T > > (大根堆)
priority_queue< T , vector< T > ,greater< T > > (小根堆)
用自定義類型的前提是那個類型有定義小於號(大根堆)/ 大於號(小根堆)
關於比較符號定義能夠看個人代碼;優化


個人代碼:ui

//此題來自洛谷P3366 【模板】最小生成樹,是一道模板題,你們能夠去作一下。
//我這裏存邊用的是鄰接表,省空間,也方便;
//也能夠用vector來存邊
#include<bits/stdc++.h>
using namespace std;
struct edge{ //自定義邊類
    int to,next,w;
    bool operator > (const edge& y) const { //大於號
        return w>y.w;
    }
}e[400001];
int n,m,tot;
int head[5001]; //鄰接表用
int vis[5001]; //標記點是否訪問過
priority_queue<edge,vector<edge>,greater<edge> > q; //小根堆
void addedge(int x,int y,int l){ //加邊
    tot++;
    e[tot].to=y;
    e[tot].next=head[x];
    e[tot].w=l;
    head[x]=tot;
}
int prim(){ //我這裏將1做爲起點
    for(int i=head[1];i;i=e[i].next){ //將1的鄰邊都放入優先隊列
        q.push(e[i]);
    }
    vis[1]=1;
    int ans=0;
    int left=n-1; //剩餘須要加邊數
    while(left&&!q.empty()){
        edge t=q.top();
        q.pop();
        if(vis[t.to]){
            continue;
        }
        ans+=t.w;
        left--;
        int u=t.to;
        vis[u]=1;
        for(int i=head[u];i;i=e[i].next){
            int v=e[i].to;
            if(!vis[v]){
                q.push(e[i]);
            }
        }
    }
    return ans;
}
int main(){
    scanf("%d %d",&n,&m);
    for(int i=1;i<=m;i++){
        int x,y,l;
        scanf("%d %d %d",&x,&y,&l);
        addedge(x,y,l);
        addedge(y,x,l);
    }
    int ans=prim();
    printf("%d",ans);
    return 0;
}

Kruskal
算法過程(來自百度百科):
先構造一個只含 n 個頂點、而邊集爲空的子圖,把子圖中各個頂點當作各棵樹上的根結點,以後,從網的邊集 E 中選取一條權值最小的邊,若該條邊的兩個頂點分屬不一樣的樹,則將其加入子圖,即把兩棵樹合成一棵樹,反之,若該條邊的兩個頂點已落在同一棵樹上,則不可取(會出現環),而應該取下一條權值最小的邊再試之。依次類推,直到森林中只有一棵樹,也即子圖中含有 n-1 條邊爲止。
個人描述:
將邊排序後,從小到大隻要不造成環就加邊,直到加夠n-1條邊爲止。
正確性很顯然,能夠本身去看一下具體證實。
時間複雜度(暴力判環):低於O(m^2)
很明顯,判環這裏能夠用並查集優化到近似O(1),那麼算法的時間就都會集中在對邊進行排序上,時間也就是排序的時間;
時間複雜度(並查集判環):O(mlogm)
這樣的複雜度在稀疏圖優於Prim,稠密圖劣於Prim,本身酌情使用。
Kruskal的好處在於它極易編寫,而Prim編寫難度較高。spa


下來看代碼吧:.net

//這題也是洛谷P3366,你們能夠再寫一次Kruskal交一次。
#include<bits/stdc++.h>
#define inf 2000000000
using namespace std;
struct edge{
    int a,b,w;
}e[200001];
int n,m;
int f[5001];
int sum=0;
int find(int x){ //並查集
    return x==f[x]?x:f[x]=find(f[x]);
}
void add(int i,int x,int y,int l){
    e[i].w=l;
    e[i].a=x;
    e[i].b=y;
}
int cmp(edge x,edge y){
    return x.w<y.w;
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        f[i]=i;
    }
    for(int i=1;i<=m;i++){
        int x,y,l;
        scanf("%d %d %d",&x,&y,&l);
        add(i,x,y,l);
    }
    sort(e+1,e+m+1,cmp);
    int tot=0;
    //Kruskal的主體只有這一個循環
    for(int i=1;i<=m&&tot<=n-1;i++){
        if(find(e[i].a)!=find(e[i].b)){ //判環
            sum+=e[i].w;
            f[find(e[i].a)]=find(e[i].b);
            tot++;
        }
    }
    if(tot==n-1){
        cout<<sum;
    }
    else{
        cout<<"orz";
    }
    return 0;
}

交了以後不出意外應該是Kruskal略快於Prim,這是由於這題m較小。 不過,Kruskal通常狀況都足夠了,具體看數據範圍。 以上即是我對最小生成樹的總結。 end

相關文章
相關標籤/搜索