图论 最小生成树

Posted keliven

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图论 最小生成树相关的知识,希望对你有一定的参考价值。

图论 最小生成树

最小生成树 (MST)

? 最小生成树是一副连通加权无向图中一棵权值最小的生成树.

? 在一个有n个点的图中, 在所有边中选择n - 1条边, 连接所有n个点, 使这n - 1条边的权值和是所有方案中最小的.

? 最小生成树在算法竞赛中很少单独考察, 常与图论及其他树上算法结合, 因此对于最小生成树的掌握是十分必要的.

算法

Kruskal (克鲁斯卡尔)

基本思路

? 将所有点视为单独集合, 并将所有边按照边权从小到大排序, 然后顺序枚举每一条边. 如果这条边连接两个不同集合, 就将此边加入最小生成树中, 并合并这两个不同集合, 直至取了n - 1边.

? 对于不同点的关系,使用并查集维护.

代码

#include<bits/stdc++.h>

using namespace std;

    struct Edge
    
        int dis, from, to;
     edge[1000001];
    int ans;
    int cnt;
    int num_edge;
    int n, m;//点数, 边数
    int bin[1000001];
    void add(int from, int to, int dis)
    
        edge[++cnt].dis = dis;
        edge[cnt].to = to;
        edge[cnt].from = from;
    

    int find(int x)
    
        if(bin[x] == x) return x;
        return bin[x] = find(bin[x]);
    //查询
    void uni(int x, int y)
    
        bin[find(x)] = find(y);
    //合并
    bool check(int x, int y)
    
        if(find(x) == find(y)) return true;
        return false;
    //判断

    bool cmp(Edge x, Edge y)
    
        return x.dis < y.dis;
    

    void Kruskal()
    
        for(int i = 1;i <= n;i++)
        bin[i] = i;
        sort(edge + 1, edge + 1 + m, cmp);
        //未建图情况下构建最小生成树
        /*for(int i = 1;i <= m;i++)
        
            if(! check(edge[i].from, edge[i].to))
            
                add(edge[i].from, edge[i].to, edge[i].dis);
                add(edge[i].to, edge[i].from, edge[i].dis);
                uni(edge[i].from, edge[i].to);
            
        */
        for(int i = 1;i <= m;i++)
        
            if(check(edge[i].to, edge[i].from))
            continue;
            ans += edge[i].dis;
            uni(edge[i].from, edge[i].to);
            if(++num_edge == n - 1)//树中边数 = 点数 - 1;
            break;
        
    
int main()

    scanf("%d %d", &n, &m);
    for(int i = 1;i <= m;i++)
    
        int x, y, z;
        scanf("%d %d %d", &x, &y, &z);
        add(x, y, z);
    
    Kruskal();
    printf("%d", ans);
    return 0;

? Kruskal使用贪心思想, 每次选取的边可以连接两不同集合且代价最小, 因此最后一定可以将n个点合并为一个集合并使总代价最小.

? 时间复杂度:O(m*logm), m为边数.

? 另外还有Prim算法也常用于求最小生成树,时间复杂度为O(n^2), 与Kruskal在实际使用时差距不大, 也可另作学习,

例题

luogu p1111 修复公路

题目背景

AAA地区在地震过后,连接所有村庄的公路都造成了损坏而无法通车。政府派人修复这些公路。

题目描述

给出A地区的村庄数n和公路数m,公路是双向的。并告诉你每条公路的连着哪两个村庄,并告诉你什么时候能修完这条公路。问最早什么时候任意两个村庄能够通车,即最早什么时候任意两条村庄都存在至少一条修复完成的道路(可以由多条公路连成一条道路)

输入格式

第1行两个正整数n, m

下面m行,每行3个正整数x, y, t,告诉你这条公路连着x, y两个村庄,在时间t时能修复完成这条公路。

输出格式

如果全部公路修复完毕仍然存在两个村庄无法通车,则输出?1,否则输出最早什么时候任意两个村庄能够通车。

思路

? 接近裸题, 直接根据题意求最小生成树, 唯一不同是因为题中需判断是否可以全连通, 所以在算法执行过程中直接统计全部边数, 最后根据边数进行判断.

代码

#include<bits/stdc++.h>

