数据结构与算法 - 最小生成树
我们定义无向连通图的 最小生成树(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)