圖論篇2——最小生成樹算法(kurskal算法&prim算法)

基本概念

(Tree)

若是一個無向連通圖中不存在迴路,則這種圖稱爲樹。html

生成樹 (Spanning Tree)

無向連通圖G的一個子圖若是是一顆包含G的全部頂點的樹,則該子圖稱爲G的生成樹。ios

生成樹是連通圖的極小連通子圖。這裏所謂極小是指:若在樹中任意增長一條邊,則將出現一條迴路;若去掉一條邊,將會使之變成非連通圖。算法

最小生成樹

一個帶權值連通圖用$n-1$條邊把$n$個頂點鏈接起來,且鏈接起來的權值最小數組

應用場景

設想有9個村莊,這些村莊構成以下圖所示的地理位置,每一個村莊的直線距離都不同。若要在每一個村莊間架設網絡線纜,若要保證成本最小,則須要選擇一條可以聯通9個村莊,且長度最小的路線。網絡

 

Kruskal算法

知識點:數據結構——並查集數據結構

基本思想

始終選擇當前可用、不會(和已經選取的邊)構成迴路的最小權植邊。post

具體步驟:測試

1. 將全部邊按權值進行降序排序優化

2. 依次選擇權值最小的邊spa

3. 若該邊的兩個頂點落在不一樣的連通份量上,選擇這條邊,並把這兩個頂點標記爲同一連通份量;若這條邊的兩個頂點落到同一連通份量上,捨棄這條邊。反覆執行2,3,直到全部的都在同一連通份量上。【這一步須要用到上面的並查集】

模板題:https://www.luogu.org/problem/P3366

#include <iostream>
#include <algorithm>
using namespace std;
int pre[5005];
int n, m; //n個定點,m條邊

struct ENode {
    int from, to, dis;
    bool operator<(ENode p) {
        return dis < p.dis;
    }
}M[200005];

int Find(int x) {
    return x == pre[x] ? pre[x] : pre[x] = Find(pre[x]);
}

int kurskal() {
    sort(M, M + m);
    int N = n, res = 0;
    for (int i = 0; i < m && N > 1; i++) {
        int fx = Find(M[i].from), fy = Find(M[i].to);
        if (fx != fy) {
            pre[fx] = fy;
            N--;//找到了一條邊,當N減到1的時候代表已經找到N-1條邊了,就完成了
            res += M[i].dis;
        }
    }
    if (N == 1)//循環作完,N不等於1 代表沒有找到合適的N-1條邊來構成最小生成樹
        return res;
    return -1;
}

int main() {
    cin >> n >> m;
    for (int i = 0; i <= n; i++) {
        pre[i] = i;
    }
    for (int i = 0; i < m; i++) {
        scanf("%d%d%d", &M[i].from, &M[i].to, &M[i].dis);;
    }
    int ans = kurskal();
    if (ans != -1)
        cout << ans << endl;
    else
        cout << "orz" << endl;
    return 0;
}

Prim算法

Prim算法思想

首先將圖的點分爲兩部分,一種是訪問過的$u$(第一條邊任選),一種是沒有訪問過的$v$

1: 每次找$u$到$v$的權值最小的邊。

2: 而後將這條邊中的$v$中的頂點添加到$u$中,直到$v$中邊的個數$=$頂點數$-1$

圖解步驟:

維護一個$dis$數組,記錄只使用已訪問節點可以到達各未訪問節點最短的權值。

初始值爲節點1(任意一個均可以)到各點的值,規定到本身是0,到不了的是$inf$(定義一個特別大的數)。

找當前能到達的權值最短的點。1-->4,節點4

將dis[4]賦值爲0,標記爲已訪問過,同時藉助4節點更新dis數組。

 後面依次

 

最後整個dis數組都是0了,最小生成樹也就出來了,若是$dis$數組中還有 $inf$ 的話,說明這不是一個連通圖。

 仍是上面那道模板題:https://www.luogu.org/problem/P3366

#include <iostream>

#include <fstream>
using namespace std;

struct ENode {
    int dis, to;//權重、指向
    ENode* next = NULL;
    void push(int to, int dis) {
        ENode* p = new ENode;
        p->to = to; p->dis = dis;
        p->next = next;
        next = p;
    }
}*head;
const int inf = 1 << 30;
int N, M;
int dis[5005];

