首页 > 编程语言 >最小生成树算法(Prim算法 + Kruskal算法)

最小生成树算法(Prim算法 + Kruskal算法)

时间:2024-12-10 11:33:34浏览次数:5  
标签:Prim weight int Kruskal second 算法 forest 顶点 节点

最小生成树(MST)算法


完整版万字原文见史上最全详解图数据结构


加权无向图中,最小生成树是一个包含图中所有节点的子图

树 ———> 包含图中所有节点

最小 ———> 树中的边权之和最小


1. Prim算法(最小生成树)


1


算法原理:

1. 贪心算法

2. 从一个起始点开始,逐步选择与当前生成树相连的、权重最小的边,直到所有节点都被包含在生成树中(贪心策略保证了每次选择的边都是当前情况下的最佳选择)

3. 时间复杂度为 O(V^2)(稠密图)

4. 应用:适用于求解加权无向图的最小生成树,尤其是稠密图。

提要 :

Prim算法 过程非常类似于 Dijkstra 算法

只有 distance 数组和 weight 数组保存的值的意义不同 : 

Dijkstra 保存从该点到起始点最短路径长度,Prim 保存已选集合和未选集合之间最短的边权

复习 dijkstra算法


代码实现(未优化版本)


void MyGraph::Prim()
{
    vector<bool> visited(n, false);
    // 创建一个向量 weight,记录每个节点与生成树的最小连接权重,初始值为无穷大
    vector<int> weight(n, numeric_limits<int>::max());
    // prev 向量记录最小生成树中每个节点的父节点,初始值为 -1
    vector<int> prev(n, -1);
    // 设置起始节点的权重为 0,表示从第一个节点开始构建生成树。
    weight[0] = 0;


    for (int i = 0; i < n; i++)
    {
        int minVertex = -1;
        int minWeight = numeric_limits<int>::max();

        // 从尚未加入生成树的节点中,找到权重最小的节点 minVertex
        for (int i = 0; i < n; i++)
            if (!visited[i] && weight[i] < minWeight)
                minWeight = weight[i], minVertex = i;

        // 将该节点加入最小生成树,并增加已加入节点的计数。
        visited[minVertex] = true;
        // 遍历 minVertex 的邻接节点,更新连接权重,如果当前邻接节点的权重小于之前记录的最小权重,则更新权重并记录其父节点
        for (const auto &pair : adjList[minVertex])
            if (!visited[pair.first] && pair.second < weight[pair.first])
                weight[pair.first] = pair.second, prev[pair.first] = minVertex;

        // 打印输出
        for (int i = 1; i < n; i++)
            cout << "Edge " << prev[i] << " - " << i << " with weight " << weight[i] << endl;
    }
}

代码实现(优化版本)

void MyGraph::Prim()
{
    vector<bool> visited(n, false);                       
    vector<int> weight(n, numeric_limits<int>::max()); 
    vector<int> prev(n, -1);                              


    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;

    pq.push({0, startVertex});
    weight[startVertex] = 0; 

    //for (int i = 0; i < n; i++)
    while (!pq.empty())
    {
        // int minVertex = -1;
        // int minWeight = numeric_limits<int>::max();
        int now_minWeight = pq.top().first;
        int now_minVertex = pq.top().second;
        pq.pop();

        // for (int i = 0; i < n; i++)
        //     if (!visited[i] && weight[i] < minWeight)
        //         minWeight = weight[i], minVertex = i;

        visited[now_minVertex] = true;


        for (const auto &neighbor : adjList[now_minVertex])
        {
            if (!visited[neighbor.first] && neighbor.second < weight[neighbor.first])
            {
                weight[neighbor.first] = neighbor.second;
                prev[neighbor.first] = now_minVertex;
                pq.push({neighborWeight, neighbor.first});
            }
        }
    }
    for (int i = 0; i < n; i++)
        if (i != startVertex)
            cout << "vertex " << i << " distance from " << startVertex << " is " << weight[i] << endl;
}

2. Kruskal算法(最小生成树)


算法原理:

1. Kruskal 算法按边的权重升序排序,然后通过并查集判断边是否形成环,逐步选择不形成环的边,构建最小生成树。

2. 时间复杂度为 O(E log E)

3. 应用:适用于稀疏图的最小生成树。

算法步骤

1. 排序边:

     将所有边按照权重升序排序。

2. 初始化并查集:

    每个节点自成一个集合,即每个节点都是其自己的根。

3. 构造最小生成树:

    从排序后的边中,从最小权重开始顺序处理。

