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
  • 相关阅读:
    Orcale分析函数OVER(PARTITION BY... ORDER BY...)的讲解
    Linux下安装Redmine(项目管理软件)
    CentOS5.4安装redmine详细步骤
    CentOS安装redmine 2后的简单配置
    在linux上安装redmine
    Linux安装MediaWiki
    Linux下安装配置MediaWiKi全过程
    用Navicat_SSH 连接数据库服务器
    基于C#的MongoDB数据库开发应用(4)--Redis的安装及使用
    基于C#的MongoDB数据库开发应用(3)--MongoDB数据库的C#开发之异步接口
  • 原文地址:https://www.cnblogs.com/Gaomez/p/14517446.html
Copyright © 2011-2022 走看看