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

最小生成树(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
    }
    */
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值