「代码随想录算法训练营」第四十九天 | 图论 part7

最小生成树的解题

最小生成树类型的题目主要用于解决所有节点的最小连通子图的问题,即:以最小的成本(边的权值)将图中所有节点链接到一起。

最小生成树可以使用prim算法,也可以使用kruskal算法计算出来。

prim算法

prim算法的核心有三步:

  1. 第一步,选距离生成树最近节点。
  2. 第二步,最近节点加入生成树。
  3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组)

其中,minDist数组最为重要,用来记录每一个节点距离最小生成树的最近距离

举例说明(来自代码随想录)

下面是一个无向有权图:

1. minDist初始化:

minDist数组里的数值初始化为最大数,本例中最大值不超过10000,所以初始化最大数为10001就可以。

2. 最初随机选择一个节点加入生成树(这里选择节点1),并更新minDist数组:

选择距离最小生成树最近的节点,加入到最小生成树,刚开始还没有最小生成树,所以随便选一个节点加入就好(因为每一个节点一定会在最小生成树里,所以随便选一个就好),那我们选择节点1 (符合遍历数组的习惯,第一个遍历的也是节点1)

3. 在minDist随机选择一个最小距离的节点加入生成树(这里选择节点2),并更新minDist数组:

4. 继续选择最小距离的节点加入生成树(这里选择节点3),并更新minDist数组:

5. 继续选择最小的加入生成树(这里选择节点4),并更新minDist数组,注意,此时节点5的距离改变了:

6. 重复上述,加入节点5,继续:

7. 继续,加入节点6:

8. 继续,加入节点7:

9. 最后把minDist数组的值累加,得到总距离,当然,也可以输出路径。

题目:53. 寻宝

题目链接:https://kamacoder.com/problempage.php?pid=1053
文章讲解:https://www.programmercarl.com/kamacoder/0053.寻宝-prim.html
题目状态:看题解

思路:

prim算法,看上面。

代码:

#include <iostream>
#include <vector>
#include <climits>

using namespace std;

int main()
{
    int v, e;
    int x, y, k;
    cin >> v >> e;
    // 默认最大值
    vector<vector<int>> grid(v + 1, vector<int>(v + 1, 10001));
    while(e--)
    {
        cin >> x >> y >> k;
        // 因为是双向图,两个方向都要填上
        grid[x][y] = k;
        grid[y][x] = k;
    }
    // 所有节点到最小生成树的最小距离
    vector<int> minDist(v + 1, 10001);
    // 这个节点是否在树里
    vector<bool> isInTree(v + 1, false);

    // 初始化,用来记录边的连接
    vector<int> parent(v + 1, -1);

    // 循环n-1次,建立n-1条边,将n个节点连在一起
    for(int i = 1; i < v; ++i)
    {
        // 第一步:选距离生成树最近的节点
        int cur = -1; // 选择节点加入生成树
        int minVal = INT_MAX;
        for(int j = 1; j <= v; ++j)
        {
            // 选取条件
            // 1. 不在最小生成树里
            // 2. 距离最小生成树最近的节点
            if(!isInTree[j] && minDist[j] < minVal)
            {
                minVal = minDist[j];
                cur = j;
            }
        }
        // 第二步:最近节点加入生成树
        isInTree[cur] = true;
        // 第三步:更新非生成树节点到生成树的距离
        for(int j = 1; j <= v; ++j)
        {
            if(!isInTree[j] && grid[cur][j] < minDist[j])
            {
                minDist[j] = grid[cur][j];

                // 记录边
                parent[j] = cur;
            }
        }
    }

    // 统计结果
    int result = 0;
    for(int i = 2; i <= v; ++i) result += minDist[i];
    cout << result << endl;

    // 输出最小生成树边的连接情况
    for(int i = 1; i <= v; ++i) cout << i << "->" << parent[i] << endl;
}

Kruskal算法

Kruskal算法是维护边的集合。思路如下:

  • 边的权重排序,因为要有限选最小的边加入到生成树里。
  • 遍历排序后的边。
    • 如果边首尾的两个节点在同一个集合,说明如果连上这条边图中会出现环。
    • 如果边首尾的两个节点不在同一个结合,加入到最小生成树,并把两个节点加入同一个集合。

举例说明(来自代码随想录)

排序后的边顺序为[(1,2) (4,5) (1,3) (2,6) (3,4) (6,7) (5,7) (1,5) (3,2) (2,4) (5,6)]

使用并查集来将两个节点加入同一个集合,并且判断两个节点是否在同一个集合中。

流程如下:

题目:53. 寻宝

题目链接:https://kamacoder.com/problempage.php?pid=1053
文章讲解:https://www.programmercarl.com/kamacoder/0053.寻宝-prim.html
题目状态:看题解

思路:

Kruskal算法,看上面。

代码:

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

using namespace std;

// l、r为两边的节点,val为边的权值
struct Edge
{
    int l, r, val;
};

// 节点数量
int n = 10001;
// 并查集标记节点关系的数组
vector<int> father(n, -1); // 节点编号是从1开始的,n要大一些

// 并查集初始化
void init()
{
    for(int i = 0; i < n; ++i)
    {
        father[i] = i;
    }
}

// 并查集的查找操作
int find(int u)
{
    return u == father[u] ? u : father[u] = find(father[u]);
    // 路径压缩
}

// 并查集的加入集合
void join(int u, int v)
{
    u = find(u); // 寻找u的根
    v = find(v); // 寻找v的根
    if(u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
    father[v] = u;
}

int main()
{
    int v, e;
    int v1, v2, val;
    vector<Edge> edges;
    int result_val = 0;
    cin >> v >> e;
    while(e--)
    {
        cin >> v1 >> v2 >> val;
        edges.push_back({v1, v2, val});
    }

    // 执行kruskal算法
    // 按边的权限对边进行从小到大排序
    sort(edges.begin(), edges.end(), [](const Edge &a, const Edge &b) {
        return a.val < b.val;
    });

    vector<Edge> result; // 存储最小生成树的边

    // 并查集初始化
    init();

    // 从头开始遍历边
    for(Edge &edge : edges)
    {
        // 并查集,搜出两个节点的祖先
        int x = find(edge.l);
        int y = find(edge.r);

        // 如果祖先不同,则不在同一个集合
        if(x != y)
        {
            result.push_back(edge); // 保存最小生成树的边
            result_val += edge.val; // 这条边可以作为生成树的边
            join(x, y); // 两个节点加入到同一个集合
        }
    }
    cout << result_val << endl;
    
    // 打印最小生成树的边
    for(auto &edge : result)
    {
        cout << edge.l << "-" << edge.r << " : " << edge.val << endl;
    }
    return 0;
}

Kruskal与prim的关键区别在于,prim维护的是节点的集合,而Kruskal维护的是边的集合。 如果一个图中,节点多,但边相对较少,那么使用Kruskal更优。