1584. 连接所有点的最小费用
1584. 连接所有点的最小费用
一道最小生成树的算法题,适合用于巩固 \(Kruskal\) 算法。
1. 代码
// 交换两个整数的值(用于按秩合并时的变量交换)
void swap(int* a, int* b) {
int tmp = *a;
*a = *b, *b = tmp;
}
// 定义边结构体
struct Edge {
int len, x, y; // len: 边的权重(距离),x 和 y: 边连接的两个点
};
// 排序比较函数:按边长 len 从小到大升序排列
int cmp(const void* a, const void* b) { // 不允许通过这个指针修改它指向的数
return ((struct Edge*)a)->len - ((struct Edge*)b)->len;
}
// 并查集:查找根节点(包含路径压缩优化,将树的高度降低)
int find(int* f, int x) {
return f[x] == x ? x : (f[x] = find(f, f[x]));
} // f[i] = i 父亲是自己,说明他就是掌门人
// 并查集:合并两个集合
int unionSet(int* f, int* rank, int x, int y) {
int fx = find(f, x), fy = find(f, y);
if (fx == fy) {
return false; // 如果已经在同一个集合中,说明连接它们会形成环,返回失败
}
// 按秩合并优化:始终将较小的树合并到较大的树上
if (rank[fx] < rank[fy]) {
swap(&fx, &fy);
}
rank[fx] += rank[fy];
f[fy] = fx;
return true; // 合并成功
}
int minCostConnectPoints(int** points, int pointsSize, int* pointsColSize) {
int n = pointsSize;
// 初始化边集数组:n 个点最多有 n*(n-1)/2 条边
struct Edge edges[(n + 1) * n / 2];
int edgesSize = 0;
// 第一步:计算所有点对之间的曼哈顿距离,构建完整的边集
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
edges[edgesSize].x = i;
edges[edgesSize].y = j;
// 曼哈顿距离公式:|x1 - x2| + |y1 - y2|
edges[edgesSize++].len = abs(points[i][0] - points[j][0]) +
abs(points[i][1] - points[j][1]);
}
}
// 第二步:将所有边按距离从小到大排序(贪心策略的核心)
qsort(edges, edgesSize, sizeof(struct Edge), cmp);
// 第三步:并查集初始化
int f[n], rank[n];
for (int i = 0; i < n; i++) {
f[i] = i; // 初始时每个点的父节点是它自己
rank[i] = 1; // 初始秩(集合大小)为 1
}
int ret = 0; // 累计最小生成树的总权重
int num = 1; // 记录当前已连接的点数(当连通点数等于 n 时,边数为 n-1)
// 第四步:遍历排序后的边,依次尝试加入
for (int i = 0; i < edgesSize; i++) {
// 如果这条边连接了两个尚未连通的集合
if (unionSet(f, rank, edges[i].x, edges[i].y)) {
ret += edges[i].len; // 记录权重
num++; // 增加已连接的点数
if (num == n) { // 优化:如果所有点都已连通,提前结束
break;
}
}
}
return ret;
}
下面进行拆解。
2. 工具函数
// 交换两个整数的值(用于按秩合并时的变量交换)
void swap(int* a, int* b) {
int tmp = *a;
*a = *b, *b = tmp;
}
// 定义边结构体
struct Edge {
int len, x, y; // len: 边的权重(距离),x 和 y: 边连接的两个点
};
// 排序比较函数:按边长 len 从小到大升序排列
int cmp(const void* a, const void* b) { // 不允许通过这个指针修改它指向的数
return ((struct Edge*)a)->len - ((struct Edge*)b)->len;
}
3. 并查集
// 并查集:查找根节点(包含路径压缩优化,将树的高度降低)
int find(int* f, int x) {
return f[x] == x ? x : (f[x] = find(f, f[x]));
}
// 并查集:合并两个集合
int unionSet(int* f, int* rank, int x, int y) {
int fx = find(f, x), fy = find(f, y);
if (fx == fy) {
return false; // 如果已经在同一个集合中,说明连接它们会形成环,返回失败
}
// 按秩合并优化:始终将较小的树合并到较大的树上
if (rank[fx] < rank[fy]) {
swap(&fx, &fy);
}
rank[fx] += rank[fy];
f[fy] = fx;
return true; // 合并成功
}
f[]是一个集合。f[i]表示节点的父亲,如果f[i] = i,也就是父亲为自己,说明没有父亲,此时i是根结点。- 路径压缩的关键代码:
f[x] = find(f, f[x])。 - 并查集的合并在王道书上有讲过,见 [[【第六章】图]]的 \(Kruskal\) 算法部分。
4. 主程序
int minCostConnectPoints(int** points, int pointsSize, int* pointsColSize) {
int n = pointsSize;
// 初始化边集数组:n 个点最多有 n*(n-1)/2 条边
struct Edge edges[(n + 1) * n / 2];
int edgesSize = 0;
// 第一步:计算所有点对之间的曼哈顿距离,构建完整的边集
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
edges[edgesSize].x = i;
edges[edgesSize].y = j;
// 曼哈顿距离公式:|x1 - x2| + |y1 - y2|
edges[edgesSize++].len = abs(points[i][0] - points[j][0]) +
abs(points[i][1] - points[j][1]);
}
}
// 第二步:将所有边按距离从小到大排序(贪心策略的核心)
qsort(edges, edgesSize, sizeof(struct Edge), cmp);
// 第三步:并查集初始化
int f[n], rank[n];
for (int i = 0; i < n; i++) {
f[i] = i; // 初始时每个点的父节点是它自己
rank[i] = 1; // 初始秩(集合大小)为 1
}
int ret = 0; // 累计最小生成树的总权重
int num = 1; // 记录当前已连接的点数(当连通点数等于 n 时,边数为 n-1)
// 第四步:遍历排序后的边,依次尝试加入
for (int i = 0; i < edgesSize; i++) {
// 如果这条边连接了两个尚未连通的集合
if (unionSet(f, rank, edges[i].x, edges[i].y)) {
ret += edges[i].len; // 记录权重
num++; // 增加已连接的点数
if (num == n) { // 优化:如果所有点都已连通,提前结束
break;
}
}
}
return ret;
}

浙公网安备 33010602011771号