4. 对于每条边 (u, v):

    使用并查集检查 u 和 v 是否在同一个集合中。

    (1)如果不在同一个集合中,那么这条边可以安全地加入到生成树中,因为没有形成环。
    (2)如果在同一个集合中,那么这条边会导致生成环,因此跳过这条边。

5. 重复步骤:直到生成树包含 V-1 条边,其中 V 是图中节点的数量。

为了有效地检测边是否会形成环,Kruskal 算法使用 并查集(Union-Find) 数据结构。

建议先去了解 并查集


代码实现

void MyGraph::Kruskal()
{
    // 创建一个存储边的向量 edges,每个边由权重和两个顶点组成
    vector<pair<int, pair<int, int>>> edges;
    for (int i = 0; i < n; i++)
        for (const auto &edge : adjList[i])
            edges.push_back({edge.second, {i, edge.first}});

    // 1. 按边的权重升序排序:首先将图中的所有边按权重从小到大排序。
    sort(edges.begin(), edges.end());

    //2. 初始化并查集:
    // 初始化一个并查集 forest,用于判断不同的节点是否属于同一个连通分量(是否形成环)。
    // forest 保存每个顶点和其父节点
    vector<pair<int, int>> forest(n);

    // 每个节点自成一个集合,即每个节点都是其自己的根
    for (int i = 0; i < n; i++)
        forest[i] = {i, i};

    // 3. 构造最小生成树
    // 遍历所有边,如果两个节点不属于同一个集合(不形成环),则将该边加入生成树,并合并这两个集合。当加入的边数达到 (V-1) 时,最小生成树构建完成
    int numEdges = 0;
    for (const auto &edge : edges)
    {
        int root1 = find(forest, edge.second.first);
        int root2 = find(forest, edge.second.second);
        if (root1 != root2)
        {
            forest[root1] = {root1, root2};
            numEdges++;

            cout << "Edge " << edge.second.first << " - " << edge.second.second << " with weight " << edge.first << endl;
            if (numEdges == n - 1)
                break;
        }
    }
}

// 用于并查集(Union-Find)算法中的“路径压缩”查找操作。它递归地查找某个顶点的根,并且在查找过程中把该路径上的所有顶点直接连接到根,提高后续查找效率
// 找到 vertex 所属集合的根节点
int find(vector<pair<int, int>> &forest, int vertex)
{
    // forest[vertex].second 表示顶点 vertex 的父节点。如果当前顶点的父节点不是它自己,说明它不是根节点。
    // 在递归返回后,路径压缩的核心操作发生:将 vertex 直接连接到根节点。这一步显著提高了未来查找的效率,因为路径被压缩了
    if (forest[vertex].second != vertex)
        forest[vertex].second = find(forest, forest[vertex].second);
    return forest[vertex].second;
    /*假设
    forest = {
        {0, 0},  // 顶点 0 的父节点是 0 (根节点)
        {1, 0},  // 顶点 1 的父节点是 0
        {2, 1},  // 顶点 2 的父节点是 1
        {3, 2},  // 顶点 3 的父节点是 2
        {4, 3},  // 顶点 4 的父节点是 3
    }
    这个结构表示顶点 0 是根节点,其他顶点通过不同的路径连接到根节点 0。

    调用 find(forest, 4) 来查找顶点 4 的根节点。
    forest[4].second != 4:顶点 4 的父节点是 3,而不是它自己,所以继续递归查找。
    递归调用 find(forest, 3):
    forest[3].second != 3:顶点 3 的父节点是 2,继续递归查找。
    递归调用 find(forest, 2):
    forest[2].second != 2:顶点 2 的父节点是 1,继续递归查找。
    递归调用 find(forest, 1):
    forest[1].second != 1:顶点 1 的父节点是 0,继续递归查找。
    递归调用 find(forest, 0):
    forest[0].second == 0:顶点 0 的父节点是它自己,返回 0 作为根节点。

    接着路径压缩开始生效:
    路径压缩应该是从递归返回时,按递归调用的顺序依次更新父节点。
    forest[1].second = 0(顶点 1 的父节点更新为根节点 0)
    forest[2].second = 0(顶点 2 的父节点更新为根节点 0)
    forest[3].second = 0(顶点 3 的父节点更新为根节点 0)
    forest[4].second = 0(顶点 4 的父节点更新为根节点 0)

    最终,find(forest, 4) 返回 0,并且路径压缩后,forest 变为:
    forest = {
        {0, 0},  // 顶点 0 的父节点是 0
        {1, 0},  // 顶点 1 的父节点是 0
        {2, 0},  // 顶点 2 的父节点是 0
        {3, 0},  // 顶点 3 的父节点是 0
        {4, 0},  // 顶点 4 的父节点是 0
    }
    */
}

