Leetcode刷题 | Day61_图论07

 一、学习任务

  • 最小生成树——prim算法代码随想录
  • 最小生成树——kruskal算法代码随想录

Kruskal 与 prim 的关键区别在于,prim维护的是节点的集合,而 Kruskal 维护的是边的集合。

  • 在节点数量固定的情况下,图中的边越少,Kruskal 需要遍历的边也就越少。
  • 而 prim 算法是对节点进行操作的,节点数量越少,prim算法效率就越优。

边数量较少为稀疏图,接近或等于完全图(所有节点皆相连)为稠密图。

所以在 稀疏图中,用Kruskal更优。 在稠密图中,用prim算法更优。

  • Prim 算法 时间复杂度为 O(n^2),其中 n 为节点数量,它的运行效率和图中边树无关,适用稠密图。
  • Kruskal算法 时间复杂度 为 nlogn,其中n 为边的数量,适用稀疏图。

二、具体题目

1.prim算法53. 寻宝(第七期模拟笔试)

【题目描述】

在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路,方便运输。

不同岛屿之间,路途距离不同,国王希望你可以规划建公路的方案,如何可以以最短的总公路距离将所有岛屿联通起来。

给定一张地图,其中包括了所有的岛屿,以及它们之间的距离。以最小化公路建设长度,确保可以链接到所有岛屿。

【输入描述】

第一行包含两个整数V和E,V代表顶点数,E代表边数。顶点编号是从1到V。例如:V=2,一个有两个顶点,分别是1和2。

接下来共有E行,每行三个整数v1,v2和val,v1和v2为边的起点和终点,val代表边的权值。

【输出描述】

输出联通所有岛屿的最小路径总距离

注意:

  1. 本题是最小生成树的模板题。最小生成树可以使用prim算法也可以使用kruskal算法计算出来。
  2. 最小生成树是所有节点的最小连通子图,即:以最小的成本(边的权值)将图中所有节点链接到一起。
  3. 图中有n个节点,那么一定可以用n-1条边将所有节点连接到一起。那么如何选择这n-1条边就是最小生成树算法的任务所在。

prim算法是从节点的角度采用贪心的策略每次寻找距离最小生成树最近的节点并加入到最小生成树中。

prim三部曲

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

画图举例阐述prim三部曲以及minDist数组的作用,具体见代码随想录。

在prim算法中,有一个数组特别重要,这里起名为:minDist。

minDist数组用来记录每一个节点距离最小生成树的最近距离(最小生成树所有点里距离每个点最近的那个)。理解这一点非常重要。

#include
#include
#include
using namespace std;

int main() {
    int v, e, x, y, k;
    cin >> v >> e;
    // 填一个默认最大值,题目描述val最大为10000
    vector> grid(v + 1, vector(v + 1, 10001));
    while(e--) {
        cin >> x >> y >> k;
        // 因为是双向图,所以两个方向都要填上
        grid[x][y] = k;
        grid[y][x] = k;
    }
    // 所有节点到最小生成树的最小距离
    vector minDist(v + 1, 10001);
    // 这个节点是否在树里
    vector isInTree(v + 1, false);
    // 我们只需要循环 n-1次,建立 n - 1条边,就可以把n个节点的图连在一起
    for (int i = 1; i < v; i++) {
        // 1、prim三部曲,第一步:选距离生成树最近节点
        int cur = -1; // 选中哪个节点,加入最小生成树
        int minVal = INT_MAX;
        for (int j = 1; j <= v; j++) { // 1 - v,顶点编号,这里下标从1开始
            //  选取最小生成树节点的条件:
            //  (1)不在最小生成树里
            //  (2)距离最小生成树最近的节点
            if (isInTree[j] == false && minDist[j] < minVal) {
                minVal = minDist[j];
                cur = j;
            }
        }
        // 2、prim三部曲,第二步:最近节点(cur)加入生成树
        isInTree[cur] = true;

         // 3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组)
        // cur节点加入之后, 最小生成树加入了新的节点,那么所有节点到 最小生成树的距离(即minDist数组)需要更新一下
        // 由于cur节点是新加入到最小生成树,那么只需要关心与 cur 相连的 非生成树节点 的距离 是否比 原来非生成树节点到生成树节点的距离更小 即可
        for (int j = 1; j <= v; j++) {
            // 更新的条件:
            // (1)节点是 非生成树里的节点
            // (2)与cur相连的某节点的权值 比 该某节点距离最小生成树的距离小
            // cur 是新加入最小生成树的节点,那么 所有非生成树的节点距离生成树节点的最近距离 由于 cur的新加入,需要更新一下数据了
            if (isInTree[j] == false && grid[cur][j] < minDist[j]) {
                minDist[j] = grid[cur][j]; // 得到更小的最小距离,就更新
                // 这里grid[cur][j]和grid[j][cur]都是一样的,因为无向图的存储是一个对称的矩阵
            }
        }
    }
    // 统计结果
    int result = 0;
    for (int i = 2; i <= v; i++) { // 不计第一个顶点,因为统计的是边的权值,v个节点有 v-1条边
        result += minDist[i];
    }
    cout << result << endl;
    return 0;
}

