数据结构与算法 - 最小生成树
我们定义无向连通图的 最小生成树(Minimum Spanning Tree,MST)为边权和最小的生成树。
注意:只有连通图才有生成树,而对于非连通图,只存在生成森林。
1584. 连接所有点的最小费用
题目链接:https://leetcode-cn.com/problems/min-cost-to-connect-all-points/
给你一个 points 数组,表示 2D 平面上的一些点,其中 points [i] = [xi, yi] 。
连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。
请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。
示例:
输入:points = [[0,0],[2,2],[3,10],[5,2],[7,0]] 输出:20
Kruskal 算法
Kruskal 算法是一种常见并且好写的最小生成树算法,由 Kruskal 发明。该算法的基本思想是从小到大加入边,是个贪心算法。
前置知识:
- 并查集:数据结构与算法 - 并查集)
- 贪心
- 图的存储(直接存边,邻接矩阵,邻接表)
代码实现:
// 这里是基础的并查集实现 class UnionFind { private: vector<int> parent; int count; public: UnionFind(int n) : count(n) { parent.resize(n); for (int i = 0; i < n; i++) parent[i] = i; } int Count() { return count;} int Find(int n) { return n == parent[n] ? n : parent[n] = Find(parent[n]); } void Union(int a, int b) { int rootA = Find(a), rootB = Find(b); if (rootA != rootB) count--, parent[rootA] = rootB; } }; // 这个结构体使用直接存边的方式存图 struct Edge { int len, x, y; Edge(int len = 0, int x = 0, int y = 0) : len(len), x(x), y(y) {}; }; class Solution { public: int minCostConnectPoints(vector<vector<int>>& points) { auto caldist = [&](int i, int j) -> int { return abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]); }; // 1. 建图 存储方式:直接存边 int N = points.size(); UnionFind uf(N); vector<Edge> edges; for (int i = 0; i < N; i++) for (int j = i + 1; j < N; j++) { edges.emplace_back(caldist(i, j), i, j); } // 2. 排序所有的边,贪心选择较短的边 int cost = 0; sort(edges.begin(), edges.end(), [](Edge &a, Edge &b) { return a.len < b.len; }); for (auto & edge : edges) { // 如果这条边的左右顶点已经连接上 则跳过这条边 否则这条边加入最小生成树 if (uf.Find(edge.x) == uf.Find(edge.y)) continue; uf.Union(edge.x, edge.y); cost += edge.len; if (uf.Count() == 1) return cost; // 最小生成树已经生成 } return 0; } };
运行结果:
Accepted 72/72 cases passed (532 ms) Your runtime beats 58.35 % of cpp submissions Your memory usage beats 25.13 % of cpp submissions (56.9 MB)
Prim 算法
Prim 算法是一种常见并且好写的最小生成树算法。该算法的基本思想是从一个结点开始,不断加点(而不是 Kruskal 算法的加边)。
在稠密图尤其是完全图上,暴力 Prim 的复杂度比 Kruskal 优,但不一定实际跑得更快。
代码实现:
// Distance 用来存储定点到最小生成树的距离 struct Distance { int index /* 定点在 points 中的索引值 */, len /* 定点到最小生成树的距离 */; Distance(int index = 0, int len = 0) : index(index), len(len) {}; bool operator <(const Distance& a) const { return len > a.len; } }; class Solution { public: int minCostConnectPoints(vector<vector<int>>& points) { auto caldist = [&](int i, int j) -> int { return abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]); }; int N = points.size(); vector<int> visited(N, 0); int cost = 0; priority_queue<Distance> Q; // 优先队列 用来查找距离最小生成树最近的点 Q.push(Distance{0, 0}); while (!Q.empty()) { Distance e = Q.top(); Q.pop(); if (!visited[e.index]) { cost += e.len; visited[e.index] = 1; // 将找到的最近的点加入最小生成树 for (int i = 0; i < N; i++) { // 遍历计算未加入点距离最小生成树的距离 这里不是最小距离 最小距离由优先队列选择 if (!visited[i] && i != e.index) Q.push(Distance{i, caldist(i, e.index)}); } } } return cost; } };
运行结果:
Accepted 72/72 cases passed (668 ms) Your runtime beats 24.34 % of cpp submissions Your memory usage beats 64.91 % of cpp submissions (41.3 MB)
特殊的 Prim 算法
对于稠密图来说,有一种特殊的 Prim 算法可以达到 O (n^2) 的时间复杂度
class Solution { public: int minCostConnectPoints(vector<vector<int>>& points) { auto caldist = [&](int i, int j) -> int { return abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]); }; // 1. 建图 存储方式:邻接矩阵 int N = points.size(); vector<vector<int>> edges(N, vector<int>(N, 0)); for (int i = 0; i < N; i++) for (int j = i + 1; j < N; j++) edges[i][j] = edges[j][i] = caldist(i, j); vector<int> distance(N, INT_MAX), visited(N, 0); distance[0] = 0; // 将 0 点加入最小生成树 for (int i = 0; i < N; i++) { int next = -1; for (int j = 0; j < N; j++) { // 在剩余定点中,找到最小路径值的定点 if (!visited[j] && (next == -1 || distance[j] < distance[next])) next = j; } visited[next] = true; // 将找到最小路径值的定点加入最小生成树 for (int k = 0; k < N; k++) { // 加入新定点后,导致连通分量到其他定点距离变化 if (!visited[k]) distance[k] = min(distance[k], edges[next][k]); } } return accumulate(distance.begin(), distance.end(), 0); } };
运行结果:
Accepted 72/72 cases passed (128 ms) Your runtime beats 82.67 % of cpp submissions Your memory usage beats 75.12 % of cpp submissions (26.1 MB)