最小生成树

1. Kruskal

#include <bits/stdc++.h>
#define INF 0x3f3f3f3f

using namespace std;
const int N = 1000;

int p[N], n, m;                                 //p[N]用于存放祖先节点, 有n个点, m条边

struct edge {
    int x, y, z;
    friend bool operator<(edge a, edge b) {     //友元重载<, 按权值升序排列
        return a.z < b.z;
    }
} e[N];

//并查集查找祖先节点
int find(int x) {
    return p[x] == x ? x : find(p[x]);
}

int kruskal() {
    int ans = 0, cnt = 0;                       //ans表示已连边的权值之和, cnt代表已连边的个数
    sort(e + 1, e + 1 + m);                     //按照权值升序排序

    for (int i = 0; i <= n; i++) p[i] = i;      //初始化并查集

    //求最小生成树
    for (int i = 1; i <= m; i++) {
        //首先寻找当前节点的祖先节点
        int x = find(e[i].x);
        int y = find(e[i].y);
        if (x == y) continue;                   //如果x和y在同一颗树上则不连边, 防止出现环
        p[x] = y;                               //将x祖先节点加到y上
        ans += e[i].z;                          //权值加一下
        cnt++;                                  //连边的个数加一下
    }

    if (cnt < n - 1) return INF;                //如果n个节点都连了边则一定有n-1条边, 如果已连边的个数不为n-1则说明有的点是不连通的,故不能形成最小生成树
    return ans;                                 //最小生成树存在则返回边权总和
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    int x, y, z;
    for (int i = 1; i <= m; i++) {
        cin >> x >> y >> z;
        e[i] = {x, y, z};                       //建图
    }

    //以下部分根据题目自己填, 这里举个简单例子, 如果最小生成树不存在输出“impossible”, 否则输出权值总和
    int w = kruskal();
    if(w == INF)  cout << "impossible" << endl;
    else cout << w << endl;

    return 0;
}

2. Prim

#include <bits/stdc++.h>
#define INF 0x3f3f3f3f

using namespace std;
const int N = 1000;

int g[N][N], d[N];                                      //数组g用于构建邻接矩阵, 数组d表示当前点到生成树中所有点的距离的最小值
int n, m;
bool v[N];                                              //数组v表示当前点是否已经加入到生成树中

int Prim() {
    memset(d, 0x3f, sizeof d);
    memset(v, 0, sizeof v);
    d[1] = 0;                                           //刚开始的时候随便加一个点进生成树中, 这里选标号为1的点
    int ans = 0;                                        //ans用于记录 最短边 权值总和
    
    for (int i = 0; i < n; i++) {                       //这里其实循环n-1次就足够了,但是需要计算边权所以循环n次
        int x = 0;
        //寻找离生成树所有点最近的那个点(这个点显然必须是生成树外的点)
        for (int j = 1; j <= n; j++)
            if (!v[j] && (x == 0 || d[j] < d[x])) x = j;//如果找到了距离更近的点则把x更新成那个点的编号

        //执行完上述循环之后如果图是连通的则一定能找到一个离生成树中所有点最近的节点x
        //如果x到生成树的距离为INF说明生成树中的点和节点x是不连通的, 故无法形成最小生成树
        if(i && d[x] == INF) return INF;                //当前点不为第一个点且不连通, 直接结束
        if(i) ans += d[x];                              //当前点不为第一个点且连通, 则把最短边的权值加一下
        v[x] = 1;                                       //把当前点加入到生成树中
        
        //遍历当前节点x的出边即x-->y, 更新一下与节点x相邻的点到生成树的距离
        //这一步要放在上述的判断之后, 因为当 y=x 时会出现自环, 如果这个自环权值是负数就会使最小边权值总和越更新越小
        for (int y = 1; y <= n; y++)
            if (!v[y]) d[y] = min(d[y], g[x][y]);
    }
    
    return ans;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    memset(g, 0x3f, sizeof g);
    for (int i = 1; i <= n; i++) g[i][i] = 0;           //去除自环
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        cin >> x >> y >> z;
        g[x][y] = g[y][x] = min(g[x][y], z);            //双向建图
    }

    //以下部分自己根据题意写, 这里举个简单的例子
    int w = Prim();
    if(w == INF) cout << "impossible" << endl;          //不连通无法形成最小生成树
    else cout << w << endl;                             //能形成最小生成树则输出最小边权值总和
    
    return 0;
}

3. Borůvka

#include <bits/stdc++.h>
#define INF 0x3f3f3f3f

using namespace std;
const int N = 1000;

struct edge {
    int x, y, z;
} e[N];

//割边就是当前集合向外伸出去的边
//数组p用于存放公共祖先节点, 数组d表示各个子树最小割边的权值, idx表示各个子树最小割边的编号
int p[N], d[N], idx[N];
int n, m;                                         //n个点, m条边
bool v[N];                                        //数组v用于标记当前边是否已经被使用过

//并查集查找祖先节点
int find(int x) {
    return p[x] == x ? x : find(p[x]);
}

int Boruvka() {
    int ans = 0;                                  //ans用于记录各个子树最小割边的权值之和
    for (int i = 0; i <= n; i++) p[i] = i;        //初始化, 给每个节点都分配一个编号, 并且使他们都处于互相不连通的状态
    memset(v, 0, sizeof v);

    while (1) {
        int cnt = 0;                              //cnt用于表示是否有合并子树的操作
        memset(d, 0x3f, sizeof d);

        for (int i = 1; i <= m; i++) {            //遍历所有的边
            //查找每条边的祖先节点, 其实就是想知道当前处理的边属于哪颗子树
            int x = find(e[i].x);
            int y = find(e[i].y);
            int z = e[i].z;
            if (x == y) continue;                 //e[i].x和e[i].y在同一个子树上则不做处理
            cnt++;                                //每进行一次合并子树操作数加1

          /*为方便解释,将所有以a为祖先节点的子树称为“子树a”
            当前边的权值比子树x/y的记录的最小割边的权值还要小, 则更新以下子树x/y的最小割边权值
            还有一点很重要, 如果当前边和子树x/y的最小割边权值相同, 若不加区分应该连哪条边就有可能出现环, 
            所以当两边权值相同时我们以编号小的作为当前子树的最小割边编号, 这样就能有效避免环的出现*/
            if (z < d[x] || z == d[x] && i < idx[x]) d[x] = z, idx[x] = i;
            if (z < d[y] || z == d[y] && i < idx[y]) d[y] = z, idx[y] = i;
        }

        if (!cnt) break;                          //没有合并操作了, 说明所有的子树都合成为一颗树了, 退出循环

        //子树的合并操作
        for (int i = 1; i <= n; i++)
            //如果当前节点是某一子树的祖先节点,并且没有被合并过,则进行合并
            if (d[i] != INF && !v[idx[i]]) {
                //这里用不用find函数都可以, 因为当前节点已经是祖先节点了, 但为了形式上的美观这里还是用了find函数
                int x = find(e[i].x);
                int y = find(e[i].y);
                int z = e[i].z;
                p[x] = y;
                ans += z;
                v[idx[i]] = 1;
            }
    }
    return ans;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    int x, y, z;
    for (int i = 1; i <= m; i++) {
        cin >> x >> y >> z;
        e[i] = {x, y, z};
    }

    int w = Boruvka();
    //这个部分自己根据题意写
    return 0;
}
posted @ 2021-03-05 14:15  Xiezeju  阅读(74)  评论(0编辑  收藏  举报