Prim算法的核心流程:

  1. v-1轮选择了v-1个点
    • 第1轮选择起始点(如节点1)
    • 接下来v-2轮分别选择剩余的v-2个点(除了最后一个点)
  2. 中间通过v-2条边的判断
    • 在第2轮到第v-1轮,每轮选择一条边将一个新节点连接到已有的树中
    • 这样,总共明确选择了v-2条边
  3. 最后一个v-1边是隐式考虑的
    • 在第v-1轮结束后,最后一个点(第v个点)虽然没有被显式选择
    • 但它到树的最小距离已经被计算出来(保存在minDist[v]中)
    • 当我们计算最终结果时,我们包含了这条隐式的第v-1条边

这正是Prim算法的巧妙之处 - 不需要显式地进行第v轮迭代,因为第v-1条边(连接最后一个点到树的边)已经在前面v-1轮的更新中被确定了。

算法在处理有v个节点的图时只需要v-1轮迭代,恰好对应于最小生成树的v-1条边。

2.kruskal算法53. 寻宝(第七期模拟笔试)

【题目描述】

在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路,方便运输。

不同岛屿之间,路途距离不同,国王希望你可以规划建公路的方案,如何可以以最短的总公路距离将所有岛屿联通起来。

给定一张地图,其中包括了所有的岛屿,以及它们之间的距离。以最小化公路建设长度,确保可以链接到所有岛屿。

【输入描述】

第一行包含两个整数V和E,V代表顶点数,E代表边数。顶点编号是从1到V。例如:V=2,一个有两个顶点,分别是1和2。

接下来共有E行,每行三个整数v1,v2和val,v1和v2为边的起点和终点,val代表边的权值。

【输出描述】

输出联通所有岛屿的最小路径总距离

prim 算法是维护节点的集合,而 Kruskal 是维护边的集合

kruscal的思路:

  • 边的权值排序,因为要优先选最小的边加入到生成树里
  • 遍历排序后的边(利用并查集)
    • 如果边首尾的两个节点在同一个集合,说明如果连上这条边图中会出现环
    • 如果边首尾的两个节点不在同一个集合,加入到最小生成树,并把两个节点加入同一个集合

具体过程见代码随想录。

#include 
#include 
#include 

using namespace std;

// l,r为 边两边的节点,val为边的数值
struct Edge {
    int l, r, val;
};
// 节点数量
int n = 10001;
// 并查集标记节点关系的数组
vector 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 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;
    });

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

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

        // 如果祖先不同,则不在同一个集合
        if (x != y) {
            result_val += edge.val; // 这条边可以作为生成树的边
            join(x, y); // 两个节点加入到同一个集合
        }
    }
    cout << result_val << endl;
    return 0;
}

你可能感兴趣的:(leetcode,图论,算法,数据结构,c++)