最小生成树(简析Prim算法和Kruskal算法)

1. 明确概念:

子图:从原图中选中一些顶点和边组成的图,称为原图的子图。

生成子图:选中一些边和所有顶点组成的图,称为原图的生成子图。

生成树:如果生成子图恰好是一棵树,则称为生成树。

最小生成树:权值之和最小的生成树,则称为最小生成树。

2. Prim算法

图示+伪代码(摘自oi-wiki)

证明

从任意一个结点开始,将结点分成两类:已加入的,未加入的。

每次从未加入的结点中,找一个与已加入的结点之间边权最小值最小的结点。

然后将这个结点加入,并连上那条边权最小的边。

重复n-1次即可。

证明:还是说明在每一步,都存在一棵最小生成树包含已选边集。

基础:只有一个结点的时候,显然成立。

归纳:如果某一步成立,当前边集为F,属于T这棵 MST,接下来要加入边e。

如果e属于T,那么成立。

否则考虑T+e中环上另一条可以加入当前边集的边f。

首先,f的权值一定不小于e的权值,否则就会选择f而不是e了。

然后,f的权值一定不大于e的权值,否则T+e-f就是一棵更小的生成树了。

因此,e和f的权值相等,T+e-f也是一棵最小生成树,且包含了F。

存图

链式前向星存图,建双边。

int head[MAXN], cnt;
struct node {
    int v, w, next;
}e[MAXM];

void add(int u, int v, int w) {
    e[cnt].v = v;
    e[cnt].w = w;
    e[cnt].next = head[u];
    head[u] = cnt++;
}

void adde(int u, int v, int w) {
    add(u, v, w);
    add(v, u, w);
}

void init() {
    memset(head, -1, sizeof(head));
    cnt = 0;
}

Prim

变量定义

lowcost[i]:从i点到生成树的最短距离

vis[i]:i点已经访问过,即已经加入U战队

res:用来存最小生成树的边权之和

主要思路

首先初始化lowcost[2~n]的值为INF,将起点加入U战队,标为已访问。

接着遍历起点的邻接点,更新lowcost的值为边权。

再进入循环,循环n-1次,因为最小生成树是n-1条边。

找最小--> 加入U战队 --> 更新邻接点lowcost[]值。

bool vis[MAXN];
int lowcost[MAXN];
int res = 0;
int n, m; 

int prim(int u) {
    for (int i = 2; i <= n; i++) {
        lowcost[i] = INF;
    }
    vis[u] = 1;
    for (int i = head[u]; ~i; i = e[i].next) {
        int v = e[i].v;
        lowcost[v] = min(lowcost[v], e[i].w);
    }
    for (int i = 1; i < n; i++) {
        int mn = INF;
        int v = -1;
        for (int j = 1; j <= n; j++) {
            if (!vis[j] && lowcost[j] < mn) {
                mn = lowcost[j];
                v = j;
            }
        }
        if (v == -1) return 0;
        vis[v] = 1;
        res += mn;
        for (int j = head[v]; ~j; j = e[j].next) {
            int x = e[j].v;
            if (!vis[j] && lowcost[x] > e[j].w) {
                lowcost[x] = e[j].w;
            }
        }
    }
    return res;
}

main函数

按题意编写。

int main() {
    init();
    scanf("%d%d", &n, &m);
    int u, v, w;
    for (int i = 1; i <= m; i++) {
        scanf("%d%d%d", &u, &v, &w);
        adde(u, v, w);
    }
    int ans = prim(1);
    if (ans == 0) {
        printf("orz\n");
    }
    else {
        printf("%d\n", ans);
    }

    return 0;
}

时间复杂度

Prim算法的时间复杂度为 O(n²) ,其中n为顶点数。

3. Kruskal算法

图示+伪代码(摘自oi-wiki)

证明

思路很简单,为了造出一棵最小生成树,我们从最小边权的边开始,按边权从小到大依次加入,如果某次加边产生了环,就扔掉这条边,直到加入了 n-1 条边,即形成了一棵树。

证明:使用归纳法,证明任何时候 K 算法选择的边集都被某棵 MST 所包含。

基础:对于算法刚开始时,显然成立(最小生成树存在)。

归纳:假设某时刻成立,当前边集为 F,令 T 为这棵 MST,考虑下一条加入的边 e。

如果 e 属于 T,那么成立。

否则,T+e 一定存在一个环,考虑这个环上不属于 F 的另一条边 f(一定只有一条)。

首先,f 的权值一定不会比 e 小,不然 f 会在 e 之前被选取。

然后,f 的权值一定不会比 e 大,不然 T+e-f 就是一棵比 T 还优的生成树了。

所以,T+e-f 包含了 F,并且也是一棵最小生成树,归纳成立。

前置知识——并查集

这里贴一份并查集模板代码(洛谷P3367【模板】并查集)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e4 + 5;
int fa[MAXN];
int n, m;

void init() {
    for (int i = 0; i < MAXN; i++) {
        fa[i] = i;
    }
}

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

void merge(int x, int y) {
    x = find(x);
    y = find(y);
    fa[x] = y; 
}

bool ask(int x, int y) {
    return find(x) == find(y);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin >> n >> m;
    
    init();
    
    int x, y, z;
    for (int i = 1; i <= m; i++) {
        cin >> z >> x >> y;
        if (z == 1) {
            merge(x, y);
        }
        else {
            if (ask(x, y)) {
                cout << "Y" << endl;
            }
            else {
                cout << "N" << endl;
            }
        }
    }   

    return 0;
}

存图方式

这里我们定义一个边集数组(结构体),然后直接输入。

struct node {
    int u, v, w;
}e[MAXM];

// 输入
for (int i = 1; i <= m; i++) {
    scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
}

并查集处理

① 初始化

void init() {
    for (int i = 1; i <= n; i++) {
        fa[i] = i;
    }
}

② 合并

int merge(int a, int b) {
    int x = fa[a];
    int y = fa[b];
    if (x == y) return 0;
    else {
        for (int i = 1; i <= n; i++) {
            if (fa[i] == y) {
                fa[i] = x;
            }
        }
    }
    return 1;
}

当然,这种合并方式略显特殊,我们还可以采取我们熟悉的常规方式:

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

int merge(int a, int b) {
    int x = find(a);
    int y = find(b);
    if (x == y) return 0;
    fa[y] = x;
    return 1;
}

Kruskal

变量定义

ans:最小生成树边权之和

cnt:最小生成树已包含的边数

主要思路

首先给边集数组进行排序,按照边权值从小到大排

遍历已排好的m条边,如果能合并,cnt++,ans+=e[i].w

bool cmp(node a, node b) {
    return a.w < b.w;
}

int Kruskal() {
    int ans = 0;
    int cnt = 0;
    sort(e + 1, e + m + 1, cmp);
    for (int i = 1; i <= m; i++) {
        if (merge(e[i].u, e[i].v)) {
            ans += e[i].w;
            cnt++;
            if (cnt == n - 1) return ans;
        }
    }
    return 0;
}

main函数

按题意编写即可。

int main() {
    while (~scanf("%d", &n) && n) {
        init();
        scanf("%d", &m);
        for (int i = 1; i <= m; i++) {
            scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
        }
        printf("%d", Kruskal());
    }

    return 0;
}

时间复杂度

Kruskal算法的时间复杂度为 O(ElogE) ,其中E为边数。

感谢各位神犇的阅读!祝生活愉快~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值