int prim() {
    int res = 0;

    for (int i = 2; i <= N; i++) {
        dis[i] = inf;
    }

    for (int i = 0; i < N; i++) {//與kurskal區分,找邊是N-1條邊,找點是N個點
        int v = 1 , MIN = inf;
        for (int j = 1; j <= N; j++) {
            //到不了的,訪問過的不進行比較
            if (dis[j] != 0 && dis[j] < MIN) {
                v = j;
                MIN = dis[j];
            }
        }
        if (MIN == inf && v != 1)//這裏v!=1是爲了把dis的初始化放在循環裏面作,也能夠放在循環外面作,可是外層循環就只須要作N-1次了
            return -1;//還沒找夠n個點,沒路了
        res += dis[v];
        dis[v] = 0;
        ENode *p = head[v].next;
        while (p) {
            if (dis[p->to] > p->dis) {
                dis[p->to] = p->dis;
            }
            p = p->next;
        }
    }
    return res;
}

int main() {
#ifdef LOCAL
    fstream cin("data.in");
#endif // LOCAL

    cin >> N >> M;
    head = new ENode[N + 1];
    for (int i = 0; i < M; i++) {
        int from, to, dis;
        scanf("%d%d%d", &from, &to, &dis);
        //cin >> from >> to >> dis;
        head[from].push(to, dis);
        head[to].push(from, dis);
    }
    int ans = prim();
    if (ans != -1)
        cout << ans << endl;
    else
        cout << "orz" << endl;
    return 0;
}

二者區別

時間複雜度

prim算法

時間複雜度爲$O(n^2)$,$n$爲頂點的數量,其時間複雜度與邊得數目無關,適合稠密圖。

kruskal算法

時間複雜度爲$O(e\cdot loge)$,$e$爲邊的數目,與頂點數量無關,適合稀疏圖。

其實就是排序的時間,由於並查集的查詢、合併操做都是$O(1)$。

總結

通俗點說就是,點多邊少用Kruskal,由於Kruskal算法每次查找最短的邊。 點少邊多用Prim,由於它是每次找一個頂點。

具體選擇用那個,能夠用電腦算一下,題目給的數據級別,$n^2$和$e\cdot loge$看看那個小,好比上面的模板題,題目給的數據級別是$(n<=5000,e<=200000)$,粗略估算一下,kurskal算法必定是會快很多的,結果也確實如粗。

實現難度

明眼人都能看出來,kurskal算法要簡單太多了。kurskal算法不須要把圖表示出來,而Prim算法必須建表或者鄰接矩陣,因此從上面的數據也能看出來當邊的數目較大時,Prim算法所佔用的空間比kurskal算法多了不少。

拓展

堆優化Prim算法

用堆存儲當前全部可到達的點和距離,就是把dis數組裏的內容一式兩份,存在堆裏,而後每次取堆頂元素,每次操做爲$O(logn)$,因此使用堆優化後的Prim算法理論上時間複雜度爲$O(nlogn)$,可是好像沒有達到想要的效果

看了測試數據發現,有不少重邊,那就合理了,作了不少次的無用循環,因此時間上也和kurskal比較相近。因此在數據可靠、無重邊的狀況下,這個算法必定是上述幾種中最快的一個。

#include <iostream>
#include <fstream>
#include <cstdio>
#include <queue>

using namespace std;
struct P {
    int dis, v;
    P(int d, int v) :dis(d), v(v) {};
    bool operator<(P p)const {
        return p.dis < dis;
    }
};
struct ENode {
    int dis, to;//權重、指向
    ENode* next = NULL;
    void push(int to, int dis) {
        ENode* p = new ENode;
        p->to = to; p->dis = dis;
        p->next = next;
        next = p;
    }
}*head;
const int inf = 1 << 30;
int N, M;
int dis[5005];
bool fuck[5005];

int prim() {
    priority_queue<P>pq;
    pq.push(P(0, 1));
    int res = 0, cnt = N;
    dis[1] = 0;
    fill(dis + 1, dis + N + 1, inf);

    while (!pq.empty() && cnt > 0) {//與kurskal區分,找邊是N-1條邊,找點是N個點
        int v = pq.top().v, d = pq.top().dis;
        pq.pop();
        if (fuck[v])continue;
        fuck[v] = true;
        res += d;
        cnt--;
        ENode* p = head[v].next;
        while (p) {
            if (dis[p->to] > p->dis) {
                dis[p->to] = p->dis;
                pq.push(P(p->dis, p->to));
            }
            p = p->next;
        }
    }
    if (cnt > 1)
        return -1;
    return res;
}

int main() {
#ifdef LOCAL
    fstream cin("data.in");
#endif // LOCAL
    cin >> N >> M;
    head = new ENode[N + 1];
    for (int i = 0; i < M; i++) {
        int from, to, dis;
        scanf("%d%d%d", &from, &to, &dis);
        //cin >> from >> to >> dis;
        head[from].push(to, dis);
        head[to].push(from, dis);
    }
    int ans = prim();
    if (ans != -1)
        cout << ans << endl;
    else
        cout << "orz" << endl;
    return 0;
}
相關文章
相關標籤/搜索