标签:Prim,weight,int,Kruskal,second,算法,forest,顶点,节点
From: https://blog.csdn.net/2301_81373791/article/details/144213766

相关文章

  • 图常见算法大全( 三种遍历算法 + 三种最短路径算法 + 两种最小生成树)
    图的经典算法完整版万字原文见史上最全详解图数据结构一、图的遍历算法1.voidDFS(intstartVertex);2.voidBFS(intstartVertex);3.voidTopologicalSort();(两种实现方式)1.DFS(深度优先搜索)算法原理是一种用于遍历或搜索图(包括树)中节点的算法。其基本思想......
  • 【数据结构与算法】回溯算法:LeetCode“排列问题” 求解,解释并模拟递归+回溯的遍历过程
      【作者自述:记录学习笔记,既然写了就让更多的人看到吧!欢迎大家关注交流学习,一步一个脚印持续更新!】【更多推荐笔记】【数据结构与算法】动态规划:解密“完全背包问题”的真相!附LeetCode四大问题的实现-CSDN博客【数据结构与算法】动态规划:解密“0-1背包问题”的真相!附LeetC......
  • 成为大厂算法工程师,有什么条件?如何快速拿到offer
    对求职者来说,能成为一名大厂的算法工程师,无疑是职业生涯的巅峰。毕竟,互联网大不同厂工种薪资排序,大体是算法>工程>产品>运营>其他,同职级的员工,算法的薪水可能是运营人员的一倍,甚至还要高。目前,主流互联网大厂的算法岗位一般有搜索、广告、推荐(统称搜广推)算法;NLP(自然语言处......
  • 【风光不确定】基于多时间尺度滚动优化算法的主动配电网研究【IEEE33节点】(Matlab代码
    目录......
  • 容器算法迭代器初识
    了解STL中容器、算法、迭代器概念之后,我们利用代码感受STL的魅力STL中最常用的容器为vector,可以理解为数组,下面我们将学习如何向这个容器中插入数据,并遍历这个容器Vector存放内置数据类型容器:vector算法:for_each迭代器:vector::iterator示例:#include<iostream>usin......
  • 【唐叔学算法】第十天:广度优先遍历-探索图结构的逐层之旅
    你是否曾为如何高效地解决图论中的搜索问题而苦恼?广度优先遍历算法,就像一位经验丰富的探险家,能帮你轻松探索图中的每个角落。今天,就让我们一起揭开广度优先遍历算法的神秘面纱,探索它在Java编程中的应用。一、什么是广度优先遍历?定义广度优先遍历是一种用于遍历或搜索图(Gr......
  • CF2040D Non Prime Tree 题解
    CF992Div2D-solution给定一个\(n\)个节点的树,你可以不重复地给树的节点填\(1\sim2n\)之间的数,求一种构造方案,使得每两个相邻的节点上的数之差的绝对值为合数。我们规定每次填的数只会变大(就是在以某种方法遍历的时候后面的数一定比前面的数大)。现在我们假设填到了\(u\)......
  • 协同过滤推荐算法的个性化购物商城系统
    感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人2025最新毕业设计项目推荐-SSM篇2025最新毕业设计项目推荐-SpringBoot篇2025最新毕业设计项目推荐-小程序、uniapp篇-CSDN博客Java精品毕设实战案例推荐​基......
  • Horovod的梯度聚合,参数同步原理;加权梯度聚合算法
    目录Horovod的梯度聚合,参数同步原理梯度聚合参数同步举例说明梯度聚合方式梯度聚合的方式举例说明加权梯度聚合算法Horovod的梯度聚合,参数同步原理Horovod(hvd)是一个用于分布式深度学习的框架,它能够实现高效的梯度聚合和参数同步。 梯度聚合梯度聚合是分布式训......
  • optim.Adam优化算法
    目录optim.Adam优化算法optim.Adam优化算法是PyTorch中实现Adam优化算法的类,它是一种用于训练深度神经网络的优化器。Adam优化器结合了AdaGrad和RMSProp的优点,通过计算梯度的一阶矩估计(即平均值)和二阶矩估计(即未中心的方差)来动态调整每个参数的学习率,使得训练过......