最小生成树,Kruskal算法

最小生成树(Minimum Spanning Tree,简称 MST)是一个连通图的子图,它包含图中的所有节点,并且是一个树(无环连通图),同时保证连接所有节点的边的权重之和最小。

在一个带权重的连通图中,有许多不同的方式可以连接所有节点,但最小生成树的概念强调的是选择最小权重的边,以使整个树的总权重最小。

有两种常见的算法用于求解最小生成树问题:Prim 算法和 Kruskal 算法。

1、Prim 算法:Prim 算法从一个起始节点开始,逐步地选择连接当前生成树和非树节点的最小权重边,将该节点添加到生成树中,直到所有节点都被包含在生成树中。
2、 Kruskal 算法:Kruskal 算法首先将所有边按照权重从小到大排序,然后从最小权重的边开始逐个加入生成树,但是要保证不形成环路,即所加入的边的两个节点不能在同一个集合中。

ps:Kruskal 算法使用到了 并查集!

1584. 连接所有点的最小费用

给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。

连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。

请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。

最小生成树,Kruskal算法_第1张图片

解法

根据题意,我们得到了一张n个节点的完全图,任意两点之间的距离均为它们的曼哈顿距离。现在我们需要在这个图中取得一个子图,恰满足子图的任意两点之间有且仅有一条简单路径,且这个子图的所有边的总权值之和尽可能小。

能够满足任意两点之间有且仅有一条简单路径只有树,且这棵树包含 n 个节点。我们称这棵树为给定的图的生成树,其中总权值最小的生成树,我们称其为最小生成树。

最小生成树有一个非常经典的解法:Kruskal

代码如下:

// 定义了 DisjointSetUnion类,这是一个并查集类,用来管理节点的集合关系
class DisjointSetUnion{
private: // 在 private 下的成员变量和函数只能在 类的内部 访问
    vector<int> f,rank;
    int n;

public:
    //  构造函数
    DisjointSetUnion(int _n){
        // n个节点
        n = _n;
        rank.resize(n,1);
        f.resize(n);
        for(int i = 0;i<n;i++){
            f[i] = i;
        }
    }

    // 并查集
    int find(int x){
        // 路径压缩优化
        return f[x]==x? x:f[x]=find(f[x]);
    }

    //  判断x和y是否在一个集合中,如果已经在一个集合了,返回false
    // 如果不在一个集合中,通过rank进行合并
    int unionSet(int x,int y){
        int fx = find(x), fy = find(y);
        if(fx == fy){
            return false;
        }
        if(rank[fx] < rank[fy]){
            f[fx] = fy;
        }else if(rank[fx]>rank[fy]){
            f[fy] = fx;
        }else {
            f[fx] = fy;
            rank[fy]++;
        }
        return true;
    }
};

// 定义结构体Edge
struct Edge{
    int len,x,y;
    // 是一个构造函数 Edge,用来构造Edge对象
    // 通过 len(len), x(x) 和 y(y),将传入的参数分别赋值给结构体的成员变量
    Edge(int len,int x,int y):len(len),x(x),y(y){
    }
};

class Solution {
public:
    int minCostConnectPoints(vector<vector<int>>& points) {
        //  lambda 函数
        // [&] 表示 lambda 函数通过引用捕获外部作用域中的变量,
        // 这意味着在 lambda 函数内部, 可以访问外部作用域中的变量。
        // 计算x和y这两个点之间的曼哈顿距离
        auto dist = [&](int x,int y)-> int {
            return abs(points[x][0] - points[y][0]) 
                                + abs(points[x][1] - points[y][1]);
        };

        int n = points.size();
        // 创建了一个名为 dsu 的 DisjointSetUnion 类的对象
        DisjointSetUnion dsu(n);

        vector<Edge> edges;
        for(int i = 0;i<n;i++){
            for(int j = i+1;j<n;j++){
                // 把每两个点之间的距离、起点、终点放入 vector中
                // dist(i, j), i, j 是用于构造 Edge 对象的参数
                edges.emplace_back(dist(i,j),i,j);
            }
        }

        // 根据权重来排序Edge,权重越小的排在前面
        sort(edges.begin(),edges.end(),[](Edge a,Edge b)->int {return a.len<b.len;});
        // num 表示已经连接的点的数量
        int ret = 0,num = 1;

        //遍历排序后的边,如果两个节点不在同一个集合中,则将它们合并,
        // 并将边的长度加入 ret,同时更新 num
        for(auto& [len,x,y]:edges){
            if(dsu.unionSet(x, y)){
                // 合并 x、y
                ret += len;
                num++;
                if(num == n){
                    break;
                }
            }
        }
        return ret;
    }
};

1135. 最低成本联通所有城市

想象一下你是个城市基建规划者,地图上有 n 座城市,它们按以 1 到 n 的次序编号。

给你整数 n 和一个数组 conections,其中 connections[i] = [xi, yi, costi] 表示将城市 xi 和城市 yi 连接所要的costi(连接是双向的)。

返回连接所有城市的最低成本,每对城市之间至少有一条路径。如果无法连接所有 n 个城市,返回 -1

该 最小成本 应该是所用全部连接成本的总和。

最小生成树,Kruskal算法_第2张图片
代码:

class Solution {
private:
    vector<int> father,rank;
public:
    // 路径压缩
    int Find(int n){
        return father[n] == n? father[n] : father[n] = Find(father[n]);
    }

    // 按秩合并
    int unionSet(int x,int y){
        int fx = Find(x), fy = Find(y);
        if(fx == fy) return false;
        if(rank[fx]<rank[fy]){
            father[fx] = fy;
        }else if(rank[fx]>rank[fy]){
            father[fy] = fx;
        }else {
            father[fx] = fy;
            rank[fy]++;
        }
        return true;
    }

    int minimumCost(int n, vector<vector<int>>& connections) {
        // 初始化father数组
        father.resize(n+1,0);
        for(int i = 1;i<=n;i++){
            father[i] = i;
        }
        // 初始化rank数组
        rank.resize(n+1,1);

        // 根据权重重新排一下 connections
        sort(connections.begin(),connections.end(),
        [](vector<int> a,vector<int> b){
            return a[2]<b[2];
        });

        int ans = 0, num = 1;

        for(auto conn:connections){
            if(unionSet(conn[0],conn[1])){
                // 如果不是一个集合的,进行合并
                ans += conn[2];
                num++;
            }
            if(num == n){
                break;
            }
        }
        // 如果没有联通所有的点,则返回-1
        return num == n? ans:-1;
    }
};

你可能感兴趣的:(写力扣时自己的笔记,算法)