zoukankan      html  css  js  c++  java
  • [模板]最小生成树 Kruskal Prim

    (这是一张边已经给出的无向图,不能自行加边,只能在当前的边中选择一些来求最小生成树.)

    最小生成树:

    定义:

    给定一张边带权的无向图G=(V,E),n = |V,m=|E|。由V中全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树。边的权值之和最小的生成树被称为无向图G的最小生成树(Minimum Spanning Tree,MST)。    ---<<指南>>

    最小生成树一般只谈两种算法:Kruskal与Prim.

    Kruskal模板O(|E|log|E|):

    详细来说,Kruskal算法的流程如下:

     1.建立并查集,每个点各自构成一个集合。

     2.把所有边按照权值从小到大排序,依次扫描每条边(x,y,z)。

    3.若x,y属于同一集合(连通),则忽略这条边,继续扫描下一条。

    4.否则,合并x,y所在的集合,并把z累加到答案中。

    5.所有边扫描完成后,第4步中处理过的边就构成最小生成树。

    时间复杂度为O(mlogm)。

                          --<<指南>>

    书上这样的说法已经足够清晰了,不作证明,这里就放一些我自己写的实现供参考.

    P3366 (本题)
    P11914 (这个可读性更强一些)

    可以看出来模板性很强,不过题意可以变化多端,处理起来也有些变式,例如:

    变式:

    P2872 [USACO07DEC]Building Roads S

    已经给定了必须选择的一些边,需要再自行添加一些边来求最小生成树.换句话说,在已经确定了一些边的完全图中求最小生成树.

    而自行添加的边的选取范围即任意两点间构成的边的集合,即共有N2/2条边.

    对于必须选择的边,应当先合并两端点的集合,之后穷举所有边并升序排列等执行Kruskal的流程.

    P2872

    P1396 营救

    求最大值的最小情况,很容易想到用二分答案,这是一种可行的做法.

    联想到Kruskal具有一些性质:

    1.总是按权的升序处理边,

    2.处理过程中的任意时刻,都是在用最小的代价来"生成"最大的(无更大的)"连通".

    这意味着处理完某条边后,s与t将从不连通变为连通且保持此状态,并且此时的代价即为最小,因为任意时刻图上的边都构成当前的(若干个)最小生成树.

    可以想到,进行Kruskal流程,一旦发现s与t变得连通了即可跳出,当前边的权值即为答案.

    可以描述为:求"(最大边权值)最小(的)"生成树,使得s与t连通.

    P1396

    P1991 无线通讯网

    在可以使得若干边无条件免费的情况下求完全图的最小生成树.

    有x台卫星电话,意味着可以让最小生成树中权最大的x-1条边免费.

    P1991

    如果求最大生成树,只需要把升序排列改为降序排列边.

    见此题.

    P2121

    进一步理解最小生成树:

    罗列一下我自己感觉到的性质:

    (以下均指完全图的最小生成树,由于边太多就不画出原完全图的边了)

    (假设先移除完全图的所有边再加边计算生成树)

      1. Kruskal执行过程中的任意时刻,图中的每个连通块的边都是其最小生成树的边.(这是显然的)

      2.移除一张完全图的最小生成树中的任意边,产生的新连通块的边仍然是其最小生成树的边.

    感受一下这张图中的最小生成树(不具有全体代表性,属于比较简单的情况):

    权即为图中的长度.

    严格的证明,不会.但是基于Kruskal的贪心原理可以粗略地验证一下性质2的正确性:

    对于一个完整的最小生成树,由于树的性质,每当移除其中的一条边,一定会把含有这条边的连通块分割为两个连通块.

    (还可以知道,产生的两个连通块之间的点间的最短边即为这条边)

    现在对其中任意一个连通块再进行Kruskal算法求其最小生成树,会发现这个过程与最初的连通块求取过程的一部分完全相同,仍然是从小权边到大权边连接.

    现在看这道题:P4047 [JSOI2010]部落划分

    靠得最近的两部落离得最远.(现在发现这种说法除了可能用二分,还可以用最小生成树)

    上面提到,在最小生成树中移除一条边会使连通块数量加一,如果在完整的生成树的n-1条边中移除k-1条边,将会使得连通块数量从1增加到k,并且移除的边即为这些连通块之间的最短边.

    根据题意,希望让部落的距离(定义为距离最近的两点的距离)尽可能大,那么不停地移除权最大的边即可.

    这意味着只需要在连边时控制连到第n-k条时break,放弃此边并输出其权即可.


    Prim模板O(N2):

    Prim与Kruskal相对地,处理的对象是点.

    设置两个集合,一个集合中为所有已经加入最小生成树的点,另一个中为未加入点.在每一轮操作中,O(N)遍历所有未加入的点,寻找其中与已有生成树距离最小者并将其加入生成树.如此进行N-1轮后所有点便加入了最小生成树.

    其中,与最小生成树距离最近的点 定义为在两集合中各取一点使所得两点间距离最小,此定义即为未加入点集中所取的点.

    实现方法如下,Prim模板的写法具有技巧性:

    // used[i]的真假性相同者处于同一集合,故有两个集合
      for (int i = 1; i < n; i++) { int x = 0; for (int j = 1; j <= n; j++) if (!used[j] && (x == 0 || dist[j] < dist[x])) x = j; used[x] = true; for(int k = 1; k <= n; k++) if(!used[k]) dist[k] = min(dist[k], calc(x, k)); }
    // dist[i](i>=2)即为点i加入最小生成树时所新建的边长度
    // 因此最小生成树边长之和为dist[2]+...+dist[n]

    前面的题目中,当出现完全图时若使用Kruskal往往需要先存储N2数量的边,这在空间复杂度上相对于Prim显示出了劣势.

      Prim算法的时间复杂度为O(N2),可以用二叉堆优化到 O(MlogN)。但用二叉堆优化不如直接使用 Kruskal算法更加方便。因此,Prim主要用于稠密图,尤其是完全图的最小生成树的求解。                ------<<指南>>

    P1265 公路修建

    使用Kruskal的尝试以MLE告终,而使用Prim可以避免存储O(N2)复杂度的数据.

    注意题中"每个“城市联盟”将被看作一个城市,发挥一个城市的作用"可以视为每当一个节点加入了最小生成树时,下一步是找到与整个最小生成树最近的节点.

    并且对于成环的情况:

     A城市联盟会申请AB,C城市联盟会申请AC,但B城市联盟并不会申请BC,成环情况只可能在等边三角形时出现,而最小生成树算法本身就可以完全符合题意地处理这种情况.因此可以无视规则2.

    #include <algorithm>
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    using namespace std;
    
    struct P {
        int x, y;
    } p[5010];
    int n;
    double dist[5010];
    bool used[5010];
    
    double calc(int u, int v) {
        long long dx = p[u].x - p[v].x, dy = p[u].y - p[v].y;
        return sqrt(dx * dx + dy * dy);
    }
    inline int read() {
        char ch = getchar();
        int x = 0, f = 1;
        while (ch > '9' || ch < '0') {
            if (ch == '-') f = -1;
            ch = getchar();
        }
        while (ch >= '0' && ch <= '9') {
            x = x * 10 + ch - '0';
            ch = getchar();
        }
        return x * f;
    }
    
    int main() {
        // freopen("in.txt", "r", stdin);
        n = read();
        for (int i = 1; i <= n; i++) p[i].x = read(), p[i].y = read();
        fill(dist + 1, dist + n + 1, 1e9);
        dist[1] = 0;
    
        for (int i = 1; i < n; i++) {
            int x = 0;
            for (int j = 1; j <= n; j++)
                if (!used[j] && (x == 0 || dist[j] < dist[x])) x = j;
            used[x] = true;
            for(int k = 1; k <= n; k++)
                if(!used[k]) dist[k] = min(dist[k], calc(x, k));
        }
    
        double ans = 0;
        for(int i = 2; i <= n; i++) ans += dist[i];
        printf("%.2f
    ", ans);
    
        return 0;
    }
    P1265
  • 相关阅读:
    129 01 Android 零基础入门 02 Java面向对象 06 Java单例模式 03 饿汉模式 VS 懒汉模式 02 懒汉式的代码实现
    128 01 Android 零基础入门 02 Java面向对象 06 Java单例模式 03 饿汉模式 VS 懒汉模式 01 饿汉式的代码实现
    127 01 Android 零基础入门 02 Java面向对象 06 Java单例模式 02 单例模式概述 01 单例模式的定义和作用
    126 01 Android 零基础入门 02 Java面向对象 06 Java单例模式 01 设计模式概述 01 设计模式简介
    125 01 Android 零基础入门 02 Java面向对象 05 Java继承(下)05 Java继承(下)总结 01 Java继承(下)知识点总结
    leetcode-----121. 买卖股票的最佳时机
    leetcode-----104. 二叉树的最大深度
    Json串的字段如果和类中字段不一致,如何映射、转换?
    Mybatis-Plus的Service方法使用 之 泛型方法default <V> List<V> listObjs(Function<? super Object, V> mapper)
    模糊查询
  • 原文地址:https://www.cnblogs.com/Gaomez/p/14517446.html
Copyright © 2011-2022 走看看