代码随想录算法训练营Day63|最小生成树算法prim和kruskal、kama 53.寻宝

最小生成树

        最小生成树(Minimum Spanning Tree,MST)算法是图论中的一个重要概念,它是指在加权无向图中,找到一个边的子集,构成一个树形结构,该子集包含图中所有的顶点,同时保证所有边的权值之和最小。这个构成的树就称为最小生成树。

        常用的最小生成树算法主要有两种:

  1. 普里姆算法(Prim's algorithm):
  • 从图中的一个顶点开始,逐步增加新的边和顶点,直到形成最小生成树。
  • 算法的关键步骤是选择最小的边来扩展当前的树。
  • 该算法适合于边稠密的图。

     2.克鲁斯卡尔算法(Kruskal's algorothm):

  • 他按边的权重顺序(从小到大)选择边,并检查是否加入这条边后会形成环。
  • 如果不会形成换,则将这条边加入到最小生成树中。
  • 该算法适合于边稀疏的图。

        这两种算法能够有效地找到加权无向图的最小生成树,但它们在处理图的顶点和边的关系上有所不同。普里姆算法从顶点出发,而克鲁斯卡尔算法从边出发。

        最小生成树算法在实际应用中非常重要,例如在设计和构建物理网络(如电信网络、计算机网络、道路建设等)时,通过最小生成树算法可以以最小的成本构建出覆盖所有节点的网络结构。此外,它们在许多优化问题中也扮演着关键角色,如电路设计、网络构建等。

寻宝

53. 寻宝(第七期模拟笔试) (kamacoder.com)

prim算法

prim算法的核心有三步

  1. 选距离生成树最近节点
  2. 最近节点加入生成树
  3. 更新非生成树节点到生成树的距离(更新minDist数组)

minDist数组是用来记录每一个节点距离最小生成树的最近距离。

minDist数组的值初始化为最大数,本题初始化为10001(根据题目设定)。

vector<int>minDist(V+1,10001)//共有V个顶点,这里为了与顶点编号对应,数组从1到v,0弃用

第一步需要选距离生成树最近节点,在开始时,并没有生成树,也没有节点,考虑随便加入一个节点(注意对于最小生成树来说,每个节点都是必要的,所以可以随便加入),这里先加入节点1.

然后对与1节点相连的节点,更新minDist数组,即将权赋值到minDist数组上。

之后就是不断选择距离最小生成树最近的节点,加入到最小生成树,更新非生成树节点到生成树的距离,直到最后所有节点都在生成树中,最后对minDist数组的第二位到末尾相加,即为最小权和。

具体步骤详解可以参考代码随想录 (programmercarl.com)

#include<vector>
#include<iostream>
#include<climits>
using namespace std;

int main(){
    int V,E; // V 表示顶点的数量,E 表示边的数量
    cin>>V>>E; // 输入顶点和边的数量

    int x,y,k; // x, y, k 分别表示边的两个顶点和权重
    // 创建一个 V+1 行 V+1 列的二维向量 grid,初始化所有值为 10001(表示无穷大)
    vector<vector<int>>grid(V + 1,vector<int>(V + 1,10001));

    // 读入每条边的信息,并更新 grid 中的权重值
    for(int i = 0; i < E;i++){
        cin >>x>>y>>k; // 输入边的两个顶点和权重
        grid[x][y] = k; // 由于是无向图,所以两个方向的权重相同
        grid[y][x] = k;
    }

    // 初始化两个辅助向量,minDist 用于记录从已选树顶点到每个顶点的最小距离,isInTree 用于标记顶点是否已加入最小生成树
    vector<int> minDist(V + 1, 10001); // 初始化所有最小距离为无穷大
    vector<int> isInTree(V + 1, 0); // 初始化所有顶点都未加入最小生成树

    //Prim 算法的核心部分,选择 V-1 条边加入最小生成树
    for(int i = 1; i < V; i ++){
        int cur = -1; // 当前选择的顶点
        int minVal = INT_MAX; // 从已选树顶点到当前顶点的最小距离,初始化为无穷大

        // 遍历所有顶点,找到未加入树且距离最小的顶点
        for(int j = 1; j <=V;j++){
            if(!isInTree[j] and minDist[j] < minVal){
                minVal = minDist[j];
                cur = j;
            }
        }
        isInTree[cur] = 1; // 将当前顶点标记为已加入最小生成树

        // 更新从已选树顶点到其他顶点的最小距离
        for(int j = 1; j <= V;j++){
            if(!isInTree[j] and grid[cur][j] < minDist[j]){
                minDist[j] = grid[cur][j];
            }
        }
    }

    // 计算最小生成树的总权重
    int result = 0;
    for(int i= 2;i<=V;i++){ // 从 2 开始,因为 1 是起始顶点,其最小距离为 0,不应该计入总权重
        result += minDist[i];
    }
    cout<<result<<endl; // 输出最小生成树的总权重

}

算法的时间复杂度为O(V^2),空间复杂度为O(V^2)。

Kruskal算法

相比于prim算法,Kruskal算法侧重边的权重顺序(从小到大)选择边,并检查加入该边后是否会形成环,若不会形成环,则将这条边加入到最小生成树中。该算法适合于边稀疏的图。

算法的步骤如下:

  1. 排序:将图中的所有边按权重从小到大排序。
  2. 初始化:创建一个森林,其中每个顶点都是一个独立的树。
  3. 选择边:按排序后的顺序选择边,对于每条边(v,w),执行一下操作
    1. 使用一个并查集来检查v和w是否在同一个集合中。
    2. 若v和w不在同一个集合中,则将这条边加入的生成树中,并将v和w所在集合合并
  4. 重复:直到森林F中包含V-1条边,这是森林F就是最小生成树。(连接v个顶点最少需要v-1条边)
  5. 结束:构造最小生成树完成。

详解可以看 代码随想录 (programmercarl.com)

代码如下:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 定义边结构体,包含两个顶点和一个权重
struct Edge{
    int l, r, val;
};

// 定义顶点的最大数量
int n = 10001;

// 定义并查集数组,用于存储每个顶点的父节点
vector<int> father(n,-1);

// 初始化并查集,每个顶点的父节点设置为自己
void init(){
    for(int i = 0; i < n; i++)
        father[i] = i;
}

// 查找函数,用于找到顶点u的根节点,同时进行路径压缩
int find(int u){
    return u == father[u]? u:father[u] = find(father[u]);
}

// 合并函数,用于合并两个集合
void join(int u, int v){
    int m = find(u); // 找到顶点u的根节点
    int n = find(v); // 找到顶点v的根节点
    if(m == n) return; // 如果根节点相同,说明已经在同一个集合中
    father[n] = m; // 将顶点v的根节点设置为顶点u的根节点
}

int main(){
    int v, e; // v表示顶点数量,e表示边数量
    vector<Edge> edges; // 存储所有边的数组
    cin >> v >> e; // 输入顶点数量和边数量
    int v1, v2, val; // v1, v2表示边的两个顶点,val表示边的权重
    for(int i = 0 ; i < e; i++){ // 读取所有边的信息
        cin >> v1 >> v2 >> val; // 输入边的两个顶点和权重
        edges.push_back({v1, v2, val}); // 将边添加到数组中
    }

    // 按边的权重对边进行排序
    sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) {
        return a.val < b.val;
    });

    init(); // 初始化并查集
    int result = 0; // 用于存储最小生成树的总权重
    for (Edge edge : edges) { // 遍历所有边
        int x = find(edge.l); // 找到边左顶点的根节点
        int y = find(edge.r); // 找到边右顶点的根节点
        if (x != y) { // 如果根节点不同,说明加入这条边不会形成环
            result += edge.val; // 将边的权重加到总权重上
            join(x, y); // 合并两个集合
        }
    }
    cout << result << endl; // 输出最小生成树的总权重
    return 0;
} 

Kruskal算法的时间复杂度主要取决于边的排序和并查集操作。排序的时间复杂度是 O(E log E),其中 E 是边的数量。在边排序之后,算法会遍历每条边,并执行并查集操作,这个操作的时间复杂度接近 O(E log V),其中 V 是顶点的数量。因此,Kruskal算法的总时间复杂度是 O(E log E) 或 O(E log V),取决于哪个是主导项。

Kruskal算法的空间复杂度主要取决于并查集数据结构,它需要存储每个顶点的父节点信息,因此空间复杂度是 O(V)。

Kruskal算法适用于边比较稀疏的图,因为它的时间复杂度与边的数量成对数关系,而不是与顶点的平方成对数关系,如prim算法那样。

   

  • 10
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值