• 最小生成树模板


    prim算法

    算法思想:每次选定未进入集合中和集合距离最小的点,用该点更新其他点到集合的距离,直到可以判断出不存在最小生成树或所有点均已进入集合
    下面虽然是两种写法,但是记忆时最好还是按照算法的思路来实现,也就是第2个代码。虽然会多一些边界处理,但是只要我们理解了算法思想即使忘记了代码也能够自己实现出来

    /**
     * 不需要每次都用集合中的点去找距离集合最近的点
     * 可以维护每个点到集合的距离,可以节省很多时间
     * 
     * 每次找到距离集合最近的点,加入到集合中,然后用该点更新其它点到集合的距离,每次操作向集合中加入一个点,一共n个点所以一共需要迭代n次
     * 
     * 本题为稠密图,所以采用邻接矩阵存储
     */
    
    // 不太符合算法思想的写法,但是可以减少一些判断条件
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 510, INF = 0x3f3f3f3f;
    
    int n, m;
    int dis[N]; // 之前的dis表示某点距离起点的距离,此时表示某点距离集合的距离
    bool vis[N]; // 表示某个点是否在集合中
    int g[N][N];
    
    int prim()
    {
        memset(dis, 0x3f, sizeof dis);
        /**
         * 关于dis[1]的初始化问题
         * 按照该算法的逻辑来讲,不应该初始化这个点
         * dis[1] = 0表示1号点已经位于集合内了
         * 但是初始状态集合中应该为空
         * 不过这么写答案是正确的并且可以减少一下判断条件,只是和算法思想有些不符
         */
        dis[1] = 0;
         
        int res = 0;
        for (int i = 0; i < n; ++ i) // 每次将一点放入集合
        {
            // 找到未在集合且距集合最近的点
            int t = -1;
            for (int j = 1; j <= n; ++ j)
                if (!vis[j] && (t == -1 || dis[t] > dis[j]))
                    t = j;
            
            // 将该点放入集合并累加答案
            vis[t] = true;
            if (dis[t] == INF) return -1; // 说明目前找到的距离集合最近的点已经是无穷远的点了,说明无法构成一棵最小生成树
            res += dis[t];
        
            // 用该点更新其它节点距离集合的距离
            for (int j = 1; j <= n; ++ j)
                dis[j] = min(dis[j], g[t][j]); // 因为此时t已经进入了集合,所以和t相连的j点距离集合的距离就是j点距离t点的距离
        }
         
        return res;
    }
    int main()
    {
        cin >> n >> m;
        // memset(g, 0x3f, sizeof g);
        /**
         * 对于g的初始化问题
         * 朴素版dijkstra和Floyd都采用的是邻接矩阵
         * dijkstra全部初始为无穷大
         * Floyd自己到自己初始化为0,其它初始化为无穷大
         * 怎么初始化完全要看算法执行过程对这些数据的要求
         * 按照我们在prim中的写法dis[j] = min(dis[j], g[t][j])
         * 可以发现用到自己到自己距离时说明t已经进入集合了,那么它的dis正确与否已经无所谓了,并不会影响到其它点到集合的距离,所以自己到自己不用初始化也可以
         */
         while (m --)
         {
             int a, b, k;
             cin >> a >> b >> k;
             g[a][b] = g[b][a] = min(g[a][b], k);
         }
         
         int t = prim(); // t表示返回的最小生成树的树边权重之和,-1表示最小生成树不存在
         
         if (t == -1) cout << "impossible" << endl;
         else cout << t << endl;
         
         return 0;
    }
    
    // 符合算法思想的写法
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 510, INF = 0x3f3f3f3f;
    
    int n, m;
    int dis[N];
    bool vis[N];
    int g[N][N];
    
    int prim()
    {
        memset(dis, 0x3f, sizeof dis); // 初始化dis[1] = 0并不会使答案发生改变,因为第一个点无论距离集合的距离为多少,总是会进入集合的,并且用它更新其它点时也不会用到自己的dis,所以无影响,但是从实际意义角度来讲不应该赋值0
        int res = 0;
        for (int i = 0; i < n; ++ i)
        {
            // 寻找距离集合距离最小的点
            // 所有距离都是无穷大,那么无穷大就是最小距离,我这思维定势了,觉得无穷大就不会是距离最小的点
            int t = -1;
            for (int j = 1; j <= n; ++ j)
                if (!vis[j] && (t == -1 || dis[t] > dis[j]))
                    t = j;
                    
            if (i && dis[t] == INF) return INF; // 不存在最小生成树的条件也是找到的最小距离都是无穷远,但是第一个点的距离肯定是INF,所以需要特判一下
            vis[t] = true;
            if (i) res += dis[t]; // 因为第一个点的dis是无穷大,所以不能累加到答案上,本身第一个点到集合的距离就是0,所以加不加无所谓
            
            for (int j = 1; j <= n; ++ j)
                dis[j] = min(dis[j], g[t][j]);
        }
        
        return res;
    }
    int main()
    {
        memset(g, 0x3f, sizeof g); // 从使用g的位置可以看出,即使g[i][i]这种没更新答案也不会受到影响,其实更新了肯定不会错,如果不知道需不需要初始化那就写上,总不会错
        cin >> n >> m;
        while (m --)
        {
            int a, b, k;
            cin >> a >> b >> k;
            g[a][b] = g[b][a] = min(g[a][b], k);
        }
        
        int t = prim();
    
        if (t == INF) cout << "impossible" << endl;
        else cout << t << endl;
        
        return 0;
    }
    

    Kruskal算法

    /**
     * Kruskal算法的理论思路
     * 首先对所有边从小到大排序,然后遍历所有边,如果该边的加入不会使得树中出现回路,那么就加入这条边
     * 
     * Kruskal算法的实现思路
     * 理论思想中边的排序和遍历都很好实现,难点就在于如何判断该边的加入会不会出现回路
     * 假设点a和b已经连通了,如果再在a和b之间连一条边那么势必就产生回路
     * 所以代码实现中只需要判断一条边的两个端点是否已经连通,是则不加入这条边,否则加入
     */
     
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 1e5 + 10, M = 2e5 + 10, INF = 0x3f3f3f3f;
    
    struct Edge {
        int a, b, w;
    }edges[M];
    
    int n, m;
    int p[N];
    
    int find(int u)
    {
        if (p[u] != u) p[u] = find(p[u]);
        return p[u];
    }
    int kruskal()
    {
        int res = 0, cnt = 0; // res:最小生成树的权重之和 cnt:最小生成树中边数,负责判断是否存在最小生成树
        
        // kruskal第一步:将所有边按照权值从小到大排序
        sort(edges, edges + m, [](Edge a, Edge b) {return a.w < b.w;});
        
        // kruskal第二步:遍历所有边,将符合条件的边加入集合中
        for (int i = 0; i < m; ++ i)
        {
            int a = edges[i].a, b = edges[i].b, w = edges[i].w;
            int pa = find(a), pb = find(b);
            
            if (pa != pb)
            {
                p[pa] = pb;
                res += w;
                ++ cnt;
            }
        }
        
        if (cnt == n - 1) return res; // n个点的最小生成树有n-1条边
        else return INF;
    }
    int main()
    {
        cin >> n >> m;
        for (int i = 0; i < m; ++ i)
        {
            int a, b, w;
            cin >> a >> b >> w;
            edges[i] = {a, b, w};
        }
        for (int i = 1; i <= n; ++ i) p[i] = i; // 并查集初始化
        
        int t = kruskal();
        
        if (t == INF) cout << "impossible" << endl;
        else cout << t << endl;
        
        return 0;
    }
    
  • 相关阅读:
    Java并发编程系列并发基础
    Java的引用到底是什么?和C/C++的指针有什么区别?
    逆向进阶,利用 AST 技术还原 JavaScript 混淆代码
    Java并发编程系列之二线程基础
    以Linux包管理方式安装Nvidia显卡驱动
    软件测试相关抓包charlesThrottle Settings功能模拟弱网
    APIC虚拟化
    virtio简介(五)—— virtio_blk设备分析
    QEMU pipe设备分析
    5.5文章汇总
  • 原文地址:https://www.cnblogs.com/G-H-Y/p/14347667.html
Copyright © 2020-2023  润新知