using namespace std;
    int n, m;
    int cnt;
    int num_edge;
    int bin[1000001];
    int ans;
    struct Edge
    
        int from, to, dis;
     edge[1000001];

    void add(int from, int to, int dis)
    
        edge[++cnt].from = from;
        edge[cnt].to = to;
        edge[cnt].dis = dis;
    

    int find(int x)
    
        if(bin[x] == x) return x;
        return bin[x] = find(bin[x]);
    
    void uni(int x, int y)
    
        bin[find(x)] = find(y);
    
    bool check(int x, int y)
    
        if(find(x) == find(y)) return true;
        return false;
    

    bool cmp(Edge a, Edge b)
    
        return a.dis < b.dis;
    

    void Kruskal()
    
        sort(edge + 1, edge + 1 + m, cmp);
        for(int i = 1;i <= n;i++)
        bin[i] = i;
        for(int i = 1;i <= m;i++)
        
            if(check(edge[i].from, edge[i].to)) continue;
            ans = max(ans, edge[i].dis);
            uni(edge[i].from, edge[i].to);
            //if(++num_edge == n - 1) break;
            num_edge++;
        
    
int main()

    scanf("%d %d", &n, &m);
    for(int i = 1;i <= m;i++)
    
        int u, v, w;
        scanf("%d %d %d", &u, &v, &w);
        add(u, v, w);
    
    Kruskal();
    if(num_edge >= n - 1)
    
        printf("%d", ans);
        return 0;
    
    printf("-1");
    return 0;

luogu p1194 买礼物

题目描述

又到了一年一度的明明生日了,明明想要买B样东西,巧的是,这B样东西价格都是A元。

但是,商店老板说最近有促销活动,也就是:

如果你买了第I样东西,再买第J样,那么就可以只花KI,J元,更巧的是, KI,J竟然等于KJ,I。

现在明明想知道,他最少要花多少钱。

输入格式

第一行两个整数,A,B

接下来B行,每行B个数,第I行第J个为KI,J。

我们保证KI,J=KJ,I, 并且KI,I=0。

特别的,如果KI,J = 0,那么表示这两样东西之间不会导致优惠。

输出格式

一个整数,为最小要花的钱数。

思路

? 直接最小生成树, 但因为两件物品先买哪一件优惠相同, 因此对于输入格式, 需判断i与j的大小关系以及w是否为0, 在进行Kruska前先将买第一件物品时由0到每一件物品建边, 代价为a.

代码

#include<bits/stdc++.h>

using namespace std;
    int a, b;
    int cnt;
    int ans;
    struct Edge
    
        int from, to;
        int dis;
     edge[1000001];
    int bin[1000001];
    int num_edge;
    void add(int from, int to, int dis)
    
        edge[++cnt].from = from;
        edge[cnt].to = to;
        edge[cnt].dis = dis;
    

    int find(int x)
    
        if(x == bin[x]) return x;
        return bin[x] = find(bin[x]);
    
    void uni(int x, int y)
    
        bin[find(x)] = find(y);
    
    bool check(int x, int y)
    
        if(find(x) == find(y)) return true;
        return false;
    

    bool cmp(Edge a, Edge b)
    
        return a.dis < b.dis;
    

    void Kruskal()
    
        for(int i = 1;i <= b;i++)
        bin[i] = i;
        sort(edge + 1, edge + 1 + cnt, cmp);
        for(int i = 1;i <= cnt;i++)
        
            if(check(edge[i].from, edge[i].to)) continue;
            ans += edge[i].dis;
            uni(edge[i].from, edge[i].to);
            if(++num_edge == b) break;
        
    
int main()

    scanf("%d %d", &a, &b);
    for(int i = 1;i <= b;i++)
    
        for(int j = 1;j <= b;j++)
        
            int w;
            scanf("%d", &w);
            if(i < j && w)
            add(i, j, w);
        
    
    for(int i = 1;i <= b;i++) add(0, i, a);
    Kruskal();
    printf("%d", ans);
    return 0;

END

以上是关于图论 最小生成树的主要内容,如果未能解决你的问题,请参考以下文章

怎么求最小生成树 (离散数学 图论)

浅谈图论——最小生成树

经典树与图论(最小生成树、哈夫曼树、最短路径问题---Dijkstra算法)

有没有图论高手,请教一个最小生成树的问题

图论讲解——最小生成树

图论算法零基础最小生成树学习与总结