最小生成树模板

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;
}
posted @ 2021-01-29 23:45  0x7F  阅读(80)  评论(0编辑  收藏  举报